Commit 386bf8b4d3ac6c6637f40861ed42aab056a11905

Authored by 曾国涛
1 parent a46cc9ad

feat(product): 优化采购管理功能

- 重构采购管理页面,增加更多筛选条件和操作功能- 新增采购单号、创建人等搜索条件
- 添加采购明细的可编辑表格功能
- 集成产品选择接口,实现产品信息自动填充
-优化附件展示方式,支持多附件查看
- 新增审核功能,支持采购单审核操作
src/pages/product/procure/index.tsx
1 import ButtonConfirm from '@/components/ButtomConfirm'; 1 import ButtonConfirm from '@/components/ButtomConfirm';
2 import { RESPONSE_CODE } from '@/constants/enum'; 2 import { RESPONSE_CODE } from '@/constants/enum';
3 import AddOrUpdate from '@/pages/product/procure/components/AddOrUpdate'; 3 import AddOrUpdate from '@/pages/product/procure/components/AddOrUpdate';
4 -import { postProcureBillDelete, postProcureBillPage } from '@/services'; 4 +import {
  5 + postProcureBillDelete,
  6 + postProcureBillPage,
  7 + postServiceConstProcureBillAuditStatus,
  8 +} from '@/services';
  9 +import { enumToSelect } from '@/utils';
5 import { ProTable, type ActionType } from '@ant-design/pro-components'; 10 import { ProTable, type ActionType } from '@ant-design/pro-components';
6 import { message } from 'antd'; 11 import { message } from 'antd';
7 import { useRef } from 'react'; 12 import { useRef } from 'react';
  13 +import Audit from './components/Audit';
