diff --git a/= b/= deleted file mode 100644 index e69de29..0000000 diff --git a/apps/admin-web/src/App.tsx b/apps/admin-web/src/App.tsx index e345e66..9b78d8b 100644 --- a/apps/admin-web/src/App.tsx +++ b/apps/admin-web/src/App.tsx @@ -36,6 +36,7 @@ import StoreLogsPage from './pages/StoreLogsPage'; import PartnerLogsPage from './pages/PartnerLogsPage'; import WechatBindingsPage from './pages/WechatBindingsPage'; import HqPermissionsPage from './pages/HqPermissionsPage'; +import SystemSettingsPage from './pages/SystemSettingsPage'; function RequireAuth({ children }: { children: React.ReactNode }) { if (!getToken()) return ; @@ -90,6 +91,7 @@ export default function App() { } /> } /> } /> + } /> } /> } /> diff --git a/apps/admin-web/src/components/OssUpload.tsx b/apps/admin-web/src/components/OssUpload.tsx index 7196b8e..363d1f3 100644 --- a/apps/admin-web/src/components/OssUpload.tsx +++ b/apps/admin-web/src/components/OssUpload.tsx @@ -45,11 +45,7 @@ export default function OssUpload({ const result = await uploadFileToOss(raw, { bizType, mediaType }); onChange?.(result.url); onUploaded?.(result); - if (result.mock) { - message.info('当前为 Mock OSS,已使用占位 URL'); - } else { - message.success('上传成功'); - } + message.success('上传成功'); onSuccess?.(result); } catch (e) { const err = e instanceof Error ? e : new Error('上传失败'); diff --git a/apps/admin-web/src/layouts/AdminLayout.tsx b/apps/admin-web/src/layouts/AdminLayout.tsx index b841023..2c72049 100644 --- a/apps/admin-web/src/layouts/AdminLayout.tsx +++ b/apps/admin-web/src/layouts/AdminLayout.tsx @@ -15,6 +15,7 @@ import { CloudUploadOutlined, FileTextOutlined, LockOutlined, + SettingOutlined, } from '@ant-design/icons'; import { clearAuth, request, type HqProfile } from '../lib/api'; import { bindAdminEllipsisTitle } from '../lib/ellipsis-title'; @@ -94,6 +95,7 @@ const MENU_ITEMS: MenuProps['items'] = [ ], }, { key: '/hq-permissions', icon: , label: '权限分配' }, + { key: '/system-settings', icon: , label: '系统设置' }, { key: '/hq-accounts', icon: , label: 'HQ账户' }, ]; diff --git a/apps/admin-web/src/pages/SystemSettingsPage.tsx b/apps/admin-web/src/pages/SystemSettingsPage.tsx new file mode 100644 index 0000000..d5ca185 --- /dev/null +++ b/apps/admin-web/src/pages/SystemSettingsPage.tsx @@ -0,0 +1,573 @@ +import { useEffect, useMemo, useState, type ReactNode } from 'react'; + +import { + + Alert, + + Button, + + Card, + + Collapse, + + Form, + + Input, + + InputNumber, + + Space, + + Switch, + + Table, + + Tag, + + Typography, + + message, + +} from 'antd'; + +import type { MockSmsCodeItem, SystemConfigFieldMeta, SystemConfigFormResponse } from '@dukang/shared-types'; + +import { request } from '../lib/api'; + + + +const { TextArea } = Input; + + + +function MockSmsCodePanel({ codes, loading }: { codes: MockSmsCodeItem[]; loading?: boolean }) { + + return ( + +
+ + + + 最近 Mock 验证码(写入数据库,最新 50 条) + + + + + + size="small" + + rowKey="id" + + loading={loading} + + pagination={false} + + scroll={{ y: 240 }} + + locale={{ emptyText: '暂无记录,触发短信发送后将显示在此' }} + + columns={[ + + { + + title: '时间', + + dataIndex: 'createdAt', + + width: 168, + + render: (v: string) => new Date(v).toLocaleString(), + + }, + + { title: '手机号', dataIndex: 'phone', width: 120 }, + + { title: '场景', dataIndex: 'scene', width: 160 }, + + { + + title: '验证码', + + dataIndex: 'code', + + width: 88, + + render: (code: string) => ( + + + + {code} + + + + ), + + }, + + ]} + + dataSource={codes} + + /> + +
+ + ); + +} + + + +function renderField( + + field: SystemConfigFieldMeta, + + configuredSecrets: string[], + + extra?: ReactNode, + +) { + + const isConfiguredSecret = field.secret && configuredSecrets.includes(field.key); + + if (field.type === 'boolean') { + + return ( + +
+ + + + {field.label} + + + + {field.key} + + + + {field.requiresRestart ? 需重启 : 即时} + + + + } + + tooltip={field.description} + + valuePropName="checked" + + getValueFromEvent={(checked: boolean) => (checked ? 'true' : 'false')} + + getValueProps={(v: string) => ({ checked: v === 'true' || v === '1' })} + + > + + + + + + {extra} + +
+ + ); + + } + + + + const input = + + field.type === 'textarea' ? ( + +