pages_settings.tsx 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629
  1. import React, { useState, useEffect } from 'react';
  2. import {
  3. Layout, Menu, Button, Table, Space,
  4. Form, Input, Select, message, Modal,
  5. Card, Spin, Row, Col, Breadcrumb, Avatar,
  6. Dropdown, ConfigProvider, theme, Typography,
  7. Switch, Badge, Image, Upload, Divider, Descriptions,
  8. Popconfirm, Tag, Statistic, DatePicker, Radio, Progress, Tabs, List, Alert, Collapse, Empty, Drawer, InputNumber,ColorPicker,
  9. Popover
  10. } from 'antd';
  11. import {
  12. UploadOutlined,
  13. ReloadOutlined,
  14. SaveOutlined,
  15. BgColorsOutlined
  16. } from '@ant-design/icons';
  17. import { debounce } from 'lodash';
  18. import {
  19. useQuery,
  20. useMutation,
  21. useQueryClient,
  22. } from '@tanstack/react-query';
  23. import dayjs from 'dayjs';
  24. import weekday from 'dayjs/plugin/weekday';
  25. import localeData from 'dayjs/plugin/localeData';
  26. import 'dayjs/locale/zh-cn';
  27. import type {
  28. FileLibrary, FileCategory, KnowInfo, SystemSetting, SystemSettingValue
  29. } from '../share/types.ts';
  30. import {
  31. SystemSettingGroup,
  32. SystemSettingKey,
  33. ThemeMode,
  34. FontSize,
  35. CompactMode,
  36. AllowedFileType
  37. } from '../share/types.ts';
  38. import { getEnumOptions } from './utils.ts';
  39. import {
  40. SystemAPI,
  41. } from './api.ts';
  42. import { useTheme } from './hooks_sys.tsx';
  43. import { Uploader } from './components_uploader.tsx';
  44. // 配置 dayjs 插件
  45. dayjs.extend(weekday);
  46. dayjs.extend(localeData);
  47. // 设置 dayjs 语言
  48. dayjs.locale('zh-cn');
  49. const { Title } = Typography;
  50. // 分组标题映射
  51. const GROUP_TITLES: Record<typeof SystemSettingGroup[keyof typeof SystemSettingGroup], string> = {
  52. [SystemSettingGroup.BASIC]: '基础设置',
  53. [SystemSettingGroup.FEATURE]: '功能设置',
  54. [SystemSettingGroup.UPLOAD]: '上传设置',
  55. [SystemSettingGroup.NOTIFICATION]: '通知设置'
  56. };
  57. // 分组描述映射
  58. const GROUP_DESCRIPTIONS: Record<typeof SystemSettingGroup[keyof typeof SystemSettingGroup], string> = {
  59. [SystemSettingGroup.BASIC]: '配置站点的基本信息',
  60. [SystemSettingGroup.FEATURE]: '配置系统功能的开启状态',
  61. [SystemSettingGroup.UPLOAD]: '配置文件上传相关的参数',
  62. [SystemSettingGroup.NOTIFICATION]: '配置系统通知的触发条件'
  63. };
  64. // 定义预设配色方案
  65. const COLOR_SCHEMES = {
  66. DEFAULT: {
  67. name: '默认',
  68. primary: '#1890ff',
  69. background: '#f0f2f5',
  70. text: '#000000'
  71. },
  72. DARK: {
  73. name: '深色',
  74. primary: '#177ddc',
  75. background: '#141414',
  76. text: '#ffffff'
  77. },
  78. LIGHT: {
  79. name: '浅色',
  80. primary: '#40a9ff',
  81. background: '#ffffff',
  82. text: '#000000'
  83. },
  84. BLUE: {
  85. name: '蓝色',
  86. primary: '#096dd9',
  87. background: '#e6f7ff',
  88. text: '#003a8c'
  89. },
  90. GREEN: {
  91. name: '绿色',
  92. primary: '#52c41a',
  93. background: '#f6ffed',
  94. text: '#135200'
  95. },
  96. WARM: {
  97. name: '暖橙',
  98. primary: '#fa8c16',
  99. background: '#fff7e6',
  100. text: '#873800'
  101. },
  102. SUNSET: {
  103. name: '日落',
  104. primary: '#f5222d',
  105. background: '#fff1f0',
  106. text: '#820014'
  107. },
  108. GOLDEN: {
  109. name: '金色',
  110. primary: '#faad14',
  111. background: '#fffbe6',
  112. text: '#614700'
  113. },
  114. CORAL: {
  115. name: '珊瑚',
  116. primary: '#ff7a45',
  117. background: '#fff2e8',
  118. text: '#ad2102'
  119. },
  120. ROSE: {
  121. name: '玫瑰',
  122. primary: '#eb2f96',
  123. background: '#fff0f6',
  124. text: '#9e1068'
  125. }
  126. };
  127. // 颜色选择器组件
  128. // const ColorPicker: React.FC<{
  129. // value?: string;
  130. // onChange?: (color: string) => void;
  131. // label?: string;
  132. // }> = ({ value = '#1890ff', onChange, label = '选择颜色' }) => {
  133. // const [color, setColor] = useState(value);
  134. // const [open, setOpen] = useState(false);
  135. // // 更新颜色(预览)
  136. // const handleColorChange = (e: React.ChangeEvent<HTMLInputElement>) => {
  137. // const newColor = e.target.value;
  138. // setColor(newColor);
  139. // };
  140. // // 关闭时确认颜色
  141. // const handleOpenChange = (visible: boolean) => {
  142. // if (!visible && color) {
  143. // onChange?.(color);
  144. // }
  145. // setOpen(visible);
  146. // };
  147. // return (
  148. // <Popover
  149. // open={open}
  150. // onOpenChange={handleOpenChange}
  151. // trigger="click"
  152. // content={
  153. // <div style={{ padding: '8px' }}>
  154. // <Input
  155. // type="color"
  156. // value={color}
  157. // onChange={handleColorChange}
  158. // style={{ width: 60, cursor: 'pointer' }}
  159. // />
  160. // </div>
  161. // }
  162. // >
  163. // <Button
  164. // icon={<BgColorsOutlined />}
  165. // style={{
  166. // backgroundColor: color,
  167. // borderColor: color,
  168. // color: '#fff'
  169. // }}
  170. // >
  171. // {label}
  172. // </Button>
  173. // </Popover>
  174. // );
  175. // };
  176. // 基础设置页面
  177. export const SettingsPage = () => {
  178. const [form] = Form.useForm();
  179. const queryClient = useQueryClient();
  180. const { isDark } = useTheme();
  181. // 获取系统设置
  182. const { data: settingsData, isLoading: isLoadingSettings } = useQuery({
  183. queryKey: ['systemSettings'],
  184. queryFn: SystemAPI.getSettings,
  185. });
  186. // 更新系统设置
  187. const updateSettingsMutation = useMutation({
  188. mutationFn: (values: Partial<SystemSetting>[]) => SystemAPI.updateSettings(values),
  189. onSuccess: () => {
  190. message.success('基础设置已更新');
  191. queryClient.invalidateQueries({ queryKey: ['systemSettings'] });
  192. },
  193. onError: (error) => {
  194. message.error('更新基础设置失败');
  195. console.error('更新基础设置失败:', error);
  196. },
  197. });
  198. // 重置系统设置
  199. const resetSettingsMutation = useMutation({
  200. mutationFn: SystemAPI.resetSettings,
  201. onSuccess: () => {
  202. message.success('基础设置已重置');
  203. queryClient.invalidateQueries({ queryKey: ['systemSettings'] });
  204. },
  205. onError: (error) => {
  206. message.error('重置基础设置失败');
  207. console.error('重置基础设置失败:', error);
  208. },
  209. });
  210. // 初始化表单数据
  211. useEffect(() => {
  212. if (settingsData) {
  213. const formValues = settingsData.reduce((acc: Record<string, any>, group) => {
  214. group.settings.forEach(setting => {
  215. // 根据值的类型进行转换
  216. let value = setting.value;
  217. if (typeof value === 'string') {
  218. if (value === 'true' || value === 'false') {
  219. value = value === 'true';
  220. } else if (!isNaN(Number(value)) && !value.includes('.')) {
  221. value = parseInt(value, 10);
  222. } else if (setting.key === SystemSettingKey.ALLOWED_FILE_TYPES) {
  223. value = (value ? (value as string).split(',') : []) as unknown as string;
  224. }
  225. }
  226. acc[setting.key] = value;
  227. });
  228. return acc;
  229. }, {});
  230. form.setFieldsValue(formValues);
  231. }
  232. }, [settingsData, form]);
  233. // 处理表单提交
  234. const handleSubmit = async (values: Record<string, SystemSettingValue>) => {
  235. const settings = Object.entries(values).map(([key, value]) => ({
  236. key: key as typeof SystemSettingKey[keyof typeof SystemSettingKey],
  237. value: String(value),
  238. group: key.startsWith('SITE_') ? SystemSettingGroup.BASIC :
  239. key.startsWith('ENABLE_') || key.includes('LOGIN_') || key.includes('SESSION_') ? SystemSettingGroup.FEATURE :
  240. key.includes('UPLOAD_') || key.includes('FILE_') || key.includes('IMAGE_') ? SystemSettingGroup.UPLOAD :
  241. SystemSettingGroup.NOTIFICATION
  242. }));
  243. updateSettingsMutation.mutate(settings);
  244. };
  245. // 处理重置
  246. const handleReset = () => {
  247. Modal.confirm({
  248. title: '确认重置',
  249. content: '确定要将所有设置重置为默认值吗?此操作不可恢复。',
  250. okText: '确认',
  251. cancelText: '取消',
  252. onOk: () => {
  253. resetSettingsMutation.mutate();
  254. },
  255. });
  256. };
  257. // 根据设置类型渲染不同的输入控件
  258. const renderSettingInput = (setting: SystemSetting) => {
  259. const value = setting.value;
  260. if (typeof value === 'boolean' || value === 'true' || value === 'false') {
  261. return <Switch checkedChildren="开启" unCheckedChildren="关闭" />;
  262. }
  263. if (setting.key === SystemSettingKey.ALLOWED_FILE_TYPES) {
  264. return <Select
  265. mode="tags"
  266. placeholder="请输入允许的文件类型"
  267. tokenSeparators={[',']}
  268. options={Object.values(AllowedFileType).map(type => ({
  269. label: type.toUpperCase(),
  270. value: type
  271. }))}
  272. />;
  273. }
  274. if (setting.key.includes('MAX_SIZE') || setting.key.includes('ATTEMPTS') ||
  275. setting.key.includes('TIMEOUT') || setting.key.includes('MAX_WIDTH')) {
  276. return <InputNumber min={1} style={{ width: '100%' }} />;
  277. }
  278. if (setting.key === SystemSettingKey.SITE_LOGO || setting.key === SystemSettingKey.SITE_FAVICON) {
  279. return (
  280. <div>
  281. {value && <img src={String(value)} alt="图片" style={{ width: 100, height: 100, objectFit: 'contain', marginBottom: 8 }} />}
  282. <div style={{ width: 100 }}>
  283. <Uploader
  284. maxSize={2 * 1024 * 1024}
  285. prefix={setting.key === SystemSettingKey.SITE_LOGO ? 'logo/' : 'favicon/'}
  286. allowedTypes={['image/jpeg', 'image/png', 'image/svg+xml', 'image/x-icon']}
  287. onSuccess={(fileUrl) => {
  288. form.setFieldValue(setting.key, fileUrl);
  289. updateSettingsMutation.mutate([{
  290. key: setting.key,
  291. value: fileUrl,
  292. group: SystemSettingGroup.BASIC
  293. }]);
  294. }}
  295. onError={(error) => {
  296. message.error(`上传失败:${error.message}`);
  297. }}
  298. />
  299. </div>
  300. </div>
  301. );
  302. }
  303. return <Input placeholder={`请输入${setting.description || setting.key}`} />;
  304. };
  305. return (
  306. <div>
  307. <Card
  308. title={
  309. <Space>
  310. <Title level={2} style={{ margin: 0 }}>系统设置</Title>
  311. </Space>
  312. }
  313. extra={
  314. <Space>
  315. <Button
  316. icon={<ReloadOutlined />}
  317. onClick={handleReset}
  318. loading={resetSettingsMutation.isPending}
  319. >
  320. 重置默认
  321. </Button>
  322. </Space>
  323. }
  324. >
  325. <Spin spinning={isLoadingSettings || updateSettingsMutation.isPending}>
  326. <Tabs
  327. type="card"
  328. items={Object.values(SystemSettingGroup).map(group => ({
  329. key: group,
  330. label: String(GROUP_TITLES[group]),
  331. children: (
  332. <div>
  333. <Alert
  334. message={GROUP_DESCRIPTIONS[group]}
  335. type="info"
  336. showIcon
  337. style={{ marginBottom: 24 }}
  338. />
  339. <Form
  340. form={form}
  341. layout="vertical"
  342. onFinish={handleSubmit}
  343. >
  344. {settingsData
  345. ?.find(g => g.name === group)
  346. ?.settings.map(setting => (
  347. <Form.Item
  348. key={setting.key}
  349. label={setting.description || setting.key}
  350. name={setting.key}
  351. rules={[{ required: true, message: `请输入${setting.description || setting.key}` }]}
  352. >
  353. {renderSettingInput(setting)}
  354. </Form.Item>
  355. ))}
  356. <Form.Item>
  357. <Button
  358. type="primary"
  359. htmlType="submit"
  360. icon={<SaveOutlined />}
  361. loading={updateSettingsMutation.isPending}
  362. >
  363. 保存设置
  364. </Button>
  365. </Form.Item>
  366. </Form>
  367. </div>
  368. )
  369. }))}
  370. />
  371. </Spin>
  372. </Card>
  373. </div>
  374. );
  375. };
  376. // 主题设置页面
  377. export const ThemeSettingsPage = () => {
  378. const { isDark, currentTheme, updateTheme, saveTheme, resetTheme } = useTheme();
  379. const [form] = Form.useForm();
  380. const [loading, setLoading] = useState(false);
  381. // 处理配色方案选择
  382. const handleColorSchemeChange = (schemeName: keyof typeof COLOR_SCHEMES) => {
  383. const scheme = COLOR_SCHEMES[schemeName];
  384. form.setFieldsValue({
  385. primary_color: scheme.primary,
  386. background_color: scheme.background,
  387. text_color: scheme.text
  388. });
  389. updateTheme({
  390. primary_color: scheme.primary,
  391. background_color: scheme.background,
  392. text_color: scheme.text
  393. });
  394. };
  395. // 初始化表单数据
  396. useEffect(() => {
  397. form.setFieldsValue({
  398. theme_mode: currentTheme.theme_mode,
  399. primary_color: currentTheme.primary_color,
  400. background_color: currentTheme.background_color || (isDark ? '#141414' : '#f0f2f5'),
  401. font_size: currentTheme.font_size,
  402. is_compact: currentTheme.is_compact
  403. });
  404. }, [currentTheme, form, isDark]);
  405. // 处理表单提交
  406. const handleSubmit = async (values: any) => {
  407. try {
  408. setLoading(true);
  409. await saveTheme(values);
  410. } catch (error) {
  411. message.error('保存主题设置失败');
  412. } finally {
  413. setLoading(false);
  414. }
  415. };
  416. // 处理重置
  417. const handleReset = async () => {
  418. try {
  419. setLoading(true);
  420. await resetTheme();
  421. } catch (error) {
  422. message.error('重置主题设置失败');
  423. } finally {
  424. setLoading(false);
  425. }
  426. };
  427. // 处理表单值变化 - 实时预览
  428. const handleValuesChange = (changedValues: any) => {
  429. updateTheme(changedValues);
  430. };
  431. return (
  432. <div>
  433. <Title level={2}>主题设置</Title>
  434. <Card>
  435. <Spin spinning={loading}>
  436. <Form
  437. form={form}
  438. layout="vertical"
  439. onFinish={handleSubmit}
  440. onValuesChange={handleValuesChange}
  441. initialValues={{
  442. theme_mode: currentTheme.theme_mode,
  443. primary_color: currentTheme.primary_color,
  444. background_color: currentTheme.background_color || (isDark ? '#141414' : '#f0f2f5'),
  445. font_size: currentTheme.font_size,
  446. is_compact: currentTheme.is_compact
  447. }}
  448. >
  449. {/* 配色方案选择 */}
  450. <Form.Item label="预设配色方案">
  451. <Space wrap>
  452. {Object.entries(COLOR_SCHEMES).map(([key, scheme]) => (
  453. <Button
  454. key={key}
  455. onClick={() => handleColorSchemeChange(key as keyof typeof COLOR_SCHEMES)}
  456. style={{
  457. backgroundColor: scheme.background,
  458. color: scheme.text,
  459. borderColor: scheme.primary
  460. }}
  461. >
  462. {scheme.name}
  463. </Button>
  464. ))}
  465. </Space>
  466. </Form.Item>
  467. {/* 主题模式 */}
  468. <Form.Item
  469. label="主题模式"
  470. name="theme_mode"
  471. rules={[{ required: true, message: '请选择主题模式' }]}
  472. >
  473. <Radio.Group>
  474. <Radio value={ThemeMode.LIGHT}>浅色模式</Radio>
  475. <Radio value={ThemeMode.DARK}>深色模式</Radio>
  476. </Radio.Group>
  477. </Form.Item>
  478. {/* 主题色 */}
  479. <Form.Item
  480. label="主题色"
  481. name="primary_color"
  482. rules={[{ required: true, message: '请选择主题色' }]}
  483. >
  484. <ColorPicker
  485. value={form.getFieldValue('primary_color')}
  486. onChange={(color) => {
  487. form.setFieldValue('primary_color', color.toHexString());
  488. updateTheme({ primary_color: color.toHexString() });
  489. }}
  490. allowClear
  491. />
  492. </Form.Item>
  493. {/* 背景色 */}
  494. <Form.Item
  495. label="背景色"
  496. name="background_color"
  497. rules={[{ required: true, message: '请选择背景色' }]}
  498. >
  499. <ColorPicker
  500. value={form.getFieldValue('background_color')}
  501. onChange={(color) => {
  502. form.setFieldValue('background_color', color.toHexString());
  503. updateTheme({ background_color: color.toHexString() });
  504. }}
  505. allowClear
  506. />
  507. </Form.Item>
  508. {/* 文字颜色 */}
  509. <Form.Item
  510. label="文字颜色"
  511. name="text_color"
  512. rules={[{ required: true, message: '请选择文字颜色' }]}
  513. >
  514. <ColorPicker
  515. value={form.getFieldValue('text_color')}
  516. onChange={(color) => {
  517. form.setFieldValue('text_color', color.toHexString());
  518. updateTheme({ text_color: color.toHexString() });
  519. }}
  520. allowClear
  521. />
  522. </Form.Item>
  523. {/* 圆角大小 */}
  524. <Form.Item
  525. label="圆角大小"
  526. name="border_radius"
  527. rules={[{ required: true, message: '请设置圆角大小' }]}
  528. initialValue={6}
  529. >
  530. <InputNumber<number>
  531. min={0}
  532. max={20}
  533. addonAfter="px"
  534. />
  535. </Form.Item>
  536. {/* 字体大小 */}
  537. <Form.Item
  538. label="字体大小"
  539. name="font_size"
  540. rules={[{ required: true, message: '请选择字体大小' }]}
  541. >
  542. <Radio.Group>
  543. <Radio value={FontSize.SMALL}>小</Radio>
  544. <Radio value={FontSize.MEDIUM}>中</Radio>
  545. <Radio value={FontSize.LARGE}>大</Radio>
  546. </Radio.Group>
  547. </Form.Item>
  548. {/* 紧凑模式 */}
  549. <Form.Item
  550. label="紧凑模式"
  551. name="is_compact"
  552. valuePropName="checked"
  553. getValueFromEvent={(checked: boolean) => checked ? CompactMode.COMPACT : CompactMode.NORMAL}
  554. getValueProps={(value: CompactMode) => ({
  555. checked: value === CompactMode.COMPACT
  556. })}
  557. >
  558. <Switch
  559. checkedChildren="开启"
  560. unCheckedChildren="关闭"
  561. />
  562. </Form.Item>
  563. {/* 操作按钮 */}
  564. <Form.Item>
  565. <Space>
  566. <Button type="primary" htmlType="submit">
  567. 保存设置
  568. </Button>
  569. <Popconfirm
  570. title="确定要重置主题设置吗?"
  571. onConfirm={handleReset}
  572. okText="确定"
  573. cancelText="取消"
  574. >
  575. <Button>重置为默认值</Button>
  576. </Popconfirm>
  577. </Space>
  578. </Form.Item>
  579. </Form>
  580. </Spin>
  581. </Card>
  582. </div>
  583. );
  584. };