diff --git a/client/shared/model/config.ts b/client/shared/model/config.ts index 5c7173f9..6356f35f 100644 --- a/client/shared/model/config.ts +++ b/client/shared/model/config.ts @@ -20,6 +20,11 @@ export interface GlobalConfig { * 服务器名 */ serverName?: string; + + /** + * 服务器入口背景图 + */ + serverEntryImage?: string; } export function getGlobalConfig(): GlobalConfig { diff --git a/client/web/src/routes/Entry/index.tsx b/client/web/src/routes/Entry/index.tsx index 5e4980f9..15207b71 100644 --- a/client/web/src/routes/Entry/index.tsx +++ b/client/web/src/routes/Entry/index.tsx @@ -8,10 +8,16 @@ import { RegisterView } from './RegisterView'; import { useRecordMeasure } from '@/utils/measure-helper'; import { GuestView } from './GuestView'; import { ForgetPasswordView } from './ForgetPasswordView'; +import { Helmet } from 'react-helmet'; +import { parseUrlStr, useGlobalConfigStore } from 'tailchat-shared'; const EntryRoute = React.memo(() => { useRecordMeasure('appEntryRenderStart'); + const serverEntryImage = useGlobalConfigStore( + (state) => state.serverEntryImage + ); + return (
{
+ {serverEntryImage && ( + + + + )} +
); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3fb9f915..7570cbe3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1007,6 +1007,7 @@ importers: specifiers: '@fastify/busboy': ^1.1.0 '@mui/icons-material': ^5.11.0 + '@mui/lab': 5.0.0-alpha.122 '@mui/material': ^5.11.3 '@remix-run/dev': ^1.9.0 '@remix-run/express': ^1.9.0 @@ -1048,6 +1049,7 @@ importers: dependencies: '@fastify/busboy': 1.1.0 '@mui/icons-material': 5.11.0_oiuuhmk4wjjpe4qb2sby7usney + '@mui/lab': 5.0.0-alpha.122_g64p43h6yqqjvdymvb55np57cu '@mui/material': 5.11.3_ib3m5ricvtkl2cll7qpr2f6lvq '@remix-run/express': 1.9.0_cwk4saenierp7pa7l5cpbeswge '@remix-run/node': 1.9.0_biqbaboplfbrettd7655fr4n2y @@ -8847,7 +8849,31 @@ packages: '@babel/runtime': 7.21.0 '@emotion/is-prop-valid': 1.2.0 '@mui/types': 7.2.3_@types+react@18.0.26 - '@mui/utils': 5.11.2_react@18.2.0 + '@mui/utils': 5.11.12_react@18.2.0 + '@popperjs/core': 2.11.6 + '@types/react': 18.0.26 + clsx: 1.2.1 + prop-types: 15.8.1 + react: 18.2.0 + react-dom: 18.2.0_react@18.2.0 + react-is: 18.2.0 + dev: false + + /@mui/base/5.0.0-alpha.120_ib3m5ricvtkl2cll7qpr2f6lvq: + resolution: {integrity: sha512-UoIXLjbl8ghK7OSD1dYzHIj79sx9v5S2J7vYeuhxUS0QR0FwGZ3WLHd31TQ2CT2faPX/AXsHQeFn93wKSnjPUQ==} + engines: {node: '>=12.0.0'} + peerDependencies: + '@types/react': ^17.0.0 || ^18.0.0 + react: ^17.0.0 || ^18.0.0 + react-dom: ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@babel/runtime': 7.21.0 + '@emotion/is-prop-valid': 1.2.0 + '@mui/types': 7.2.3_@types+react@18.0.26 + '@mui/utils': 5.11.12_react@18.2.0 '@popperjs/core': 2.11.6 '@types/react': 18.0.26 clsx: 1.2.1 @@ -8878,6 +8904,38 @@ packages: react: 18.2.0 dev: false + /@mui/lab/5.0.0-alpha.122_g64p43h6yqqjvdymvb55np57cu: + resolution: {integrity: sha512-rJyu9llUWAluUQgDEmN0WrpcFxeTdJgu+XYriJtp/MchdvKl/qVTlx+vhnIhqas2bySj5N1VQnkI6qOvfXiYvQ==} + engines: {node: '>=12.0.0'} + peerDependencies: + '@emotion/react': ^11.5.0 + '@emotion/styled': ^11.3.0 + '@mui/material': ^5.0.0 + '@types/react': ^17.0.0 || ^18.0.0 + react: ^17.0.0 || ^18.0.0 + react-dom: ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@emotion/react': + optional: true + '@emotion/styled': + optional: true + '@types/react': + optional: true + dependencies: + '@babel/runtime': 7.21.0 + '@mui/base': 5.0.0-alpha.120_ib3m5ricvtkl2cll7qpr2f6lvq + '@mui/material': 5.11.3_ib3m5ricvtkl2cll7qpr2f6lvq + '@mui/system': 5.11.12_kzbn2opkn2327fwg5yzwzya5o4 + '@mui/types': 7.2.3_@types+react@18.0.26 + '@mui/utils': 5.11.12_react@18.2.0 + '@types/react': 18.0.26 + clsx: 1.2.1 + prop-types: 15.8.1 + react: 18.2.0 + react-dom: 18.2.0_react@18.2.0 + react-is: 18.2.0 + dev: false + /@mui/material/5.11.3_af5ln35zuaotaffazii6n6bke4: resolution: {integrity: sha512-Oz+rMFiMtxzzDLUxKyyj4mSxF9ShmsBoJ9qvglXCYqklgSrEl1R/Z4hfPZ+2pWd5CriO8U/0CFHr4DksrlTiCw==} engines: {node: '>=12.0.0'} @@ -8984,8 +9042,8 @@ packages: react-transition-group: 4.4.5_biqbaboplfbrettd7655fr4n2y dev: false - /@mui/private-theming/5.11.2_kzbn2opkn2327fwg5yzwzya5o4: - resolution: {integrity: sha512-qZwMaqRFPwlYmqwVKblKBGKtIjJRAj3nsvX93pOmatsXyorW7N/0IPE/swPgz1VwChXhHO75DwBEx8tB+aRMNg==} + /@mui/private-theming/5.11.12_kzbn2opkn2327fwg5yzwzya5o4: + resolution: {integrity: sha512-hnJ0svNI1TPeWZ18E6DvES8PB4NyMLwal6EyXf69rTrYqT6wZPLjB+HiCYfSOCqU/fwArhupSqIIkQpDs8CkAw==} engines: {node: '>=12.0.0'} peerDependencies: '@types/react': ^17.0.0 || ^18.0.0 @@ -8995,7 +9053,7 @@ packages: optional: true dependencies: '@babel/runtime': 7.21.0 - '@mui/utils': 5.11.2_react@18.2.0 + '@mui/utils': 5.11.12_react@18.2.0 '@types/react': 18.0.26 prop-types: 15.8.1 react: 18.2.0 @@ -9040,8 +9098,30 @@ packages: react: 18.2.0 dev: false - /@mui/styled-engine/5.11.0_react@18.2.0: - resolution: {integrity: sha512-AF06K60Zc58qf0f7X+Y/QjaHaZq16znliLnGc9iVrV/+s8Ln/FCoeNuFvhlCbZZQ5WQcJvcy59zp0nXrklGGPQ==} + /@mui/styled-engine/5.11.11_hfzxdiydbrbhhfpkwuv3jhvwmq: + resolution: {integrity: sha512-wV0UgW4lN5FkDBXefN8eTYeuE9sjyQdg5h94vtwZCUamGQEzmCOtir4AakgmbWMy0x8OLjdEUESn9wnf5J9MOg==} + engines: {node: '>=12.0.0'} + peerDependencies: + '@emotion/react': ^11.4.1 + '@emotion/styled': ^11.3.0 + react: ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@emotion/react': + optional: true + '@emotion/styled': + optional: true + dependencies: + '@babel/runtime': 7.21.0 + '@emotion/cache': 11.10.5 + '@emotion/react': 11.10.4_kzbn2opkn2327fwg5yzwzya5o4 + '@emotion/styled': 11.10.4_5edjfi7y5ucoavvp3vhupkkowq + csstype: 3.1.1 + prop-types: 15.8.1 + react: 18.2.0 + dev: false + + /@mui/styled-engine/5.11.11_react@18.2.0: + resolution: {integrity: sha512-wV0UgW4lN5FkDBXefN8eTYeuE9sjyQdg5h94vtwZCUamGQEzmCOtir4AakgmbWMy0x8OLjdEUESn9wnf5J9MOg==} engines: {node: '>=12.0.0'} peerDependencies: '@emotion/react': ^11.4.1 @@ -9060,6 +9140,34 @@ packages: react: 18.2.0 dev: false + /@mui/system/5.11.12_kzbn2opkn2327fwg5yzwzya5o4: + resolution: {integrity: sha512-sYjsXkiwKpZDC3aS6O/6KTjji0jGINLQcrD5EJ5NTkIDiLf19I4HJhnufgKqlTWNfoDBlRohuTf3TzfM06c4ug==} + engines: {node: '>=12.0.0'} + peerDependencies: + '@emotion/react': ^11.5.0 + '@emotion/styled': ^11.3.0 + '@types/react': ^17.0.0 || ^18.0.0 + react: ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@emotion/react': + optional: true + '@emotion/styled': + optional: true + '@types/react': + optional: true + dependencies: + '@babel/runtime': 7.21.0 + '@mui/private-theming': 5.11.12_kzbn2opkn2327fwg5yzwzya5o4 + '@mui/styled-engine': 5.11.11_react@18.2.0 + '@mui/types': 7.2.3_@types+react@18.0.26 + '@mui/utils': 5.11.12_react@18.2.0 + '@types/react': 18.0.26 + clsx: 1.2.1 + csstype: 3.1.1 + prop-types: 15.8.1 + react: 18.2.0 + dev: false + /@mui/system/5.11.2_4mv32nu4vciambuqqzuu4gtvj4: resolution: {integrity: sha512-PPkYhrcP2MkhscX6SauIl0wPgra0w1LGPtll+hIKc2Z2JbGRSrUCFif93kxejB7I1cAoCay9jWW4mnNhsOqF/g==} engines: {node: '>=12.0.0'} @@ -9107,10 +9215,10 @@ packages: optional: true dependencies: '@babel/runtime': 7.21.0 - '@mui/private-theming': 5.11.2_kzbn2opkn2327fwg5yzwzya5o4 - '@mui/styled-engine': 5.11.0_react@18.2.0 + '@mui/private-theming': 5.11.12_kzbn2opkn2327fwg5yzwzya5o4 + '@mui/styled-engine': 5.11.11_react@18.2.0 '@mui/types': 7.2.3_@types+react@18.0.26 - '@mui/utils': 5.11.2_react@18.2.0 + '@mui/utils': 5.11.12_react@18.2.0 '@types/react': 18.0.26 clsx: 1.2.1 csstype: 3.1.1 @@ -9137,10 +9245,10 @@ packages: '@babel/runtime': 7.21.0 '@emotion/react': 11.10.4_kzbn2opkn2327fwg5yzwzya5o4 '@emotion/styled': 11.10.4_5edjfi7y5ucoavvp3vhupkkowq - '@mui/private-theming': 5.11.2_kzbn2opkn2327fwg5yzwzya5o4 - '@mui/styled-engine': 5.11.0_hfzxdiydbrbhhfpkwuv3jhvwmq + '@mui/private-theming': 5.11.12_kzbn2opkn2327fwg5yzwzya5o4 + '@mui/styled-engine': 5.11.11_hfzxdiydbrbhhfpkwuv3jhvwmq '@mui/types': 7.2.3_@types+react@18.0.26 - '@mui/utils': 5.11.2_react@18.2.0 + '@mui/utils': 5.11.12_react@18.2.0 '@types/react': 18.0.26 clsx: 1.2.1 csstype: 3.1.1 @@ -9170,6 +9278,20 @@ packages: '@types/react': 18.0.26 dev: false + /@mui/utils/5.11.12_react@18.2.0: + resolution: {integrity: sha512-5vH9B/v8pzkpEPO2HvGM54ToXV6cFdAn8UrvdN8TMEEwpn/ycW0jLiyBcgUlPsQ+xha7hqXCPQYHaYFDIcwaiw==} + engines: {node: '>=12.0.0'} + peerDependencies: + react: ^17.0.0 || ^18.0.0 + dependencies: + '@babel/runtime': 7.21.0 + '@types/prop-types': 15.7.5 + '@types/react-is': 17.0.3 + prop-types: 15.8.1 + react: 18.2.0 + react-is: 18.2.0 + dev: false + /@mui/utils/5.11.2_react@18.2.0: resolution: {integrity: sha512-AyizuHHlGdAtH5hOOXBW3kriuIwUIKUIgg0P7LzMvzf6jPhoQbENYqY6zJqfoZ7fAWMNNYT8mgN5EftNGzwE2w==} engines: {node: '>=12.0.0'} @@ -14657,7 +14779,7 @@ packages: /@types/react-is/17.0.3: resolution: {integrity: sha512-aBTIWg1emtu95bLTLx0cpkxwGW3ueZv71nE2YFBpL8k/z5czEW8yYpOo8Dp+UUAFAtKwNaOsh/ioSeQnWlZcfw==} dependencies: - '@types/react': 17.0.53 + '@types/react': 18.0.26 dev: false /@types/react-mentions/4.1.8: @@ -14696,7 +14818,7 @@ packages: /@types/react-transition-group/4.4.5: resolution: {integrity: sha512-juKD/eiSM3/xZYzjuzH6ZwpP+/lejltmiS3QEzV/vmb/Q8+HfDmxu+Baga8UEMGBqV88Nbg4l2hY/K2DkyaLLA==} dependencies: - '@types/react': 18.0.20 + '@types/react': 18.0.26 /@types/react-virtualized-auto-sizer/1.0.1: resolution: {integrity: sha512-GH8sAnBEM5GV9LTeiz56r4ZhMOUSrP43tAQNSRVxNexDjcNKLCEtnxusAItg1owFUFE6k0NslV26gqVClVvong==} diff --git a/server/admin/app/ra/components/Image.tsx b/server/admin/app/ra/components/Image.tsx index e75cec46..59ad6642 100644 --- a/server/admin/app/ra/components/Image.tsx +++ b/server/admin/app/ra/components/Image.tsx @@ -1,7 +1,9 @@ -import React from 'react'; +import React, { ImgHTMLAttributes } from 'react'; import { parseUrlStr } from '../utils'; -export const Image: React.FC<{ src: string }> = React.memo((props) => { - return ; -}); +export const Image: React.FC> = React.memo( + (props) => { + return ; + } +); Image.displayName = 'Image'; diff --git a/server/admin/app/ra/i18n/custom.ts b/server/admin/app/ra/i18n/custom.ts index bf8bedb2..5dba6d5a 100644 --- a/server/admin/app/ra/i18n/custom.ts +++ b/server/admin/app/ra/i18n/custom.ts @@ -10,6 +10,7 @@ export const englishCustom = { errorOccurred: 'some errors occurred', operateSuccess: 'Operate Success', operateFailed: 'Operate Failed', + upload: 'Upload', }, menu: { network: 'Tailchat Network', @@ -87,6 +88,7 @@ export const chineseCustom = { errorOccurred: '发生了一些错误', operateSuccess: '操作成功', operateFailed: '操作失败', + upload: '上传', }, menu: { network: 'Tailchat 网络', diff --git a/server/admin/app/ra/routes/system/index.tsx b/server/admin/app/ra/routes/system/index.tsx index a949e075..dfb6c25c 100644 --- a/server/admin/app/ra/routes/system/index.tsx +++ b/server/admin/app/ra/routes/system/index.tsx @@ -2,10 +2,12 @@ import React, { PropsWithChildren } from 'react'; import { request } from '../../request'; import { useRequest } from 'ahooks'; import { CircularProgress, Box, Grid, Input } from '@mui/material'; -import { useTranslate, useDelete, useNotify } from 'react-admin'; +import { useTranslate, useNotify } from 'react-admin'; import DoneIcon from '@mui/icons-material/Done'; import ClearIcon from '@mui/icons-material/Clear'; import { useEditValue } from '../../utils/hooks'; +import { Image } from '../../components/Image'; +import LoadingButton from '@mui/lab/LoadingButton'; const SystemItem: React.FC< PropsWithChildren<{ @@ -13,9 +15,9 @@ const SystemItem: React.FC< }> > = React.memo((props) => { return ( - + - {props.label} + {props.label}: {props.children} @@ -35,6 +37,7 @@ export const SystemConfig: React.FC = React.memo(() => { data: config, loading, error, + refresh, } = useRequest(async () => { const { data } = await request.get('/config/client'); @@ -53,6 +56,7 @@ export const SystemConfig: React.FC = React.memo(() => { key: 'serverName', value: val, }); + refresh(); notify('custom.common.operateSuccess', { type: 'info', }); @@ -64,6 +68,49 @@ export const SystemConfig: React.FC = React.memo(() => { } ); + const { + loading: loadingServerEntryImage, + run: handleUploadServerEntryImage, + } = useRequest( + async (file: File) => { + try { + const formdata = new FormData(); + formdata.append('file', file); + + const { data } = await request.put('/file/upload', formdata, { + headers: { + 'Content-Type': 'multipart/form-data', + }, + }); + + const fileInfo = data.files[0]; + + if (!fileInfo) { + throw new Error('not get file'); + } + + const url = fileInfo.url; + await request.patch('/config/client', { + key: 'serverEntryImage', + value: url, + }); + refresh(); + + notify('custom.common.operateSuccess', { + type: 'info', + }); + } catch (err) { + console.log(err); + notify('custom.common.operateFailed', { + type: 'info', + }); + } + }, + { + manual: true, + } + ); + if (loading) { return ; } @@ -100,6 +147,38 @@ export const SystemConfig: React.FC = React.memo(() => { placeholder="Tailchat" /> + + +
+ + {translate('custom.common.upload')} + { + const file = e.target.files[0]; + if (file) { + handleUploadServerEntryImage(file); + } + }} + /> + + +
+ {config?.serverEntryImage && ( + + )} +
+
+
); }); diff --git a/server/admin/app/ra/utils/index.ts b/server/admin/app/ra/utils/index.ts index 12bae4e7..bfb26b37 100644 --- a/server/admin/app/ra/utils/index.ts +++ b/server/admin/app/ra/utils/index.ts @@ -4,5 +4,10 @@ * @returns 解析后的url */ export function parseUrlStr(originUrl: string): string { - return String(originUrl).replace('{BACKEND}', window.location.origin); + return String(originUrl).replace( + '{BACKEND}', + process.env.NODE_ENV === 'development' + ? 'http://localhost:11000' + : window.location.origin + ); } diff --git a/server/admin/app/server/router/api.ts b/server/admin/app/server/router/api.ts index 6131d4a4..685a1281 100644 --- a/server/admin/app/server/router/api.ts +++ b/server/admin/app/server/router/api.ts @@ -5,7 +5,7 @@ import { callBrokerAction } from '../broker'; import { adminAuth, auth, authSecret } from '../middleware/auth'; import { configRouter } from './config'; import { networkRouter } from './network'; -import { fileRouter } from './upload'; +import { fileRouter } from './file'; const router = Router(); diff --git a/server/admin/app/server/router/upload.ts b/server/admin/app/server/router/file.ts similarity index 100% rename from server/admin/app/server/router/upload.ts rename to server/admin/app/server/router/file.ts diff --git a/server/admin/package.json b/server/admin/package.json index efeefdff..a6657a3f 100644 --- a/server/admin/package.json +++ b/server/admin/package.json @@ -13,6 +13,7 @@ "dependencies": { "@fastify/busboy": "^1.1.0", "@mui/icons-material": "^5.11.0", + "@mui/lab": "5.0.0-alpha.122", "@mui/material": "^5.11.3", "@remix-run/express": "^1.9.0", "@remix-run/node": "^1.9.0",