8 14
9 export default () => { 15 export default () => {
10 const actionRef = useRef<ActionType>(); 16 const actionRef = useRef<ActionType>();
11 const columns = [ 17 const columns = [
12 { 18 {
  19 + title: '单号',
  20 + dataIndex: 'id',
  21 + ellipsis: true,
  22 + hideInSearch: true,
  23 + },
  24 + {
13 title: '创建时间', 25 title: '创建时间',
14 dataIndex: 'createTime', 26 dataIndex: 'createTime',
15 ellipsis: true, 27 ellipsis: true,
@@ -52,32 +64,84 @@ export default () =&gt; { @@ -52,32 +64,84 @@ export default () =&gt; {
52 hideInSearch: true, 64 hideInSearch: true,
53 }, 65 },
54 { 66 {
  67 + title: '采购单号',
  68 + dataIndex: 'id',
  69 + ellipsis: true,
  70 + hideInTable: true,
  71 + },
  72 + {
  73 + title: '创建人',
  74 + dataIndex: 'createByNameLike',
  75 + ellipsis: true,
  76 + hideInTable: true,
  77 + },
  78 + {
  79 + title: '创建时间',
  80 + valueType: 'dateTimeRange',
  81 + hideInTable: true,
  82 + search: {
  83 + transform: (value) => {
  84 + if (value) {
  85 + return {
  86 + createTimeGe: value[0],
  87 + createTimeLe: value[1],
  88 + };
  89 + }
  90 + },
  91 + },
  92 + },
  93 + {
  94 + title: '审核状态',
  95 + valueType: 'select',
  96 + key: 'auditStatus',
  97 + dataIndex: 'auditStatus',
  98 + filters: true,
  99 + onFilter: true,
  100 + hideInTable: true,
  101 + request: async () => {
  102 + const res = await postServiceConstProcureBillAuditStatus();
  103 + return enumToSelect(res.data);
  104 + },
  105 + },
  106 + {
55 title: '操作', 107 title: '操作',
56 valueType: 'option', 108 valueType: 'option',
57 key: 'option', 109 key: 'option',
58 render: (text, record) => [ 110 render: (text, record) => [
59 - <AddOrUpdate  
60 - key="update"  
61 - record={record}  
62 - onfinish={() => {  
63 - actionRef.current?.reload();  
64 - }}  
65 - />,  
66 - <ButtonConfirm  
67 - key="delete"  
68 - className="p-0"  
69 - title={'确认删除该记录?'}  
70 - text="删除"  
71 - onConfirm={async () => {  
72 - let res = await postProcureBillDelete({  
73 - query: { id: record.id },  
74 - });  
75 - if (res) {  
76 - message.success(res.message); 111 + record.paths?.includes('UPDATE') && (
  112 + <AddOrUpdate
  113 + key="update"
  114 + record={record}
  115 + onfinish={() => {
77 actionRef.current?.reload(); 116 actionRef.current?.reload();
78 - }  
79 - }}  
80 - />, 117 + }}
  118 + />
  119 + ),
  120 + record.paths?.includes('UPDATE') && (
  121 + <ButtonConfirm
  122 + key="delete"
  123 + className="p-0"
  124 + title={'确认删除该记录?'}
  125 + text="删除"
  126 + onConfirm={async () => {
  127 + let res = await postProcureBillDelete({
  128 + query: { id: record.id },
  129 + });
  130 + if (res) {
  131 + message.success(res.message);
  132 + actionRef.current?.reload();
  133 + }
  134 + }}
  135 + />
  136 + ),
  137 + record.paths?.includes('AUDIT') && (
  138 + <Audit
  139 + recordId={record.id}
  140 + onClose={() => {
  141 + actionRef.current?.reload();
  142 + }}
  143 + ></Audit>
  144 + ),
81 ], 145 ],
82 }, 146 },
83 ]; 147 ];
@@ -128,7 +192,41 @@ export default () =&gt; { @@ -128,7 +192,41 @@ export default () =&gt; {
128 }, 192 },
129 { title: '数量', dataIndex: 'number', key: 'number' }, 193 { title: '数量', dataIndex: 'number', key: 'number' },
130 { title: '小计', dataIndex: 'totalPrice', key: 'totalPrice' }, 194 { title: '小计', dataIndex: 'totalPrice', key: 'totalPrice' },
131 - { title: '附件', dataIndex: 'annex', key: 'annex' }, 195 + {
  196 + title: '附件',
  197 + dataIndex: 'annex',
  198 + key: 'annex',
  199 + render: (_, record) => (
  200 + <div>
  201 + {record.annexList?.map((url, index) => {
  202 + const shortName =
  203 + url.split('/').pop()?.slice(0, 15) ||
  204 + `附件 ${index + 1}`;
  205 + return (
  206 + <a
  207 + key={index}
  208 + href={url}
  209 + target="_blank"
  210 + rel="noopener noreferrer"
  211 + title={url} // 悬停显示完整链接
  212 + style={{
  213 + display: 'block',
  214 + marginBottom: '4px',
  215 + whiteSpace: 'nowrap',
  216 + overflow: 'hidden',
  217 + textOverflow: 'ellipsis',
  218 + maxWidth: '200px', // 限制显示宽度
  219 + }}
  220 + >
  221 + {shortName.length < url.split('/').pop()?.length
  222 + ? `${shortName}...`
  223 + : shortName}
  224 + </a>
  225 + );
  226 + })}
  227 + </div>
  228 + ),
  229 + },
132 { title: '备注', dataIndex: 'notes', key: 'notes' }, 230 { title: '备注', dataIndex: 'notes', key: 'notes' },
133 ]} 231 ]}
134 headerTitle={false} 232 headerTitle={false}
@@ -139,7 +237,6 @@ export default () =&gt; { @@ -139,7 +237,6 @@ export default () =&gt; {
139 /> 237 />
140 ), 238 ),
141 }} 239 }}
142 - search={false}  
143 dateFormatter="string" 240 dateFormatter="string"
144 headerTitle="采购管理" 241 headerTitle="采购管理"
145 options={false} 242 options={false}
src/pages/product/productCollect/components/AddOrUpdate.tsx
1 import { postProductCollectBillAddOrModify } from '@/services'; 1 import { postProductCollectBillAddOrModify } from '@/services';
2 import { useModel } from '@@/exports'; 2 import { useModel } from '@@/exports';
3 import { 3 import {
  4 + ActionType,
  5 + EditableProTable,
4 ModalForm, 6 ModalForm,
5 - ProFormDigit,  
6 - ProFormSelect,  
7 - ProFormText, 7 + ProCard,
  8 + ProColumns,
  9 + ProForm,
  10 + ProFormDependency,
  11 + ProFormField,
  12 + ProFormSwitch,
8 ProFormTextArea, 13 ProFormTextArea,
9 } from '@ant-design/pro-components'; 14 } from '@ant-design/pro-components';
10 import { Button, Form, message } from 'antd'; 15 import { Button, Form, message } from 'antd';
  16 +import React, { useEffect, useRef, useState } from 'react';
  17 +
  18 +export default ({ record, onfinish }) => {
  19 + const [editableKeys, setEditableRowKeys] = useState<React.Key[]>([]);
  20 + const [controlled, setControlled] = useState<boolean>(false);
  21 + const [processedRecord, setProcessedRecord] = useState(record);
  22 + const formRef = useRef(null);
  23 + const editorFormRef = useRef(null);
  24 + const { getProducts } = useModel('enum');
  25 + const actionRef = useRef<ActionType>();
  26 + useEffect(() => {
  27 + if (record?.details) {
  28 + const updateddetails = record.details.map((item) => ({
  29 + ...item,
  30 + key: item.key || `key-${Math.random().toString(36).substr(2, 9)}`, // 动态生成唯一 key
  31 + }));
  32 + setProcessedRecord({
  33 + ...record,
  34 + details: updateddetails,
  35 + });
  36 + }
  37 + }, [record]);
  38 +
  39 + const columns: ProColumns[] = [
  40 + {
  41 + title: '商品',
  42 + dataIndex: 'productId',
  43 + valueType: 'select',
  44 + request: async () => {
  45 + const res = await getProducts();
  46 + return res.map((item) => ({
  47 + ...item,
  48 + label: item.name,
  49 + value: item.id,
  50 + productUnitName: item.baseUnitName,
  51 + productUnitPrice: item.unitPrice,
  52 + }));
  53 + },
  54 + fieldProps: (_, { rowIndex }) => ({
  55 + onSelect: (value, option) => {
  56 + const currentTableData = editorFormRef.current?.getRowsData?.();
  57 + if (currentTableData) {
  58 + const updatedData = [...currentTableData];
  59 + updatedData[rowIndex] = {
  60 + ...updatedData[rowIndex],
  61 + productUnitName: option.productUnitName,
  62 + productUnitPrice: option.productUnitPrice,
  63 + };
  64 + formRef.current?.setFieldsValue({
  65 + details: updatedData,
  66 + });
  67 + }
  68 + },
  69 + }),
  70 + },
  71 + {
  72 + title: '单位',
  73 + dataIndex: 'productUnitName',
  74 + valueType: 'text',
  75 + editable: false,
  76 + },
  77 + {
  78 + title: '单价',
  79 + dataIndex: 'productUnitPrice',
  80 + valueType: 'digit',
  81 + editable: false,
  82 + },
  83 + {
  84 + title: '数量',
  85 + dataIndex: 'number',
  86 + valueType: 'digit',
  87 + },
  88 + {
  89 + title: '备注',
  90 + dataIndex: 'notes',
  91 + valueType: 'textarea',
  92 + },
  93 + {
  94 + title: '操作',
  95 + valueType: 'option',
  96 + render: (text, record, _, action) => [
  97 + <a
  98 + key="editable"
  99 + onClick={() => {
  100 + action?.startEditable?.(record.key);
  101 + }}
  102 + >
  103 + 编辑
  104 + </a>,
  105 + <a
  106 + key="delete"
  107 + onClick={() => {
  108 + const tableDataSource = formRef.current?.getFieldValue('details');
  109 + formRef.current?.setFieldsValue({
  110 + table: tableDataSource.filter((item) => item.key !== record.key),
  111 + });
  112 + }}
  113 + >
  114 + 删除
  115 + </a>,
  116 + ],
  117 + },
  118 + ];
11 119
12 -export default ({ record, onFinish }) => {  
13 const [form] = Form.useForm(); 120 const [form] = Form.useForm();
14 - const { getWarehouse } = useModel('enum');  
15 return ( 121 return (
16 <ModalForm 122 <ModalForm
  123 + formRef={formRef}
  124 + initialValues={processedRecord}
  125 + validateTrigger="onBlur"
17 title="新建表单" 126 title="新建表单"
18 trigger={ 127 trigger={
19 record?.id ? ( 128 record?.id ? (
@@ -24,66 +133,99 @@ export default ({ record, onFinish }) =&gt; { @@ -24,66 +133,99 @@ export default ({ record, onFinish }) =&gt; {
24 } 133 }
25 form={form} 134 form={form}
26 autoFocusFirstInput 135 autoFocusFirstInput
  136 + width={1500}
27 modalProps={{ 137 modalProps={{
28 destroyOnClose: true, 138 destroyOnClose: true,
29 onCancel: () => console.log('run'), 139 onCancel: () => console.log('run'),
30 }} 140 }}
31 submitTimeout={2000} 141 submitTimeout={2000}
32 onFinish={async (values) => { 142 onFinish={async (values) => {
33 - let res = await postProductCollectBillAddOrModify({  
34 - data: values, 143 + const res = await postProductCollectBillAddOrModify({
  144 + data: {
  145 + ...record,
  146 + ...values,
  147 + },
35 }); 148 });
36 if (res) { 149 if (res) {
37 message.success(res.message); 150 message.success(res.message);
38 - onFinish(); 151 + onfinish();
39 } 152 }
40 return true; 153 return true;
41 }} 154 }}
42 > 155 >
43 - <ProFormDigit  
44 - label="id"  
45 - name="id"  
46 - initialValue={record?.id}  
47 - width="sm"  
48 - hidden={true}  
49 - />  
50 - <ProFormText  
51 - width="md"  
52 - initialValue={record?.productName}  
53 - name="productName"  
54 - label="申领物品"  
55 - rules={[{ required: true, message: '申领物品必填' }]}  
56 - />  
57 - <ProFormDigit  
58 - label="申领数量"  
59 - name="productNumber"  
60 - initialValue={record?.productNumber}  
61 - width="sm"  
62 - min={1}  
63 - rules={[{ required: true, message: '申领数量必填' }]}  
64 - />  
65 - <ProFormSelect  
66 - width="md"  
67 - request={async () => {  
68 - const res = await getWarehouse();  
69 - console.log('options:' + res);  
70 - let options = Object.entries(res).map(([value, label]) => ({  
71 - label,  
72 - value,  
73 - }));  
74 - console.log('options:' + options);  
75 - return options; 156 + <EditableProTable
  157 + rowKey="key"
  158 + scroll={{
  159 + x: 960,
  160 + }}
  161 + editableFormRef={editorFormRef}
  162 + headerTitle="可编辑表格"
  163 + maxLength={5}
  164 + name="details"
  165 + controlled={controlled}
  166 + recordCreatorProps={{
  167 + position: 'bottom',
  168 + record: () => ({
  169 + key: `key-${Math.random().toString(36).substr(2, 9)}`,
  170 + }),
  171 + }}
  172 + actionRef={actionRef}
  173 + toolBarRender={() => [
  174 + <ProFormSwitch
  175 + key="render"
  176 + fieldProps={{
  177 + style: {
  178 + marginBlockEnd: 0,
  179 + },
  180 + checked: controlled,
  181 + onChange: (value) => {
  182 + setControlled(value);
  183 + },
  184 + }}
  185 + checkedChildren="数据更新通知 Form"
  186 + unCheckedChildren="保存后通知 Form"
  187 + noStyle
  188 + />,
  189 + <Button
  190 + key="rows"
  191 + onClick={() => {
  192 + const rows = editorFormRef.current?.getRowsData?.();
  193 + console.log(rows);
  194 + }}
  195 + >
  196 + 获取 table 的数据
  197 + </Button>,
  198 + ]}
  199 + columns={columns}
  200 + editable={{
  201 + type: 'multiple',
  202 + editableKeys,
  203 + onChange: setEditableRowKeys,
  204 + actionRender: (row, config, defaultDom) => {
  205 + return [defaultDom.save, defaultDom.delete, defaultDom.cancel];
  206 + },
76 }} 207 }}
77 - // initialValue={record?.warehouseCode}  
78 - name="warehouseCode"  
79 - label="申领仓库"  
80 - rules={[{ required: true, message: '申领仓库为必填项' }]}  
81 - />  
82 - <ProFormTextArea  
83 - initialValue={record?.applyRemarks}  
84 - name="applyRemarks"  
85 - label="申领备注"  
86 /> 208 />
  209 + <ProForm.Item>
  210 + <ProCard title="表格数据" headerBordered collapsible defaultCollapsed>
  211 + <ProFormDependency name={['details']}>
  212 + {({ details }) => (
  213 + <ProFormField
  214 + ignoreFormItem
  215 + fieldProps={{
  216 + style: {
  217 + width: '100%',
  218 + },
  219 + }}
  220 + mode="read"
  221 + valueType="jsonCode"
  222 + text={JSON.stringify(details)}
  223 + />
  224 + )}
  225 + </ProFormDependency>
  226 + </ProCard>
  227 + </ProForm.Item>
  228 + <ProFormTextArea name="auditRemarks" label="备注" />
87 </ModalForm> 229 </ModalForm>
88 ); 230 );
89 }; 231 };