diff --git a/server/admin/app/ra/components/Image.tsx b/server/admin/app/ra/components/Image.tsx new file mode 100644 index 00000000..e75cec46 --- /dev/null +++ b/server/admin/app/ra/components/Image.tsx @@ -0,0 +1,7 @@ +import React from 'react'; +import { parseUrlStr } from '../utils'; + +export const Image: React.FC<{ src: string }> = 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 1bc4a6b4..bf8bedb2 100644 --- a/server/admin/app/ra/i18n/custom.ts +++ b/server/admin/app/ra/i18n/custom.ts @@ -7,6 +7,9 @@ export const englishCustom = { permission: 'Permission', confirmTitle: 'Are you sure you want to perform this operation?', confirmContent: 'This action cannot be undone', + errorOccurred: 'some errors occurred', + operateSuccess: 'Operate Success', + operateFailed: 'Operate Failed', }, menu: { network: 'Tailchat Network', @@ -66,6 +69,8 @@ export const englishCustom = { config: { uploadFileLimit: 'Upload file limit (KB)', emailVerification: 'Mandatory Email Verification', + serverName: 'Server Name', + serverEntryImage: 'Server Entry Page Image', }, }, }; @@ -79,6 +84,9 @@ export const chineseCustom = { permission: '权限', confirmTitle: '确认要进行该操作么?', confirmContent: '该操作不可撤回', + errorOccurred: '发生了一些错误', + operateSuccess: '操作成功', + operateFailed: '操作失败', }, menu: { network: 'Tailchat 网络', @@ -136,6 +144,8 @@ export const chineseCustom = { config: { uploadFileLimit: '上传文件限制(KB)', emailVerification: '邮箱强制验证', + serverName: '服务器名', + serverEntryImage: '服务器登录图', }, }, }; diff --git a/server/admin/app/ra/routes/system/index.tsx b/server/admin/app/ra/routes/system/index.tsx index d35da90f..a949e075 100644 --- a/server/admin/app/ra/routes/system/index.tsx +++ b/server/admin/app/ra/routes/system/index.tsx @@ -1,10 +1,11 @@ import React, { PropsWithChildren } from 'react'; import { request } from '../../request'; import { useRequest } from 'ahooks'; -import { CircularProgress, Box, Grid } from '@mui/material'; -import { useTranslate } from 'react-admin'; +import { CircularProgress, Box, Grid, Input } from '@mui/material'; +import { useTranslate, useDelete, useNotify } from 'react-admin'; import DoneIcon from '@mui/icons-material/Done'; import ClearIcon from '@mui/icons-material/Clear'; +import { useEditValue } from '../../utils/hooks'; const SystemItem: React.FC< PropsWithChildren<{ @@ -29,16 +30,48 @@ SystemItem.displayName = 'SystemItem'; */ export const SystemConfig: React.FC = React.memo(() => { const translate = useTranslate(); - const { data: config, loading } = useRequest(async () => { - const { data } = await request('/config/client'); + const notify = useNotify(); + const { + data: config, + loading, + error, + } = useRequest(async () => { + const { data } = await request.get('/config/client'); return data.config ?? {}; }); + const [serverName, setServerName, saveServerName] = useEditValue( + config?.serverName, + async (val) => { + if (val === config?.serverName) { + return; + } + + try { + await request.patch('/config/client', { + key: 'serverName', + value: val, + }); + notify('custom.common.operateSuccess', { + type: 'info', + }); + } catch (err) { + notify('custom.common.operateFailed', { + type: 'info', + }); + } + } + ); + if (loading) { return ; } + if (error) { + return
{translate('custom.common.errorOccurred')}
; + } + return ( { )} + + + setServerName(e.target.value)} + onBlur={() => saveServerName()} + placeholder="Tailchat" + /> + ); }); diff --git a/server/admin/app/ra/utils/hooks.ts b/server/admin/app/ra/utils/hooks.ts new file mode 100644 index 00000000..6363ba3b --- /dev/null +++ b/server/admin/app/ra/utils/hooks.ts @@ -0,0 +1,15 @@ +import { useCallback, useLayoutEffect, useState } from 'react'; + +export function useEditValue(value: T, onChange: (val: T) => void) { + const [inner, setInner] = useState(value); + + useLayoutEffect(() => { + setInner(value); + }, [value]); + + const onSave = useCallback(() => { + onChange(inner); + }, [inner, onChange]); + + return [inner, setInner, onSave] as const; +} diff --git a/server/admin/app/ra/utils/index.ts b/server/admin/app/ra/utils/index.ts new file mode 100644 index 00000000..12bae4e7 --- /dev/null +++ b/server/admin/app/ra/utils/index.ts @@ -0,0 +1,8 @@ +/** + * parse url, and replace some constants with variable + * @param originUrl 原始Url + * @returns 解析后的url + */ +export function parseUrlStr(originUrl: string): string { + return String(originUrl).replace('{BACKEND}', window.location.origin); +} diff --git a/server/admin/app/routes/admin/$.tsx b/server/admin/app/routes/admin/$.tsx index a72cadb5..6a7549ae 100644 --- a/server/admin/app/routes/admin/$.tsx +++ b/server/admin/app/routes/admin/$.tsx @@ -1,5 +1,5 @@ -import { App } from '~/ra/App'; -import styles from '~/styles/app.css'; +import { App } from '../../ra/App'; +import styles from '../../styles/app.css'; export function links() { return [{ rel: 'stylesheet', href: styles }]; diff --git a/server/admin/app/routes/admin/index.tsx b/server/admin/app/routes/admin/index.tsx index a72cadb5..6a7549ae 100644 --- a/server/admin/app/routes/admin/index.tsx +++ b/server/admin/app/routes/admin/index.tsx @@ -1,5 +1,5 @@ -import { App } from '~/ra/App'; -import styles from '~/styles/app.css'; +import { App } from '../../ra/App'; +import styles from '../../styles/app.css'; export function links() { return [{ rel: 'stylesheet', href: styles }]; diff --git a/server/admin/app/server/index.ts b/server/admin/app/server/index.ts index 15e2744e..ee9dc17c 100644 --- a/server/admin/app/server/index.ts +++ b/server/admin/app/server/index.ts @@ -7,8 +7,13 @@ import mongoose from 'mongoose'; import bodyParser from 'body-parser'; import { apiRouter } from './router/api'; +if (!process.env.MONGO_URL) { + console.error('Require env: MONGO_URL'); + process.exit(1); +} + // 链接数据库 -mongoose.connect(process.env.MONGO_URL!, (error: any) => { +mongoose.connect(process.env.MONGO_URL, (error: any) => { if (!error) { return console.info('Datebase connected'); } @@ -56,6 +61,11 @@ app.all( }) ); +app.use((err, req, res, next) => { + res.status(500); + res.json({ error: err.message }); +}); + const port = process.env.ADMIN_PORT || 3000; app.listen(port, () => { diff --git a/server/admin/app/server/router/config.ts b/server/admin/app/server/router/config.ts index 992ffdac..134bc0a7 100644 --- a/server/admin/app/server/router/config.ts +++ b/server/admin/app/server/router/config.ts @@ -8,12 +8,31 @@ import { auth } from '../middleware/auth'; const router = Router(); -router.get('/client', auth(), async (req, res) => { - const config = await broker.call('config.client'); +router.get('/client', auth(), async (req, res, next) => { + try { + const config = await broker.call('config.client'); - res.json({ - config, - }); + res.json({ + config, + }); + } catch (err) { + next(err); + } +}); + +router.patch('/client', auth(), async (req, res, next) => { + try { + await broker.call('config.setClientConfig', { + key: req.body.key, + value: req.body.value, + }); + + res.json({ + success: true, + }); + } catch (err) { + next(err); + } }); export { router as configRouter }; diff --git a/server/admin/tsconfig.json b/server/admin/tsconfig.json index 969727bd..f22bfbed 100644 --- a/server/admin/tsconfig.json +++ b/server/admin/tsconfig.json @@ -16,13 +16,8 @@ "forceConsistentCasingInFileNames": true, "importsNotUsedAsValues": "error", "experimentalDecorators": true, - "baseUrl": ".", - "paths": { - "~/*": ["./app/*"] - }, "jsx": "react-jsx", - - // Remix takes care of building everything in `remix build`. - "noEmit": true + "noEmit": true, + "baseUrl": "." } } diff --git a/server/models/config.ts b/server/models/config.ts index f9ddd766..e4558bff 100644 --- a/server/models/config.ts +++ b/server/models/config.ts @@ -5,6 +5,7 @@ import { modelOptions, Severity, ReturnModelType, + index, } from '@typegoose/typegoose'; import type { Base } from '@typegoose/typegoose/lib/defaultClasses'; import type { Types } from 'mongoose'; @@ -14,6 +15,7 @@ import type { Types } from 'mongoose'; allowMixed: Severity.ALLOW, }, }) +@index({ name: 1 }) export class Config implements Base { _id: Types.ObjectId; id: string; @@ -47,14 +49,17 @@ export class Config implements Base { key: string, value: any ): Promise { - await this.updateOne( + await this.findOneAndUpdate( { name: Config.globalClientConfigName, }, { $set: { - [key]: value, + [`data.${key}`]: value, }, + }, + { + upsert: true, } ); } diff --git a/server/packages/sdk/src/services/base.ts b/server/packages/sdk/src/services/base.ts index bb48f62b..67fca5af 100644 --- a/server/packages/sdk/src/services/base.ts +++ b/server/packages/sdk/src/services/base.ts @@ -407,9 +407,13 @@ export abstract class TcService extends Service { * NOTICE: 这里使用Redis作为缓存管理器,因此不需要通知所有的service */ async cleanActionCache(actionName: string, keys: string[] = []) { - await this.broker.cacher.clean( - `${this.serviceName}.${actionName}:${keys.join('|')}**` - ); + if (keys.length === 0) { + await this.broker.cacher.clean(`${this.serviceName}.${actionName}`); + } else { + await this.broker.cacher.clean( + `${this.serviceName}.${actionName}:${keys.join('|')}**` + ); + } } /** diff --git a/server/services/core/config.service.ts b/server/services/core/config.service.ts index 2dc952a7..3cb2d865 100644 --- a/server/services/core/config.service.ts +++ b/server/services/core/config.service.ts @@ -71,7 +71,7 @@ class ConfigService extends TcService { * NOTICE: 返回内容比较简单,因此不建议增加缓存 */ async client(ctx: TcPureContext) { - const persistConfig = this.adapter.model.getAllClientPersistConfig(); + const persistConfig = await this.adapter.model.getAllClientPersistConfig(); return { uploadFileLimit: config.storage.limit,