pages_settings.tsx 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298
  1. import React, { useEffect } from 'react';
  2. import {
  3. Button,Space,
  4. Form, Input, Select, message, Modal,
  5. Card, Spin, Typography,
  6. Switch, Tabs, Alert, InputNumber
  7. } from 'antd';
  8. import {
  9. ReloadOutlined,
  10. SaveOutlined,
  11. } from '@ant-design/icons';
  12. import {
  13. useQuery,
  14. useMutation,
  15. useQueryClient,
  16. } from '@tanstack/react-query';
  17. import dayjs from 'dayjs';
  18. import weekday from 'dayjs/plugin/weekday';
  19. import localeData from 'dayjs/plugin/localeData';
  20. import 'dayjs/locale/zh-cn';
  21. import type {
  22. SystemSetting, SystemSettingValue
  23. } from '../share/types.ts';
  24. import {
  25. SystemSettingGroup,
  26. SystemSettingKey,
  27. AllowedFileType
  28. } from '../share/types.ts';
  29. import {
  30. SystemAPI
  31. } from './api/index.ts';
  32. import { useTheme } from './hooks_sys.tsx';
  33. import { validateUrl, validateAuthHeader } from './utils.ts';
  34. import { Uploader } from './components_uploader.tsx';
  35. // 配置 dayjs 插件
  36. dayjs.extend(weekday);
  37. dayjs.extend(localeData);
  38. // 设置 dayjs 语言
  39. dayjs.locale('zh-cn');
  40. const { Title } = Typography;
  41. // 分组标题映射
  42. const GROUP_TITLES: Record<typeof SystemSettingGroup[keyof typeof SystemSettingGroup], string> = {
  43. [SystemSettingGroup.BASIC]: '基础设置',
  44. [SystemSettingGroup.FEATURE]: '功能设置',
  45. [SystemSettingGroup.UPLOAD]: '上传设置',
  46. [SystemSettingGroup.NOTIFICATION]: '通知设置'
  47. };
  48. // 分组描述映射
  49. const GROUP_DESCRIPTIONS: Record<typeof SystemSettingGroup[keyof typeof SystemSettingGroup], string> = {
  50. [SystemSettingGroup.BASIC]: '配置站点的基本信息',
  51. [SystemSettingGroup.FEATURE]: '配置系统功能的开启状态',
  52. [SystemSettingGroup.UPLOAD]: '配置文件上传相关的参数',
  53. [SystemSettingGroup.NOTIFICATION]: '配置系统通知的触发条件'
  54. };
  55. export const SettingsPage = () => {
  56. const [form] = Form.useForm();
  57. const queryClient = useQueryClient();
  58. const { isDark } = useTheme();
  59. // 获取系统设置
  60. const { data: settingsData, isLoading: isLoadingSettings } = useQuery({
  61. queryKey: ['systemSettings'],
  62. queryFn: SystemAPI.getSettings,
  63. });
  64. // 更新系统设置
  65. const updateSettingsMutation = useMutation({
  66. mutationFn: (values: Partial<SystemSetting>[]) => SystemAPI.updateSettings(values),
  67. onSuccess: () => {
  68. message.success('基础设置已更新');
  69. queryClient.invalidateQueries({ queryKey: ['systemSettings'] });
  70. },
  71. onError: (error) => {
  72. message.error('更新基础设置失败');
  73. console.error('更新基础设置失败:', error);
  74. },
  75. });
  76. // 重置系统设置
  77. const resetSettingsMutation = useMutation({
  78. mutationFn: SystemAPI.resetSettings,
  79. onSuccess: () => {
  80. message.success('基础设置已重置');
  81. queryClient.invalidateQueries({ queryKey: ['systemSettings'] });
  82. },
  83. onError: (error) => {
  84. message.error('重置基础设置失败');
  85. console.error('重置基础设置失败:', error);
  86. },
  87. });
  88. // 初始化表单数据
  89. useEffect(() => {
  90. if (settingsData) {
  91. const formValues = settingsData.reduce((acc: Record<string, any>, group) => {
  92. group.settings.forEach(setting => {
  93. // 根据值的类型进行转换
  94. let value = setting.value;
  95. if (typeof value === 'string') {
  96. if (value === 'true' || value === 'false') {
  97. value = value === 'true';
  98. } else if (!isNaN(Number(value)) && !value.includes('.')) {
  99. value = parseInt(value, 10);
  100. } else if (setting.key === SystemSettingKey.ALLOWED_FILE_TYPES) {
  101. value = (value ? (value as string).split(',') : []) as unknown as string;
  102. }
  103. }
  104. acc[setting.key] = value;
  105. });
  106. return acc;
  107. }, {});
  108. form.setFieldsValue(formValues);
  109. }
  110. }, [settingsData, form]);
  111. // 处理表单提交
  112. const handleSubmit = async (values: Record<string, SystemSettingValue>) => {
  113. const settings = Object.entries(values).map(([key, value]) => ({
  114. key: key as typeof SystemSettingKey[keyof typeof SystemSettingKey],
  115. value: String(value),
  116. group: key.startsWith('SITE_') ? SystemSettingGroup.BASIC :
  117. key.startsWith('ENABLE_') || key.includes('LOGIN_') || key.includes('SESSION_') ? SystemSettingGroup.FEATURE :
  118. key.includes('UPLOAD_') || key.includes('FILE_') || key.includes('IMAGE_') ? SystemSettingGroup.UPLOAD :
  119. SystemSettingGroup.NOTIFICATION
  120. }));
  121. updateSettingsMutation.mutate(settings);
  122. };
  123. // 处理重置
  124. const handleReset = () => {
  125. Modal.confirm({
  126. title: '确认重置',
  127. content: '确定要将所有设置重置为默认值吗?此操作不可恢复。',
  128. okText: '确认',
  129. cancelText: '取消',
  130. onOk: () => {
  131. resetSettingsMutation.mutate();
  132. },
  133. });
  134. };
  135. // 根据设置类型渲染不同的输入控件
  136. const renderSettingInput = (setting: SystemSetting) => {
  137. const value = setting.value;
  138. if (typeof value === 'boolean' || value === 'true' || value === 'false') {
  139. return <Switch checkedChildren="开启" unCheckedChildren="关闭" />;
  140. }
  141. if (setting.key === SystemSettingKey.ALLOWED_FILE_TYPES) {
  142. return <Select
  143. mode="tags"
  144. placeholder="请输入允许的文件类型"
  145. tokenSeparators={[',']}
  146. options={Object.values(AllowedFileType).map(type => ({
  147. label: type.toUpperCase(),
  148. value: type
  149. }))}
  150. />;
  151. }
  152. if (setting.key.includes('MAX_SIZE') || setting.key.includes('ATTEMPTS') ||
  153. setting.key.includes('TIMEOUT') || setting.key.includes('MAX_WIDTH') ||
  154. setting.key === 'SMS_API_TIMEOUT' || setting.key === 'SMS_API_RETRY') {
  155. return <InputNumber min={1} style={{ width: '100%' }} />;
  156. }
  157. if (setting.key === SystemSettingKey.SITE_LOGO || setting.key === SystemSettingKey.SITE_FAVICON) {
  158. return (
  159. <div>
  160. {value && <img src={String(value)} alt="图片" style={{ width: 100, height: 100, objectFit: 'contain', marginBottom: 8 }} />}
  161. <div style={{ width: 100 }}>
  162. <Uploader
  163. maxSize={2 * 1024 * 1024}
  164. prefix={setting.key === SystemSettingKey.SITE_LOGO ? 'logo/' : 'favicon/'}
  165. allowedTypes={['image/jpeg', 'image/png', 'image/svg+xml', 'image/x-icon']}
  166. onSuccess={(fileUrl) => {
  167. form.setFieldValue(setting.key, fileUrl);
  168. updateSettingsMutation.mutate([{
  169. key: setting.key,
  170. value: fileUrl,
  171. group: SystemSettingGroup.BASIC
  172. }]);
  173. }}
  174. onError={(error) => {
  175. message.error(`上传失败:${error.message}`);
  176. }}
  177. />
  178. </div>
  179. </div>
  180. );
  181. }
  182. if (setting.key === 'SMS_API_URL') {
  183. return <Input placeholder="请输入短信接口URL" />;
  184. }
  185. if (setting.key === 'SMS_API_AUTH') {
  186. return <Input.Password placeholder="请输入Basic Auth认证信息" />;
  187. }
  188. return <Input placeholder={`请输入${setting.description || setting.key}`} />;
  189. };
  190. return (
  191. <div>
  192. <Card
  193. title={
  194. <Space>
  195. <Title level={2} style={{ margin: 0 }}>系统设置</Title>
  196. </Space>
  197. }
  198. extra={
  199. <Space>
  200. <Button
  201. icon={<ReloadOutlined />}
  202. onClick={handleReset}
  203. loading={resetSettingsMutation.isPending}
  204. >
  205. 重置默认
  206. </Button>
  207. </Space>
  208. }
  209. >
  210. <Spin spinning={isLoadingSettings || updateSettingsMutation.isPending}>
  211. <Tabs
  212. type="card"
  213. items={Object.values(SystemSettingGroup).map(group => ({
  214. key: group,
  215. label: String(GROUP_TITLES[group]),
  216. children: (
  217. <div>
  218. <Alert
  219. message={GROUP_DESCRIPTIONS[group]}
  220. type="info"
  221. showIcon
  222. style={{ marginBottom: 24 }}
  223. />
  224. <Form
  225. form={form}
  226. layout="vertical"
  227. onFinish={handleSubmit}
  228. >
  229. {settingsData
  230. ?.find(g => g.name === group)
  231. ?.settings.map(setting => (
  232. <Form.Item
  233. key={setting.key}
  234. label={setting.description || setting.key}
  235. name={setting.key}
  236. rules={[
  237. { required: true, message: `请输入${setting.description || setting.key}` },
  238. ...(setting.key === 'SMS_API_URL' ? [{
  239. validator: (_: unknown, value: string) =>
  240. validateUrl(value) ? Promise.resolve() : Promise.reject(new Error('请输入有效的URL'))
  241. }] : []),
  242. ...(setting.key === 'SMS_API_AUTH' ? [{
  243. validator: (_: unknown, value: string) =>
  244. validateAuthHeader(value) ? Promise.resolve() : Promise.reject(new Error('格式应为Basic base64字符串'))
  245. }] : [])
  246. ]}
  247. >
  248. {renderSettingInput(setting)}
  249. </Form.Item>
  250. ))}
  251. <Form.Item>
  252. <Button
  253. type="primary"
  254. htmlType="submit"
  255. icon={<SaveOutlined />}
  256. loading={updateSettingsMutation.isPending}
  257. >
  258. 保存设置
  259. </Button>
  260. </Form.Item>
  261. </Form>
  262. </div>
  263. )
  264. }))}
  265. />
  266. </Spin>
  267. </Card>
  268. </div>
  269. );
  270. };