feat: allow admin upload and edit serverEntryImage

priority: plugin image -> admin config -> fallback(cat)
pull/90/head
moonrailgun 2 years ago
parent d9adf84014
commit fb13a3c928

@ -20,6 +20,11 @@ export interface GlobalConfig {
*
*/
serverName?: string;
/**
*
*/
serverEntryImage?: string;
}
export function getGlobalConfig(): GlobalConfig {

@ -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 (
<div className="h-full flex flex-row">
<div
@ -33,6 +39,18 @@ const EntryRoute = React.memo(() => {
</Routes>
</div>
{serverEntryImage && (
<Helmet>
<style type="text/css">
{`
#tailchat-app {
--tc-background-image: url(${parseUrlStr(serverEntryImage)});
}
`}
</style>
</Helmet>
)}
<div className="flex-1 mobile:hidden tc-background" />
</div>
);

@ -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==}

@ -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 <img src={parseUrlStr(props.src)} />;
});
export const Image: React.FC<ImgHTMLAttributes<HTMLImageElement>> = React.memo(
(props) => {
return <img {...props} src={parseUrlStr(props.src)} />;
}
);
Image.displayName = 'Image';

@ -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 网络',

@ -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 (
<Grid container spacing={2}>
<Grid container spacing={2} marginBottom={2}>
<Grid item xs={4}>
{props.label}
{props.label}:
</Grid>
<Grid item xs={8}>
{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 <CircularProgress />;
}
@ -100,6 +147,38 @@ export const SystemConfig: React.FC = React.memo(() => {
placeholder="Tailchat"
/>
</SystemItem>
<SystemItem label={translate('custom.config.serverEntryImage')}>
<div>
<LoadingButton
loading={loadingServerEntryImage}
variant="contained"
component="label"
>
{translate('custom.common.upload')}
<input
hidden
accept="image/*"
type="file"
onChange={(e) => {
const file = e.target.files[0];
if (file) {
handleUploadServerEntryImage(file);
}
}}
/>
</LoadingButton>
<div style={{ marginTop: 10 }}>
{config?.serverEntryImage && (
<Image
style={{ maxWidth: '100%', maxHeight: 360 }}
src={config?.serverEntryImage}
/>
)}
</div>
</div>
</SystemItem>
</Box>
);
});

@ -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
);
}

@ -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();

@ -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",

Loading…
Cancel
Save