pages_settings.tsx 19 KB

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