WEB/src/pages/Product/List/index.tsx

586 lines
17 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import {
productcontrollerBatchdeleteproduct,
productcontrollerBatchupdateproduct,
productcontrollerDeleteproduct,
productcontrollerGetcategoriesall,
productcontrollerGetproductlist,
productcontrollerUpdatenamecn,
} from '@/servers/api/product';
import {
ActionType,
ModalForm,
PageContainer,
ProColumns,
ProFormSelect,
ProFormText,
ProTable,
} from '@ant-design/pro-components';
import { request } from '@umijs/max';
import { App, Button, Modal, Popconfirm, Tag, Upload } from 'antd';
import React, { useEffect, useRef, useState } from 'react';
import CreateForm from './CreateForm';
import EditForm from './EditForm';
import SyncToSiteModal from './SyncToSiteModal';
const NameCn: React.FC<{
id: number;
value: string | undefined;
tableRef: React.MutableRefObject<ActionType | undefined>;
}> = ({ value, tableRef, id }) => {
const { message } = App.useApp();
const [editable, setEditable] = React.useState<boolean>(false);
if (!editable)
return <div onClick={() => setEditable(true)}>{value || '-'}</div>;
return (
<ProFormText
initialValue={value}
fieldProps={{
autoFocus: true,
onBlur: async (e: React.FocusEvent<HTMLInputElement>) => {
if (!e.target.value) return setEditable(false);
const { success, message: errMsg } =
await productcontrollerUpdatenamecn({
id,
nameCn: e.target.value,
});
setEditable(false);
if (!success) {
return message.error(errMsg);
}
tableRef?.current?.reloadAndRest?.();
},
}}
/>
);
};
const AttributesCell: React.FC<{ record: any }> = ({ record }) => {
return (
<div>
{(record.attributes || []).map((data: any, idx: number) => (
<Tag key={idx} color="purple" style={{ marginBottom: 4 }}>
{data?.dict?.name}: {data.name}
</Tag>
))}
</div>
);
};
const ComponentsCell: React.FC<{ components?: any[] }> = ({ components }) => {
return (
<div>
{components && components.length ? (
components.map((component: any) => (
<Tag key={component.id} color="blue" style={{ marginBottom: 4 }}>
{component.sku || `#${component.id}`} × {component.quantity}
(:
{component.stock
?.map((s: any) => `${s.name}:${s.quantity}`)
.join(', ') || '-'}
)
</Tag>
))
) : (
<span>-</span>
)}
</div>
);
};
const BatchEditModal: React.FC<{
visible: boolean;
onClose: () => void;
selectedRows: API.Product[];
tableRef: React.MutableRefObject<ActionType | undefined>;
onSuccess: () => void;
}> = ({ visible, onClose, selectedRows, tableRef, onSuccess }) => {
const { message } = App.useApp();
const [categories, setCategories] = useState<any[]>([]);
useEffect(() => {
if (visible) {
productcontrollerGetcategoriesall().then((res: any) => {
setCategories(res?.data || []);
});
}
}, [visible]);
return (
<ModalForm
title={`批量修改 (${selectedRows.length} 项)`}
open={visible}
onOpenChange={(open) => !open && onClose()}
modalProps={{ destroyOnClose: true }}
onFinish={async (values) => {
const ids = selectedRows.map((row) => row.id);
const updateData: any = { ids };
// 只有当用户输入了值才进行更新
if (values.price) updateData.price = Number(values.price);
if (values.promotionPrice)
updateData.promotionPrice = Number(values.promotionPrice);
if (values.categoryId) updateData.categoryId = values.categoryId;
if (Object.keys(updateData).length <= 1) {
message.warning('未修改任何属性');
return false;
}
const { success, message: errMsg } =
await productcontrollerBatchupdateproduct(updateData);
if (success) {
message.success('批量修改成功');
onSuccess();
tableRef.current?.reload();
return true;
} else {
message.error(errMsg);
return false;
}
}}
>
<ProFormText name="price" label="价格" placeholder="不修改请留空" />
<ProFormText
name="promotionPrice"
label="促销价格"
placeholder="不修改请留空"
/>
<ProFormSelect
name="categoryId"
label="分类"
options={categories.map((c) => ({ label: c.title, value: c.id }))}
placeholder="不修改请留空"
/>
</ModalForm>
);
};
const ProductList = ({
filter,
columns,
}: {
filter: { skus: string[] };
columns: any[];
}) => {
return (
<ProTable
request={async (pag) => {
const { data, success } = await productcontrollerGetproductlist({
where: filter,
});
if (!success) return [];
return data || [];
}}
columns={columns}
pagination={false}
rowKey="id"
bordered
size="small"
scroll={{ x: 'max-content' }}
headerTitle={null}
toolBarRender={false}
/>
);
};
const List: React.FC = () => {
const actionRef = useRef<ActionType>();
// 状态:存储当前选中的行
const [selectedRows, setSelectedRows] = React.useState<API.Product[]>([]);
const [batchEditModalVisible, setBatchEditModalVisible] = useState(false);
const [syncProducts, setSyncProducts] = useState<API.Product[]>([]);
const [syncModalVisible, setSyncModalVisible] = useState(false);
const { message } = App.useApp();
// 导出产品 CSV(带认证请求)
const handleDownloadProductsCSV = async () => {
try {
// 发起认证请求获取 CSV Blob
const blob = await request('/product/export', { responseType: 'blob' });
// 构建下载文件名
const d = new Date();
const pad = (n: number) => String(n).padStart(2, '0');
const filename = `products-${d.getFullYear()}${pad(
d.getMonth() + 1,
)}${pad(d.getDate())}.csv`;
// 创建临时链接并触发下载
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
a.remove();
window.URL.revokeObjectURL(url);
} catch (error) {
message.error('导出失败');
}
};
const columns: ProColumns<API.Product>[] = [
{
title: 'sku',
dataIndex: 'sku',
sorter: true,
},
{
title: '关联商品',
dataIndex: 'siteSkus',
width: 200,
render: (_, record) => (
<>
{record.siteSkus?.map((siteSku, index) => (
<Tag key={index} color="cyan">
{siteSku}
</Tag>
))}
</>
),
},
{
title: '图片',
dataIndex: 'image',
width: 100,
valueType: 'image',
},
{
title: '名称',
dataIndex: 'name',
sorter: true,
},
{
title: '中文名',
dataIndex: 'nameCn',
render: (_, record) => {
return (
<NameCn value={record.nameCn} id={record.id} tableRef={actionRef} />
);
},
},
{
title: '价格',
dataIndex: 'price',
hideInSearch: true,
sorter: true,
},
{
title: '促销价',
dataIndex: 'promotionPrice',
hideInSearch: true,
sorter: true,
},
{
title: '商品类型',
dataIndex: 'category',
render: (_, record: any) => {
return record.category?.title || record.category?.name || '-';
},
},
{
title: '属性',
dataIndex: 'attributes',
hideInSearch: true,
render: (_, record) => <AttributesCell record={record} />,
},
{
title: '产品类型',
dataIndex: 'type',
valueType: 'select',
valueEnum: {
single: { text: '单品' },
bundle: { text: '套装' },
},
render: (_, record) => {
// 如果类型不存在,则返回-
if (!record.type) return '-';
// 判断是否为单品
const isSingle = record.type === 'single';
// 根据类型显示不同颜色的标签
return (
<Tag color={isSingle ? 'green' : 'orange'}>
{isSingle ? '单品' : '套装'}
</Tag>
);
},
},
{
title: '构成',
dataIndex: 'components',
hideInSearch: true,
render: (_, record) => <ComponentsCell components={record.components} />,
},
{
title: '描述',
dataIndex: 'description',
hideInSearch: true,
},
{
title: '更新时间',
dataIndex: 'updatedAt',
valueType: 'dateTime',
hideInSearch: true,
sorter: true,
},
{
title: '创建时间',
dataIndex: 'createdAt',
valueType: 'dateTime',
hideInSearch: true,
sorter: true,
},
{
title: '操作',
dataIndex: 'option',
valueType: 'option',
fixed: 'right',
render: (_, record) => (
<>
<EditForm record={record} tableRef={actionRef} />
<Button
type="link"
onClick={() => {
setSyncProducts([record]);
setSyncModalVisible(true);
}}
>
</Button>
<Popconfirm
title="删除"
description="确认删除?"
onConfirm={async () => {
try {
const { success, message: errMsg } =
await productcontrollerDeleteproduct({ id: record.id });
if (!success) {
throw new Error(errMsg);
}
actionRef.current?.reload();
} catch (error: any) {
message.error(error.message);
}
}}
>
<Button type="link" danger>
</Button>
</Popconfirm>
</>
),
},
];
return (
<PageContainer header={{ title: '产品列表' }}>
<ProTable<API.Product>
scroll={{ x: 'max-content' }}
headerTitle="查询表格"
actionRef={actionRef}
rowKey="id"
toolBarRender={() => [
// 新建按钮
<CreateForm tableRef={actionRef} />,
// 导入 CSV(使用 customRequest 以支持 request 拦截器和鉴权)
<Upload
name="file"
accept=".csv"
showUploadList={false}
maxCount={1}
customRequest={async (options) => {
const { file, onSuccess, onError } = options;
const formData = new FormData();
formData.append('file', file);
try {
const res = await request('/product/import', {
method: 'POST',
data: formData,
requestType: 'form',
});
const {
created = 0,
updated = 0,
errors = [],
} = res.data || {};
if (errors && errors.length > 0) {
Modal.warning({
title: '导入结果 (存在错误)',
width: 600,
content: (
<div>
<p>: {created}</p>
<p>: {updated}</p>
<p style={{ color: 'red', fontWeight: 'bold' }}>
: {errors.length}
</p>
<div
style={{
maxHeight: '300px',
overflowY: 'auto',
background: '#f5f5f5',
padding: '8px',
marginTop: '8px',
borderRadius: '4px',
border: '1px solid #d9d9d9',
}}
>
{errors.map((err: string, idx: number) => (
<div
key={idx}
style={{
fontSize: '12px',
marginBottom: '4px',
borderBottom: '1px solid #e8e8e8',
paddingBottom: '2px',
color: '#ff4d4f',
}}
>
{idx + 1}. {err}
</div>
))}
</div>
</div>
),
});
} else {
message.success(`导入成功: 创建 ${created}, 更新 ${updated}`);
}
onSuccess?.('ok');
actionRef.current?.reload();
} catch (error: any) {
message.error('导入失败: ' + (error.message || '未知错误'));
onError?.(error);
}
}}
>
<Button></Button>
</Upload>,
// 批量编辑按钮
<Button
disabled={selectedRows.length <= 0}
onClick={() => setBatchEditModalVisible(true)}
>
</Button>,
// 批量同步按钮
<Button
disabled={selectedRows.length <= 0}
onClick={() => {
setSyncProducts(selectedRows);
setSyncModalVisible(true);
}}
>
</Button>,
// 批量删除按钮
<Button
danger
disabled={selectedRows.length <= 0}
onClick={() => {
Modal.confirm({
title: '确认删除',
content: `确定要删除选中的 ${selectedRows.length} 个产品吗?此操作不可恢复。`,
onOk: async () => {
try {
const { success, message: errMsg } =
await productcontrollerBatchdeleteproduct({
ids: selectedRows.map((row) => row.id),
});
if (success) {
message.success('批量删除成功');
setSelectedRows([]);
actionRef.current?.reload();
} else {
message.error(errMsg || '删除失败');
}
} catch (error: any) {
message.error(error.message || '删除失败');
}
},
});
}}
>
</Button>,
// 导出 CSV(后端返回 text/csv,直接新窗口下载)
<Button onClick={handleDownloadProductsCSV}>CSV</Button>,
]}
request={async (params, sort) => {
let sortField = undefined;
let sortOrder = undefined;
if (sort && Object.keys(sort).length > 0) {
const field = Object.keys(sort)[0];
sortField = field;
sortOrder = sort[field];
}
const { current, pageSize, ...where } = params;
console.log(`params`, params);
const { data, success } = await productcontrollerGetproductlist({
where,
page: current || 1,
per_page: pageSize || 10,
sortField,
sortOrder,
} as any);
return {
total: data?.total || 0,
data: data?.items || [],
success,
};
}}
columns={columns}
// expandable={{
// expandedRowRender: (record) => {
// return <ProductList filter={{
// skus: record.components?.map(component => component.sku) || [],
// }}
// columns={columns}
// ></ProductList>
// }
// ,
// rowExpandable: (record) =>
// !!(record.type==='bundle'),
// }}
editable={{
type: 'single',
onSave: async (key, record, originRow) => {
console.log('保存数据:', record);
},
}}
rowSelection={{
onChange: (_, selectedRows) => setSelectedRows(selectedRows),
}}
pagination={{
showSizeChanger: true,
showQuickJumper: true,
pageSizeOptions: ['10', '20', '50', '100', '1000', '2000'],
}}
/>
<BatchEditModal
visible={batchEditModalVisible}
onClose={() => setBatchEditModalVisible(false)}
selectedRows={selectedRows}
tableRef={actionRef}
onSuccess={() => {
setBatchEditModalVisible(false);
setSelectedRows([]);
}}
/>
<SyncToSiteModal
visible={syncModalVisible}
onClose={() => setSyncModalVisible(false)}
products={syncProducts}
onSuccess={() => {
setSyncModalVisible(false);
setSelectedRows([]);
actionRef.current?.reload();
}}
/>
</PageContainer>
);
};
export default List;