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; }> = ({ value, tableRef, id }) => { const { message } = App.useApp(); const [editable, setEditable] = React.useState(false); if (!editable) return
setEditable(true)}>{value || '-'}
; return ( ) => { 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 (
{(record.attributes || []).map((data: any, idx: number) => ( {data?.dict?.name}: {data.name} ))}
); }; const ComponentsCell: React.FC<{ components?: any[] }> = ({ components }) => { return (
{components && components.length ? ( components.map((component: any) => ( {component.sku || `#${component.id}`} × {component.quantity} (库存: {component.stock ?.map((s: any) => `${s.name}:${s.quantity}`) .join(', ') || '-'} ) )) ) : ( - )}
); }; const BatchEditModal: React.FC<{ visible: boolean; onClose: () => void; selectedRows: API.Product[]; tableRef: React.MutableRefObject; onSuccess: () => void; }> = ({ visible, onClose, selectedRows, tableRef, onSuccess }) => { const { message } = App.useApp(); const [categories, setCategories] = useState([]); useEffect(() => { if (visible) { productcontrollerGetcategoriesall().then((res: any) => { setCategories(res?.data || []); }); } }, [visible]); return ( !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; } }} > ({ label: c.title, value: c.id }))} placeholder="不修改请留空" /> ); }; const ProductList = ({ filter, columns, }: { filter: { skus: string[] }; columns: any[]; }) => { return ( { 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(); // 状态:存储当前选中的行 const [selectedRows, setSelectedRows] = React.useState([]); const [batchEditModalVisible, setBatchEditModalVisible] = useState(false); const [syncProducts, setSyncProducts] = useState([]); 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[] = [ { title: 'sku', dataIndex: 'sku', sorter: true, }, { title: '关联商品', dataIndex: 'siteSkus', width: 200, render: (_, record) => ( <> {record.siteSkus?.map((siteSku, index) => ( {siteSku} ))} ), }, { title: '图片', dataIndex: 'image', width: 100, valueType: 'image', }, { title: '名称', dataIndex: 'name', sorter: true, }, { title: '中文名', dataIndex: 'nameCn', render: (_, record) => { return ( ); }, }, { 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) => , }, { title: '产品类型', dataIndex: 'type', valueType: 'select', valueEnum: { single: { text: '单品' }, bundle: { text: '套装' }, }, render: (_, record) => { // 如果类型不存在,则返回- if (!record.type) return '-'; // 判断是否为单品 const isSingle = record.type === 'single'; // 根据类型显示不同颜色的标签 return ( {isSingle ? '单品' : '套装'} ); }, }, { title: '构成', dataIndex: 'components', hideInSearch: true, render: (_, record) => , }, { 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) => ( <> { 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); } }} > ), }, ]; return ( scroll={{ x: 'max-content' }} headerTitle="查询表格" actionRef={actionRef} rowKey="id" toolBarRender={() => [ // 新建按钮 , // 导入 CSV(使用 customRequest 以支持 request 拦截器和鉴权) { 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: (

创建成功: {created}

更新成功: {updated}

失败数量: {errors.length}

{errors.map((err: string, idx: number) => (
{idx + 1}. {err}
))}
), }); } else { message.success(`导入成功: 创建 ${created}, 更新 ${updated}`); } onSuccess?.('ok'); actionRef.current?.reload(); } catch (error: any) { message.error('导入失败: ' + (error.message || '未知错误')); onError?.(error); } }} >
, // 批量编辑按钮 , // 批量同步按钮 , // 批量删除按钮 , // 导出 CSV(后端返回 text/csv,直接新窗口下载) , ]} 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 component.sku) || [], // }} // columns={columns} // > // } // , // 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'], }} /> setBatchEditModalVisible(false)} selectedRows={selectedRows} tableRef={actionRef} onSuccess={() => { setBatchEditModalVisible(false); setSelectedRows([]); }} /> setSyncModalVisible(false)} products={syncProducts} onSuccess={() => { setSyncModalVisible(false); setSelectedRows([]); actionRef.current?.reload(); }} />
); }; export default List;