FormManagement.tsx 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825
  1. import React, { useState, useEffect, useMemo } from 'react';
  2. import { useNavigate, useLocation } from 'react-router-dom';
  3. import { Plus, Edit2, Trash2, Copy, Eye, X, Search, RotateCcw } from 'lucide-react';
  4. import { Button, Space, Table as AntdTable, Modal, message, Input, Form as AntdForm, Select, DatePicker, TimePicker, InputNumber, Switch, Radio, Checkbox, Card, Alert, Cascader, Upload, Tooltip } from 'antd';
  5. import { UploadOutlined } from '@ant-design/icons';
  6. import type { ColumnsType } from 'antd/es/table';
  7. import { toast } from 'sonner';
  8. import * as FormApi from '../api/bpm/form';
  9. import { DICT_TYPE, getDictLabel } from '../utils/dict';
  10. import { dateFormatter } from '../utils/formatTime';
  11. import { setConfAndFields2, FormCreateData } from '../utils/formCreate';
  12. export default function FormManagement() {
  13. const navigate = useNavigate();
  14. const location = useLocation();
  15. // 调试信息
  16. useEffect(() => {
  17. console.log('FormManagement 组件已加载');
  18. }, []);
  19. const [loading, setLoading] = useState(true);
  20. const [list, setList] = useState<FormApi.FormVO[]>([]);
  21. const [total, setTotal] = useState(0);
  22. const [queryParams, setQueryParams] = useState<FormApi.FormPageParams>({
  23. pageNo: 1,
  24. pageSize: 10,
  25. name: '',
  26. });
  27. const [searchName, setSearchName] = useState('');
  28. const [detailForm] = AntdForm.useForm();
  29. // 详情弹窗
  30. const [detailVisible, setDetailVisible] = useState(false);
  31. const [detailData, setDetailData] = useState<FormCreateData>({
  32. rule: [],
  33. option: {}
  34. });
  35. const defaultFormConfig = {
  36. name: '',
  37. labelPosition: 'right',
  38. formSize: 'middle',
  39. labelSuffix: '',
  40. labelWidth: 100,
  41. hideRequiredMark: false,
  42. showValidationError: true,
  43. inlineValidation: false,
  44. showSubmitButton: false,
  45. showResetButton: false,
  46. };
  47. // 渲染字段预览(支持嵌套结构)
  48. const renderFieldPreview = (field: any, parentSpanStyle?: React.CSSProperties): React.ReactNode => {
  49. const formConfig = detailData.option?.formConfig || defaultFormConfig;
  50. const layoutColumns = formConfig.layoutColumns || 1;
  51. const spanStyle = parentSpanStyle || (layoutColumns > 1 ? { gridColumn: `span ${Math.min(layoutColumns, field.span || 1)}` } : undefined);
  52. // 处理容器类型(card 和 grid)
  53. if (field.type === 'card') {
  54. const children = field.children || [];
  55. // 优先使用 label(字段名称),如果没有则使用 cardTitle,最后使用默认值
  56. const cardTitle = field.label || field.cardTitle || '卡片容器';
  57. return (
  58. <div key={field.id} style={spanStyle} className="mb-4">
  59. <Card title={cardTitle} className="w-full">
  60. <div className="space-y-4">
  61. {children.map((child: any) => renderFieldPreview(child))}
  62. </div>
  63. </Card>
  64. </div>
  65. );
  66. }
  67. if (field.type === 'grid') {
  68. const gridColumns = field.gridColumns || 2;
  69. const children = field.children || [];
  70. return (
  71. <div key={field.id} style={spanStyle} className="mb-4">
  72. <div
  73. style={{
  74. display: 'grid',
  75. gridTemplateColumns: `repeat(${gridColumns}, minmax(0, 1fr))`,
  76. gap: '12px',
  77. rowGap: '16px',
  78. }}
  79. >
  80. {children.map((child: any) => {
  81. const childSpanStyle = gridColumns > 1
  82. ? { gridColumn: `span ${Math.min(gridColumns, child.span || 1)}` }
  83. : undefined;
  84. return (
  85. <div key={child.id} style={childSpanStyle}>
  86. {renderFieldPreview(child)}
  87. </div>
  88. );
  89. })}
  90. </div>
  91. </div>
  92. );
  93. }
  94. // 处理 alert 类型
  95. if (field.type === 'alert') {
  96. const themeClass =
  97. field.alertTheme === 'dark'
  98. ? 'bg-gray-800 text-white border-gray-700'
  99. : '';
  100. return (
  101. <div key={field.id} className="mb-4" style={spanStyle}>
  102. <Alert
  103. message={field.alertTitle || field.label}
  104. description={field.alertDescription}
  105. type={field.alertType || 'info'}
  106. showIcon={field.alertShowIcon !== false}
  107. closable={field.alertClosable}
  108. closeText={field.alertCloseText}
  109. className={`${field.alertCentered ? 'text-center' : ''} ${themeClass}`}
  110. banner
  111. style={field.style}
  112. />
  113. {field.hint && <div className="text-xs text-gray-400 mt-1">{field.hint}</div>}
  114. </div>
  115. );
  116. }
  117. // 处理普通字段
  118. switch (field.type) {
  119. case 'textarea':
  120. return (
  121. <div key={field.id} style={spanStyle}>
  122. <AntdForm.Item
  123. label={(field.label || field.title || '') + (formConfig.labelSuffix || '')}
  124. name={field.name || field.field}
  125. required={field.required && !formConfig.hideRequiredMark}
  126. rules={field.required ? [{ required: true, message: field.requiredMessage || '请输入' }] : []}
  127. help={field.hint}
  128. >
  129. <Input.TextArea
  130. placeholder={typeof field.placeholder === 'string' ? field.placeholder : '请输入'}
  131. rows={4}
  132. allowClear={field.showClear}
  133. maxLength={field.maxLength}
  134. readOnly={field.readOnly}
  135. disabled={field.disabled}
  136. size={field.size || 'middle'}
  137. />
  138. </AntdForm.Item>
  139. </div>
  140. );
  141. case 'password':
  142. return (
  143. <div key={field.id} style={spanStyle}>
  144. <AntdForm.Item
  145. label={(field.label || field.title || '') + (formConfig.labelSuffix || '')}
  146. name={field.name || field.field}
  147. required={field.required && !formConfig.hideRequiredMark}
  148. rules={field.required ? [{ required: true, message: field.requiredMessage || '请输入' }] : []}
  149. help={field.hint}
  150. >
  151. <Input.Password
  152. placeholder={typeof field.placeholder === 'string' ? field.placeholder : '请输入'}
  153. allowClear={field.showClear}
  154. maxLength={field.maxLength}
  155. readOnly={field.readOnly}
  156. disabled={field.disabled}
  157. size={field.size || 'middle'}
  158. />
  159. </AntdForm.Item>
  160. </div>
  161. );
  162. case 'number':
  163. return (
  164. <div key={field.id} style={spanStyle}>
  165. <AntdForm.Item
  166. label={(field.label || field.title || '') + (formConfig.labelSuffix || '')}
  167. name={field.name || field.field}
  168. required={field.required && !formConfig.hideRequiredMark}
  169. rules={field.required ? [{ required: true, message: field.requiredMessage || '请输入' }] : []}
  170. help={field.hint}
  171. >
  172. <InputNumber
  173. style={{ width: '100%' }}
  174. placeholder={typeof field.placeholder === 'string' ? field.placeholder : '请输入'}
  175. max={field.maxLength}
  176. readOnly={field.readOnly}
  177. disabled={field.disabled}
  178. size={field.size || 'middle'}
  179. />
  180. </AntdForm.Item>
  181. </div>
  182. );
  183. case 'select':
  184. return (
  185. <div key={field.id} style={spanStyle}>
  186. <AntdForm.Item
  187. label={(field.label || field.title || '') + (formConfig.labelSuffix || '')}
  188. name={field.name || field.field}
  189. required={field.required && !formConfig.hideRequiredMark}
  190. rules={field.required ? [{ required: true, message: field.requiredMessage || '请选择' }] : []}
  191. help={field.hint}
  192. >
  193. <Select
  194. placeholder={typeof field.placeholder === 'string' ? field.placeholder : '请选择'}
  195. allowClear={field.showClear}
  196. disabled={field.disabled}
  197. size={field.size || 'middle'}
  198. >
  199. {(field.options || []).map((opt: any, idx: number) => (
  200. <Select.Option key={idx} value={opt.value}>{opt.label}</Select.Option>
  201. ))}
  202. </Select>
  203. </AntdForm.Item>
  204. </div>
  205. );
  206. case 'date':
  207. return (
  208. <div key={field.id} style={spanStyle}>
  209. <AntdForm.Item
  210. label={(field.label || field.title || '') + (formConfig.labelSuffix || '')}
  211. name={field.name || field.field}
  212. required={field.required && !formConfig.hideRequiredMark}
  213. rules={field.required ? [{ required: true, message: field.requiredMessage || '请选择日期' }] : []}
  214. help={field.hint}
  215. >
  216. <DatePicker
  217. className="w-full"
  218. placeholder={typeof field.placeholder === 'string' ? field.placeholder : undefined}
  219. allowClear={field.showClear}
  220. disabled={field.disabled}
  221. size={field.size || 'middle'}
  222. />
  223. </AntdForm.Item>
  224. </div>
  225. );
  226. case 'daterange':
  227. return (
  228. <div key={field.id} style={spanStyle}>
  229. <AntdForm.Item
  230. label={(field.label || field.title || '') + (formConfig.labelSuffix || '')}
  231. name={field.name || field.field}
  232. required={field.required && !formConfig.hideRequiredMark}
  233. rules={field.required ? [{ required: true, message: field.requiredMessage || '请选择日期范围' }] : []}
  234. help={field.hint}
  235. >
  236. <DatePicker.RangePicker
  237. className="w-full"
  238. placeholder={Array.isArray(field.placeholder) ? field.placeholder as [string, string] : ['开始日期', '结束日期']}
  239. allowClear={field.showClear}
  240. disabled={field.disabled}
  241. size={field.size || 'middle'}
  242. />
  243. </AntdForm.Item>
  244. </div>
  245. );
  246. case 'datetime':
  247. return (
  248. <div key={field.id} style={spanStyle}>
  249. <AntdForm.Item
  250. label={(field.label || field.title || '') + (formConfig.labelSuffix || '')}
  251. name={field.name || field.field}
  252. required={field.required && !formConfig.hideRequiredMark}
  253. rules={field.required ? [{ required: true, message: field.requiredMessage || '请选择日期时间' }] : []}
  254. help={field.hint}
  255. >
  256. <DatePicker
  257. className="w-full"
  258. placeholder={typeof field.placeholder === 'string' ? field.placeholder : undefined}
  259. allowClear={field.showClear}
  260. disabled={field.disabled}
  261. size={field.size || 'middle'}
  262. showTime={{ format: 'HH:mm:ss' }}
  263. format="YYYY-MM-DD HH:mm:ss"
  264. />
  265. </AntdForm.Item>
  266. </div>
  267. );
  268. case 'timepicker':
  269. return (
  270. <div key={field.id} style={spanStyle}>
  271. <AntdForm.Item
  272. label={(field.label || field.title || '') + (formConfig.labelSuffix || '')}
  273. name={field.name || field.field}
  274. required={field.required && !formConfig.hideRequiredMark}
  275. rules={field.required ? [{ required: true, message: field.requiredMessage || '请选择时间' }] : []}
  276. help={field.hint}
  277. >
  278. <TimePicker
  279. className="w-full"
  280. placeholder={typeof field.placeholder === 'string' ? field.placeholder : undefined}
  281. allowClear={field.showClear}
  282. disabled={field.disabled}
  283. size={field.size || 'middle'}
  284. format="HH:mm:ss"
  285. />
  286. </AntdForm.Item>
  287. </div>
  288. );
  289. case 'switch':
  290. return (
  291. <div key={field.id} style={spanStyle}>
  292. <AntdForm.Item
  293. label={(field.label || field.title || '') + (formConfig.labelSuffix || '')}
  294. name={field.name || field.field}
  295. required={field.required && !formConfig.hideRequiredMark}
  296. rules={field.required ? [{ required: true, message: field.requiredMessage || '请选择' }] : []}
  297. help={field.hint}
  298. valuePropName="checked"
  299. >
  300. <Switch disabled={field.disabled} />
  301. </AntdForm.Item>
  302. </div>
  303. );
  304. case 'radio':
  305. return (
  306. <div key={field.id} style={spanStyle}>
  307. <AntdForm.Item
  308. label={(field.label || field.title || '') + (formConfig.labelSuffix || '')}
  309. name={field.name || field.field}
  310. required={field.required && !formConfig.hideRequiredMark}
  311. rules={field.required ? [{ required: true, message: field.requiredMessage || '请选择' }] : []}
  312. help={field.hint}
  313. >
  314. <Radio.Group disabled={field.disabled}>
  315. {(field.options || []).map((opt: any, idx: number) => (
  316. <Radio key={idx} value={opt.value}>{opt.label}</Radio>
  317. ))}
  318. </Radio.Group>
  319. </AntdForm.Item>
  320. </div>
  321. );
  322. case 'checkbox':
  323. return (
  324. <div key={field.id} style={spanStyle}>
  325. <AntdForm.Item
  326. label={(field.label || field.title || '') + (formConfig.labelSuffix || '')}
  327. name={field.name || field.field}
  328. required={field.required && !formConfig.hideRequiredMark}
  329. rules={field.required ? [{ required: true, message: field.requiredMessage || '请选择' }] : []}
  330. help={field.hint}
  331. >
  332. <Checkbox.Group disabled={field.disabled}>
  333. {(field.options || []).map((opt: any, idx: number) => (
  334. <Checkbox key={idx} value={opt.value}>{opt.label}</Checkbox>
  335. ))}
  336. </Checkbox.Group>
  337. </AntdForm.Item>
  338. </div>
  339. );
  340. case 'cascader':
  341. return (
  342. <div key={field.id} style={spanStyle}>
  343. <AntdForm.Item
  344. label={(field.label || field.title || '') + (formConfig.labelSuffix || '')}
  345. name={field.name || field.field}
  346. required={field.required && !formConfig.hideRequiredMark}
  347. rules={field.required ? [{ required: true, message: field.requiredMessage || '请选择' }] : []}
  348. help={field.hint}
  349. >
  350. <Cascader
  351. placeholder={typeof field.placeholder === 'string' ? field.placeholder : '请选择'}
  352. options={field.cascaderOptions || []}
  353. className="w-full"
  354. allowClear={field.showClear}
  355. disabled={field.disabled}
  356. size={field.size || 'middle'}
  357. />
  358. </AntdForm.Item>
  359. </div>
  360. );
  361. case 'upload':
  362. return (
  363. <div key={field.id} style={spanStyle}>
  364. <AntdForm.Item
  365. label={(field.label || field.title || '') + (formConfig.labelSuffix || '')}
  366. name={field.name || field.field}
  367. required={field.required && !formConfig.hideRequiredMark}
  368. rules={field.required ? [{ required: true, message: field.requiredMessage || '请上传' }] : []}
  369. help={field.hint}
  370. >
  371. <Upload
  372. disabled={field.disabled}
  373. maxCount={field.uploadType === 'single-image' ? 1 : field.maxCount || (field.uploadType === 'multiple-image' ? 9 : undefined)}
  374. accept={field.accept || (field.uploadType === 'single-image' || field.uploadType === 'multiple-image' ? 'image/*' : undefined)}
  375. listType={(field.uploadType === 'file' ? 'text' : 'picture-card') as 'text' | 'picture-card' | 'picture'}
  376. multiple={field.uploadType !== 'single-image'}
  377. >
  378. {field.uploadType === 'file' ? (
  379. <Button icon={<UploadOutlined />}>上传文件</Button>
  380. ) : (
  381. <div>
  382. <UploadOutlined />
  383. <div style={{ marginTop: 8 }}>上传</div>
  384. </div>
  385. )}
  386. </Upload>
  387. </AntdForm.Item>
  388. </div>
  389. );
  390. default:
  391. return (
  392. <div key={field.id} style={spanStyle}>
  393. <AntdForm.Item
  394. label={(field.label || field.title || '') + (formConfig.labelSuffix || '')}
  395. name={field.name || field.field}
  396. required={field.required && !formConfig.hideRequiredMark}
  397. rules={field.required ? [{ required: true, message: field.requiredMessage || '请输入' }] : []}
  398. help={field.hint}
  399. >
  400. <Input
  401. type={field.inputType || 'text'}
  402. placeholder={typeof field.placeholder === 'string' ? field.placeholder : '请输入'}
  403. allowClear={field.showClear}
  404. maxLength={field.maxLength}
  405. readOnly={field.readOnly}
  406. disabled={field.disabled}
  407. size={field.size || 'middle'}
  408. />
  409. </AntdForm.Item>
  410. </div>
  411. );
  412. }
  413. };
  414. /** 查询列表 */
  415. const getList = async (params?: FormApi.FormPageParams) => {
  416. const searchParams = params || queryParams;
  417. setLoading(true);
  418. try {
  419. console.log('FormManagement: 开始获取表单列表', searchParams);
  420. const data = await FormApi.getFormPage(searchParams);
  421. console.log('FormManagement: 获取到数据', data);
  422. setList(data.list || []);
  423. setTotal(data.total || 0);
  424. } catch (error: any) {
  425. console.error('FormManagement: 获取表单列表失败', error);
  426. toast.error(error.message || '获取表单列表失败');
  427. // 确保即使失败也显示空列表
  428. setList([]);
  429. setTotal(0);
  430. } finally {
  431. setLoading(false);
  432. }
  433. };
  434. useEffect(() => {
  435. getList();
  436. // eslint-disable-next-line react-hooks/exhaustive-deps
  437. }, [queryParams.pageNo, queryParams.pageSize, queryParams.name]);
  438. // 监听路径变化,当从表单编辑器返回时自动刷新列表
  439. useEffect(() => {
  440. // 当路径包含 /form 时,刷新列表以确保显示最新数据
  441. if (location.pathname.includes('/form')) {
  442. getList();
  443. }
  444. // eslint-disable-next-line react-hooks/exhaustive-deps
  445. }, [location.pathname]);
  446. /** 添加/修改操作 */
  447. const openForm = (type: string, id?: number) => {
  448. const query: Record<string, string> = { type };
  449. if (id !== undefined && (typeof id === 'number' || typeof id === 'string')) {
  450. query.id = String(id);
  451. }
  452. // 跳转到表单编辑器(这里需要根据实际路由配置)
  453. navigate(`/bpm/form/editor?${new URLSearchParams(query).toString()}`);
  454. };
  455. /** 删除按钮操作 */
  456. const handleDelete = async (id: number) => {
  457. Modal.confirm({
  458. title: '确认删除',
  459. content: '确定要删除这条表单数据吗?',
  460. okText: '确定',
  461. cancelText: '取消',
  462. onOk: async () => {
  463. try {
  464. await FormApi.deleteForm(id);
  465. toast.success('删除成功');
  466. await getList();
  467. } catch (error: any) {
  468. toast.error(error.message || '删除失败');
  469. }
  470. }
  471. });
  472. };
  473. /** 详情操作 */
  474. const openDetail = async (rowId: number) => {
  475. try {
  476. const data = await FormApi.getForm(rowId);
  477. setConfAndFields2(setDetailData, data.conf, data.fields);
  478. setDetailVisible(true);
  479. } catch (error: any) {
  480. toast.error(error.message || '获取表单详情失败');
  481. }
  482. };
  483. // 搜索
  484. const handleSearch = () => {
  485. setQueryParams({
  486. ...queryParams,
  487. pageNo: 1,
  488. name: searchName.trim() || undefined,
  489. });
  490. };
  491. // 重置
  492. const handleReset = () => {
  493. setSearchName('');
  494. setQueryParams({
  495. ...queryParams,
  496. pageNo: 1,
  497. name: undefined,
  498. });
  499. };
  500. // 表格列配置
  501. const columns: ColumnsType<FormApi.FormVO> = [
  502. {
  503. title: '编号',
  504. dataIndex: 'id',
  505. width: 90,
  506. align: 'center',
  507. },
  508. {
  509. title: '表单名',
  510. dataIndex: 'name',
  511. width: 220,
  512. align: 'center',
  513. render: (name: string, record: FormApi.FormVO) => {
  514. if (!name || name === '-') return <span>{name || '-'}</span>;
  515. return (
  516. <span
  517. className="text-blue-600 cursor-pointer hover:text-blue-800 hover:underline"
  518. onClick={(e) => {
  519. e.stopPropagation();
  520. openDetail(record.id!);
  521. }}
  522. >
  523. {name}
  524. </span>
  525. );
  526. },
  527. },
  528. {
  529. title: '状态',
  530. dataIndex: 'status',
  531. width: 110,
  532. align: 'center',
  533. render: (status: number) => {
  534. const isOpen = Number(status) === 0;
  535. return (
  536. <span
  537. className={`inline-flex px-3 py-1 rounded-lg text-xs ${
  538. isOpen ? 'bg-green-100 text-green-700' : 'bg-gray-100 text-gray-600'
  539. }`}
  540. >
  541. {isOpen ? '开启' : '关闭'}
  542. </span>
  543. );
  544. },
  545. },
  546. {
  547. title: '备注',
  548. dataIndex: 'remark',
  549. width: 260,
  550. align: 'center',
  551. render: (text: string) => {
  552. const remarkText = text || '-';
  553. const maxLength = 20;
  554. const shouldTruncate = remarkText.length > maxLength;
  555. const displayText = shouldTruncate ? remarkText.slice(0, maxLength) + '...' : remarkText;
  556. return (
  557. <Tooltip placement="topLeft" title={remarkText}>
  558. <span>{displayText}</span>
  559. </Tooltip>
  560. );
  561. },
  562. },
  563. {
  564. title: '创建时间',
  565. dataIndex: 'createTime',
  566. width: 180,
  567. align: 'center',
  568. render: (text: string) => dateFormatter(text),
  569. },
  570. {
  571. title: '操作',
  572. width: 260,
  573. align: 'center',
  574. fixed: 'right',
  575. render: (_: any, record: FormApi.FormVO) => (
  576. <Space size="small">
  577. <Button
  578. type="link"
  579. size="small"
  580. icon={<Copy className="w-4 h-4" style={{ color: '#000000' }} />}
  581. onClick={() => openForm('copy', record.id)}
  582. style={{ color: '#000000' }}
  583. className="transition-colors"
  584. onMouseEnter={(e) => {
  585. e.currentTarget.style.color = '#1677ff';
  586. e.currentTarget.style.textDecoration = 'underline';
  587. e.currentTarget.querySelector('svg')?.setAttribute('style', 'color: #1677ff');
  588. }}
  589. onMouseLeave={(e) => {
  590. e.currentTarget.style.color = '#000000';
  591. e.currentTarget.style.textDecoration = 'none';
  592. e.currentTarget.querySelector('svg')?.setAttribute('style', 'color: #000000');
  593. }}
  594. >
  595. 复制
  596. </Button>
  597. <Button
  598. type="link"
  599. size="small"
  600. icon={<Edit2 className="w-4 h-4" style={{ color: '#000000' }} />}
  601. onClick={() => openForm('update', record.id)}
  602. style={{ color: '#000000' }}
  603. className="transition-colors"
  604. onMouseEnter={(e) => {
  605. e.currentTarget.style.color = '#1677ff';
  606. e.currentTarget.style.textDecoration = 'underline';
  607. e.currentTarget.querySelector('svg')?.setAttribute('style', 'color: #1677ff');
  608. }}
  609. onMouseLeave={(e) => {
  610. e.currentTarget.style.color = '#000000';
  611. e.currentTarget.style.textDecoration = 'none';
  612. e.currentTarget.querySelector('svg')?.setAttribute('style', 'color: #000000');
  613. }}
  614. >
  615. 编辑
  616. </Button>
  617. <Button
  618. type="link"
  619. size="small"
  620. icon={<Eye className="w-4 h-4" style={{ color: '#000000' }} />}
  621. onClick={() => openDetail(record.id!)}
  622. style={{ color: '#000000' }}
  623. className="transition-colors"
  624. onMouseEnter={(e) => {
  625. e.currentTarget.style.color = '#1677ff';
  626. e.currentTarget.style.textDecoration = 'underline';
  627. e.currentTarget.querySelector('svg')?.setAttribute('style', 'color: #1677ff');
  628. }}
  629. onMouseLeave={(e) => {
  630. e.currentTarget.style.color = '#000000';
  631. e.currentTarget.style.textDecoration = 'none';
  632. e.currentTarget.querySelector('svg')?.setAttribute('style', 'color: #000000');
  633. }}
  634. >
  635. 预览
  636. </Button>
  637. <Button
  638. type="link"
  639. size="small"
  640. icon={<Trash2 className="w-4 h-4" style={{ color: '#000000' }} />}
  641. onClick={() => handleDelete(record.id!)}
  642. style={{ color: '#000000' }}
  643. className="transition-colors"
  644. onMouseEnter={(e) => {
  645. e.currentTarget.style.color = '#1677ff';
  646. e.currentTarget.style.textDecoration = 'underline';
  647. e.currentTarget.querySelector('svg')?.setAttribute('style', 'color: #1677ff');
  648. }}
  649. onMouseLeave={(e) => {
  650. e.currentTarget.style.color = '#000000';
  651. e.currentTarget.style.textDecoration = 'none';
  652. e.currentTarget.querySelector('svg')?.setAttribute('style', 'color: #000000');
  653. }}
  654. >
  655. 删除
  656. </Button>
  657. </Space>
  658. ),
  659. },
  660. ];
  661. // 计算分页数据
  662. const totalPages = Math.ceil(total / queryParams.pageSize) || 1;
  663. return (
  664. <div className="space-y-6">
  665. {/* 操作栏和表格容器 */}
  666. <div className="bg-white rounded-2xl border border-gray-200/50 shadow-sm overflow-hidden">
  667. {/* 查询与操作栏 */}
  668. <div className="p-4 lg:p-5 border-b border-gray-200/50">
  669. <div className="flex flex-wrap items-center justify-between gap-3">
  670. <div className="flex items-center gap-3">
  671. <span className="text-sm text-gray-700">表单名:</span>
  672. <Input
  673. value={searchName}
  674. onChange={(e) => setSearchName(e.target.value)}
  675. placeholder="请输入表单名"
  676. allowClear
  677. style={{ width: 240 }}
  678. />
  679. </div>
  680. <Space size="small">
  681. <Button type="primary" icon={<Search className="w-4 h-4" />} onClick={handleSearch}>
  682. 搜索
  683. </Button>
  684. <Button icon={<RotateCcw className="w-4 h-4" />} onClick={handleReset}>
  685. 重置
  686. </Button>
  687. <Button
  688. type="primary"
  689. icon={<Plus className="w-4 h-4" />}
  690. onClick={() => openForm('create')}
  691. >
  692. 新建
  693. </Button>
  694. </Space>
  695. </div>
  696. </div>
  697. {/* 表格容器 */}
  698. <div className="overflow-hidden min-w-0">
  699. <AntdTable
  700. loading={loading}
  701. columns={columns}
  702. dataSource={list}
  703. rowKey="id"
  704. pagination={false}
  705. scroll={{ x: 'max-content' }}
  706. />
  707. </div>
  708. </div>
  709. {/* 分页 */}
  710. {total > 0 && (
  711. <div className="bg-white rounded-lg border border-gray-200 px-6 py-4">
  712. <div className="flex items-center justify-between">
  713. <div className="text-sm text-gray-600">
  714. 共 <span className="text-blue-600 font-medium">{total}</span> 条记录
  715. </div>
  716. <div className="flex gap-2">
  717. <Button
  718. onClick={() => setQueryParams({ ...queryParams, pageNo: queryParams.pageNo - 1 })}
  719. disabled={queryParams.pageNo <= 1}
  720. >
  721. 上一页
  722. </Button>
  723. <span className="px-4 py-2 text-sm text-gray-600 flex items-center">
  724. {queryParams.pageNo} / {totalPages}
  725. </span>
  726. <Button
  727. onClick={() => setQueryParams({ ...queryParams, pageNo: queryParams.pageNo + 1 })}
  728. disabled={queryParams.pageNo >= totalPages}
  729. >
  730. 下一页
  731. </Button>
  732. </div>
  733. </div>
  734. </div>
  735. )}
  736. {/* 表单详情的弹窗 */}
  737. <Modal
  738. title="表单详情"
  739. open={detailVisible}
  740. onCancel={() => setDetailVisible(false)}
  741. footer={[
  742. <div key="tip" style={{ display: 'flex', alignItems: 'center', justifyContent: 'flex-end', gap: '12px', width: '100%' }}>
  743. <span style={{ color: '#ff4d4f', fontSize: '14px' }}>仅为表单预览,点击提交无效</span>
  744. <Button
  745. key="submit"
  746. type="primary"
  747. onClick={() => {
  748. // 详情预览提交按钮不执行任何操作,也不关闭弹窗
  749. return;
  750. }}
  751. >
  752. 提交
  753. </Button>
  754. </div>,
  755. ]}
  756. width={800}
  757. >
  758. <div className="p-2">
  759. {(() => {
  760. const formConfig = detailData.option?.formConfig || defaultFormConfig;
  761. const layoutColumns = formConfig.layoutColumns || 1;
  762. const gridStyle = layoutColumns > 1 ? {
  763. display: 'grid',
  764. gridTemplateColumns: `repeat(${layoutColumns}, minmax(0, 1fr))`,
  765. gap: '12px',
  766. rowGap: '16px',
  767. } : undefined;
  768. return (
  769. <AntdForm
  770. form={detailForm}
  771. layout={formConfig.labelPosition === 'top' ? 'vertical' : formConfig.labelPosition === 'left' ? 'horizontal' : 'horizontal'}
  772. size={formConfig.formSize === 'default' ? 'middle' : formConfig.formSize}
  773. requiredMark={formConfig.hideRequiredMark ? false : undefined}
  774. labelCol={formConfig.labelWidth ? {
  775. style: {
  776. width: `${formConfig.labelWidth}px`,
  777. textAlign: formConfig.labelPosition === 'left' ? 'left' : formConfig.labelPosition === 'right' ? 'right' : 'left'
  778. }
  779. } : undefined}
  780. >
  781. <div
  782. style={gridStyle}
  783. className={layoutColumns === 1 ? 'space-y-4' : 'form-detail-grid'}
  784. >
  785. <style>{`
  786. .form-detail-grid .ant-form-item {
  787. margin-bottom: 12px;
  788. }
  789. `}</style>
  790. {(detailData.rule || []).map((field: any) => renderFieldPreview(field))}
  791. </div>
  792. </AntdForm>
  793. );
  794. })()}
  795. </div>
  796. </Modal>
  797. </div>
  798. );
  799. }