diff --git a/.umirc.ts b/.umirc.ts index be0e479..f2d21f1 100644 --- a/.umirc.ts +++ b/.umirc.ts @@ -16,6 +16,7 @@ export default defineConfig({ layout: { title: 'YOONE', }, + esbuildMinifyIIFE: true, define: { UMI_APP_API_URL, }, @@ -100,6 +101,18 @@ export default defineConfig({ path: '/site/shop/:siteId/customers', component: './Site/Shop/Customers', }, + { + path: '/site/shop/:siteId/reviews', + component: './Site/Shop/Reviews', + }, + { + path: '/site/shop/:siteId/webhooks', + component: './Site/Shop/Webhooks', + }, + { + path: '/site/shop/:siteId/links', + component: './Site/Shop/Links', + }, ], }, { @@ -119,6 +132,11 @@ export default defineConfig({ path: '/customer/list', component: './Customer/List', }, + { + name: '数据分析列表', + path: '/customer/statistic', + component: './Customer/Statistic', + }, ], }, { diff --git a/package.json b/package.json index 58e91ac..7ceb5ad 100644 --- a/package.json +++ b/package.json @@ -47,4 +47,4 @@ "prettier-plugin-packagejson": "^2.4.3", "typescript": "^5.7.3" } -} \ No newline at end of file +} diff --git a/src/components/Address.tsx b/src/components/Address.tsx new file mode 100644 index 0000000..111619b --- /dev/null +++ b/src/components/Address.tsx @@ -0,0 +1,38 @@ +import React from 'react'; + +interface AddressProps { + address: { + address_1?: string; + address_2?: string; + city?: string; + state?: string; + postcode?: string; + country?: string; + phone?: string; + }; + style?: React.CSSProperties; +} + +const Address: React.FC = ({ address, style }) => { + if (!address) { + return -; + } + + const { address_1, address_2, city, state, postcode, country, phone } = + address; + + return ( +
+
+ {address_1} {address_2} +
+
+ {city}, {state}, {postcode} +
+
{country}
+
{phone}
+
+ ); +}; + +export default Address; diff --git a/src/pages/Category/index.tsx b/src/pages/Category/index.tsx index 74037ee..3b0cc79 100644 --- a/src/pages/Category/index.tsx +++ b/src/pages/Category/index.tsx @@ -23,7 +23,7 @@ import { message, } from 'antd'; import React, { useEffect, useState } from 'react'; -import { attributes } from '../Attribute/consts'; +import { notAttributes } from '../Product/Attribute/consts'; const { Sider, Content } = Layout; @@ -114,7 +114,9 @@ const CategoryPage: React.FC = () => { // Fetch all dicts and filter those that are allowed attributes try { const res = await request('/dict/list'); - const filtered = (res || []).filter((d: any) => attributes.has(d.name)); + const filtered = (res || []).filter( + (d: any) => !notAttributes.has(d.name), + ); // Filter out already added attributes const existingDictIds = new Set( categoryAttributes.map((ca: any) => ca.dict.id), diff --git a/src/pages/Customer/List/HistoryOrders.tsx b/src/pages/Customer/List/HistoryOrders.tsx new file mode 100644 index 0000000..006e0e1 --- /dev/null +++ b/src/pages/Customer/List/HistoryOrders.tsx @@ -0,0 +1,255 @@ +import { ordercontrollerGetorders } from '@/servers/api/order'; +import { siteapicontrollerGetorders } from '@/servers/api/siteApi'; +import { + App, + Col, + Modal, + Row, + Spin, + Statistic, + Table, + Tag, + Typography, +} from 'antd'; +import dayjs from 'dayjs'; +import { useState } from 'react'; + +const { Text, Title } = Typography; + +interface HistoryOrdersProps { + customer: API.UnifiedCustomerDTO; + siteId?: number; +} + +interface OrderStats { + totalOrders: number; + totalAmount: number; + yooneOrders: number; + yooneAmount: number; +} + +const HistoryOrders: React.FC = ({ customer, siteId }) => { + const { message } = App.useApp(); + const [modalVisible, setModalVisible] = useState(false); + const [orders, setOrders] = useState([]); + const [loading, setLoading] = useState(false); + const [stats, setStats] = useState({ + totalOrders: 0, + totalAmount: 0, + yooneOrders: 0, + yooneAmount: 0, + }); + + // 计算订单统计信息 + const calculateStats = (orders: any[]) => { + let totalOrders = 0; + let totalAmount = 0; + let yooneOrders = 0; + let yooneAmount = 0; + + orders.forEach((order) => { + totalOrders++; + // total是字符串,需要转换为数字 + const orderTotal = parseFloat(order.total || '0'); + totalAmount += orderTotal; + + // 检查订单中是否包含yoone商品 + let hasYoone = false; + let orderYooneAmount = 0; + + // 优先使用line_items,如果没有则使用items + const items = order.line_items || order.items || []; + if (Array.isArray(items)) { + items.forEach((item: any) => { + // 检查商品名称或SKU是否包含yoone(不区分大小写) + const itemName = (item.name || '').toLowerCase(); + const sku = (item.sku || '').toLowerCase(); + + if (itemName.includes('yoone') || sku.includes('yoone')) { + hasYoone = true; + const itemTotal = parseFloat(item.total || item.price || '0'); + orderYooneAmount += itemTotal; + } + }); + } + + if (hasYoone) { + yooneOrders++; + yooneAmount += orderYooneAmount; + } + }); + + return { + totalOrders, + totalAmount, + yooneOrders, + yooneAmount, + }; + }; + + // 获取客户订单数据 + const fetchOrders = async () => { + + + setLoading(true); + try { + const response = await ordercontrollerGetorders({ + customer_email: customer.email, + }); + + if (response) { + const orderList = response.items || []; + setOrders(orderList); + const calculatedStats = calculateStats(orderList); + setStats(calculatedStats); + } else { + message.error('获取订单数据失败'); + } + } catch (error) { + console.error('获取订单失败:', error); + message.error('获取订单失败'); + } finally { + setLoading(false); + } + }; + + // 打开弹框时获取数据 + const handleOpenModal = () => { + setModalVisible(true); + fetchOrders(); + }; + + // 订单表格列配置 + const orderColumns = [ + { + title: '订单号', + dataIndex: 'externalOrderId', + key: 'externalOrderId', + width: 120, + }, + { + title: '订单状态', + dataIndex: 'status', + key: 'status', + width: 100, + render: (status: string) => { + const statusMap: Record = { + pending: '待处理', + processing: '处理中', + 'on-hold': '等待中', + completed: '已完成', + cancelled: '已取消', + refunded: '已退款', + failed: '失败', + }; + return {statusMap[status] || status}; + }, + }, + { + title: '订单金额', + dataIndex: 'total', + key: 'total', + width: 100, + render: (total: string, record: any) => ( + + {record.currency_symbol || '$'} + {parseFloat(total || '0').toFixed(2)} + + ), + }, + { + title: '创建时间', + dataIndex: 'date_created', + key: 'date_created', + width: 140, + render: (date: string) => ( + {date ? dayjs(date).format('YYYY-MM-DD HH:mm') : '-'} + ), + }, + { + title: '包含Yoone', + key: 'hasYoone', + width: 80, + render: (_: any, record: any) => { + let hasYoone = false; + const items = record.line_items || record.items || []; + if (Array.isArray(items)) { + hasYoone = items.some((item: any) => { + const itemName = (item.name || '').toLowerCase(); + const sku = (item.sku || '').toLowerCase(); + return itemName.includes('yoone') || sku.includes('yoone'); + }); + } + return hasYoone ? : ; + }, + }, + ]; + + return ( + <> + 历史订单 + + setModalVisible(false)} + footer={null} + width={1000} + > + + {/* 统计信息 */} + + + + + + + + + + + + + + + + {/* 订单列表 */} + + 订单详情 + + `共 ${total} 条`, + }} + scroll={{ x: 800 }} + /> + + + + ); +}; + +export default HistoryOrders; diff --git a/src/pages/Customer/List/index.tsx b/src/pages/Customer/List/index.tsx index 7bd1877..2280537 100644 --- a/src/pages/Customer/List/index.tsx +++ b/src/pages/Customer/List/index.tsx @@ -1,11 +1,12 @@ -import { HistoryOrder } from '@/pages/Statistics/Order'; import { customercontrollerAddtag, customercontrollerDeltag, customercontrollerGetcustomerlist, customercontrollerGettags, customercontrollerSetrate, + customercontrollerSynccustomers, } from '@/servers/api/customer'; +import { sitecontrollerAll } from '@/servers/api/site'; import { ActionType, ModalForm, @@ -14,97 +15,227 @@ import { ProFormSelect, ProTable, } from '@ant-design/pro-components'; -import { App, Button, Rate, Space, Tag } from 'antd'; -import dayjs from 'dayjs'; -import { useRef, useState } from 'react'; +import { App, Avatar, Button, Rate, Space, Tag, Tooltip } from 'antd'; +import { useEffect, useRef, useState } from 'react'; +import HistoryOrders from './HistoryOrders'; -const ListPage: React.FC = () => { +// 地址格式化函数 +const formatAddress = (address: any) => { + if (!address) return '-'; + + if (typeof address === 'string') { + try { + address = JSON.parse(address); + } catch (e) { + return address; + } + } + + const { + first_name, + last_name, + company, + address_1, + address_2, + city, + state, + postcode, + country, + phone: addressPhone, + email: addressEmail, + } = address; + + const parts = []; + + // 姓名 + const fullName = [first_name, last_name].filter(Boolean).join(' '); + if (fullName) parts.push(fullName); + + // 公司 + if (company) parts.push(company); + + // 地址行 + if (address_1) parts.push(address_1); + if (address_2) parts.push(address_2); + + // 城市、州、邮编 + const locationParts = [city, state, postcode].filter(Boolean).join(', '); + if (locationParts) parts.push(locationParts); + + // 国家 + if (country) parts.push(country); + + // 联系方式 + if (addressPhone) parts.push(`电话: ${addressPhone}`); + if (addressEmail) parts.push(`邮箱: ${addressEmail}`); + + return parts.join(', '); +}; + +// 地址卡片组件 +const AddressCell: React.FC<{ address: any; title: string }> = ({ + address, + title, +}) => { + const formattedAddress = formatAddress(address); + + if (formattedAddress === '-') { + return -; + } + + return ( + + {title}: +
+ {formattedAddress} + + } + placement="topLeft" + > +
+ {formattedAddress} +
+
+ ); +}; + +const CustomerList: React.FC = () => { const actionRef = useRef(); const { message } = App.useApp(); - const columns: ProColumns[] = [ + const [syncModalVisible, setSyncModalVisible] = useState(false); + const [syncLoading, setSyncLoading] = useState(false); + const [sites, setSites] = useState([]); // 添加站点数据状态 + + // 获取站点数据 + const fetchSites = async () => { + try { + const { data, success } = await sitecontrollerAll(); + if (success) { + setSites(data || []); + } + } catch (error) { + console.error('获取站点数据失败:', error); + } + }; + + // 根据站点ID获取站点名称 + const getSiteName = (siteId: number | undefined | null) => { + if (!siteId) return '-'; + if (typeof siteId === 'string') { + return siteId; + } + const site = sites.find((s) => s.id === siteId); + console.log(`site`, site); + return site ? site.name : String(siteId); + }; + + // 组件加载时获取站点数据 + useEffect(() => { + fetchSites(); + }, []); + + const columns: ProColumns[] = [ + { + title: 'ID', + dataIndex: 'id', + hideInSearch: true, + }, + { + title: '站点', + dataIndex: 'site_id', + valueType: 'select', + request: async () => { + try { + const { data, success } = await sitecontrollerAll(); + if (success && data) { + return data.map((site: any) => ({ + label: site.name, + value: site.id, + })); + } + return []; + } catch (error) { + console.error('获取站点列表失败:', error); + return []; + } + }, + render: (siteId: any) => { + return {getSiteName(siteId) || '-'}; + }, + }, + { + title: '头像', + dataIndex: 'avatar', + hideInSearch: true, + width: 60, + render: (_, record) => ( + + {!record.avatar && record.fullname?.charAt(0)?.toUpperCase()} + + ), + }, + { + title: '姓名', + dataIndex: 'fullname', + sorter: true, + render: (_, record) => { + return ( + record.fullname || + `${record.first_name || ''} ${record.last_name || ''}`.trim() || + record.username || + '-' + ); + }, + }, { title: '用户名', dataIndex: 'username', hideInSearch: true, - render: (_, record) => { - if (record.billing.first_name || record.billing.last_name) - return record.billing.first_name + ' ' + record.billing.last_name; - return record.shipping.first_name + ' ' + record.shipping.last_name; - }, }, { title: '邮箱', dataIndex: 'email', + copyable: true, }, { - title: '客户编号', - dataIndex: 'customerId', - render: (_, record) => { - if (!record.customerId) return '-'; - return String(record.customerId).padStart(6, 0); - }, - sorter: true, - }, - { - title: '首单时间', - dataIndex: 'first_purchase_date', - valueType: 'dateMonth', - sorter: true, - render: (_, record) => - record.first_purchase_date - ? dayjs(record.first_purchase_date).format('YYYY-MM-DD HH:mm:ss') - : '-', - // search: { - // transform: (value: string) => { - // return { month: value }; - // }, - // }, - }, - { - title: '尾单时间', + title: '电话', + dataIndex: 'phone', hideInSearch: true, - dataIndex: 'last_purchase_date', - valueType: 'dateTime', - sorter: true, + copyable: true, }, { - title: '订单数', - dataIndex: 'orders', + title: '账单地址', + dataIndex: 'billing', hideInSearch: true, - sorter: true, - }, - { - title: '金额', - dataIndex: 'total', - hideInSearch: true, - sorter: true, - }, - { - title: 'YOONE订单数', - dataIndex: 'yoone_orders', - hideInSearch: true, - sorter: true, - }, - { - title: 'YOONE金额', - dataIndex: 'yoone_total', - hideInSearch: true, - sorter: true, - }, - { - title: '等级', - hideInSearch: true, - render: (_, record) => { - if (!record.yoone_orders || !record.yoone_total) return '-'; - if (Number(record.yoone_orders) === 1 && Number(record.yoone_total) > 0) - return 'B'; - return '-'; - }, - }, - { - title: '评星', - dataIndex: 'rate', width: 200, + render: (billing) => , + }, + { + title: '物流地址', + dataIndex: 'shipping', + hideInSearch: true, + width: 200, + render: (shipping) => , + }, + { + title: '评分', + dataIndex: 'rate', + width: 120, + hideInSearch: true, render: (_, record) => { return ( { try { const { success, message: msg } = await customercontrollerSetrate({ - id: record.customerId, + id: record.id, rate: val, }); if (success) { message.success(msg); actionRef.current?.reload(); } - } catch (e) { - message.error(e.message); + } catch (e: any) { + message.error(e?.message || '设置评分失败'); } }} - value={record.rate} + value={record.raw?.rate || 0} + allowHalf /> ); }, }, - { - title: 'phone', - dataIndex: 'phone', - hideInSearch: true, - render: (_, record) => record?.billing.phone || record?.shipping.phone, - }, - { - title: 'state', - dataIndex: 'state', - render: (_, record) => record?.billing.state || record?.shipping.state, - }, - { - title: 'city', - dataIndex: 'city', - hideInSearch: true, - render: (_, record) => record?.billing.city || record?.shipping.city, - }, { title: '标签', dataIndex: 'tags', + hideInSearch: true, render: (_, record) => { + const tags = record.raw?.tags || []; return ( - - {(record.tags || []).map((tag) => { + + {tags.map((tag: string) => { return ( { email: record.email, tag, }); - return false; + if (!success) { + message.error(msg); + return false; + } + actionRef.current?.reload(); + return true; }} + style={{ marginBottom: 4 }} > {tag} @@ -173,31 +296,46 @@ const ListPage: React.FC = () => { ); }, }, + { + title: '创建时间', + dataIndex: 'site_created_at', + valueType: 'dateTime', + hideInSearch: true, + sorter: true, + width: 140, + }, + { + title: '更新时间', + dataIndex: 'site_created_at', + valueType: 'dateTime', + hideInSearch: true, + sorter: true, + width: 140, + }, { title: '操作', dataIndex: 'option', valueType: 'option', fixed: 'right', + width: 120, render: (_, record) => { return ( - + - + {/* 订单 */} + ); }, }, ]; + return ( - + { const key = Object.keys(sorter)[0]; const { data, success } = await customercontrollerGetcustomerlist({ ...params, - ...(key ? { sorterKey: key, sorterValue: sorter[key] } : {}), + current: params.current?.toString(), + pageSize: params.pageSize?.toString(), + ...(key + ? { sorterKey: key, sorterValue: sorter[key] as string } + : {}), }); return { @@ -217,12 +359,37 @@ const ListPage: React.FC = () => { }; }} columns={columns} + search={{ + labelWidth: 'auto', + span: 6, + }} + pagination={{ + pageSize: 20, + showSizeChanger: true, + showTotal: (total, range) => + `第 ${range[0]}-${range[1]} 条/总共 ${total} 条`, + }} + toolBarRender={() => [ + , + // 这里可以添加导出、导入等功能按钮 + ]} + /> + setSyncModalVisible(false)} + tableRef={actionRef} /> ); }; -export const AddTag: React.FC<{ +const AddTag: React.FC<{ email: string; tags?: string[]; tableRef: React.MutableRefObject; @@ -233,7 +400,11 @@ export const AddTag: React.FC<{ return ( 修改标签} + trigger={ + + } width={800} modalProps={{ destroyOnHidden: true, @@ -250,16 +421,16 @@ export const AddTag: React.FC<{ if (!success) return []; setTagList(tags || []); return data - .filter((tag) => { + .filter((tag: string) => { return !(tags || []).includes(tag); }) - .map((tag) => ({ label: tag, value: tag })); + .map((tag: string) => ({ label: tag, value: tag })); }} fieldProps={{ value: tagList, // 当前值 onChange: async (newValue) => { - const added = newValue.filter((x) => !tagList.includes(x)); - const removed = tagList.filter((x) => !newValue.includes(x)); + const added = newValue.filter((x) => !(tags || []).includes(x)); + const removed = (tags || []).filter((x) => !newValue.includes(x)); if (added.length) { const { success, message: msg } = await customercontrollerAddtag({ @@ -282,7 +453,6 @@ export const AddTag: React.FC<{ } } tableRef?.current?.reload(); - setTagList(newValue); }, }} @@ -291,4 +461,132 @@ export const AddTag: React.FC<{ ); }; -export default ListPage; +const SyncCustomersModal: React.FC<{ + visible: boolean; + onClose: () => void; + tableRef: React.MutableRefObject; +}> = ({ visible, onClose, tableRef }) => { + const { message } = App.useApp(); + const [sites, setSites] = useState([]); + const [loading, setLoading] = useState(false); + + // 获取站点列表 + useEffect(() => { + if (visible) { + setLoading(true); + sitecontrollerAll() + .then((res: any) => { + setSites(res?.data || []); + }) + .catch((error: any) => { + message.error('获取站点列表失败: ' + (error.message || '未知错误')); + }) + .finally(() => { + setLoading(false); + }); + } + }, [visible]); + + const handleSync = async (values: { siteId: number }) => { + try { + setLoading(true); + const { + success, + message: msg, + data, + } = await customercontrollerSynccustomers({ + siteId: values.siteId, + }); + + if (success) { + // 显示详细的同步结果 + const result = data || {}; + const { + total = 0, + synced = 0, + created = 0, + updated = 0, + errors = [], + } = result; + + let resultMessage = `同步完成!共处理 ${total} 个客户:`; + if (created > 0) resultMessage += ` 新建 ${created} 个`; + if (updated > 0) resultMessage += ` 更新 ${updated} 个`; + if (synced > 0) resultMessage += ` 同步成功 ${synced} 个`; + if (errors.length > 0) resultMessage += ` 失败 ${errors.length} 个`; + + if (errors.length > 0) { + // 如果有错误,显示警告消息 + message.warning({ + content: ( +
+
{resultMessage}
+
+ 失败详情: + {errors + .slice(0, 3) + .map((err: any) => err.email || err.error) + .join(', ')} + {errors.length > 3 && ` 等 ${errors.length - 3} 个错误...`} +
+
+ ), + duration: 8, + key: 'sync-result', + }); + } else { + // 完全成功 + message.success({ + content: resultMessage, + duration: 4, + key: 'sync-result', + }); + } + + onClose(); + // 刷新表格数据 + tableRef.current?.reload(); + return true; + } else { + message.error(msg || '同步失败'); + return false; + } + } catch (error: any) { + message.error('同步失败: ' + (error.message || '未知错误')); + return false; + } finally { + setLoading(false); + } + }; + + return ( + !open && onClose()} + modalProps={{ + destroyOnClose: true, + confirmLoading: loading, + }} + onFinish={handleSync} + > + ({ + label: site.name, + value: site.id, + }))} + rules={[{ required: true, message: '请选择站点' }]} + fieldProps={{ + loading: loading, + }} + /> + + ); +}; + +export { AddTag }; + +export default CustomerList; diff --git a/src/pages/Customer/Statistic/index.tsx b/src/pages/Customer/Statistic/index.tsx new file mode 100644 index 0000000..7bd1877 --- /dev/null +++ b/src/pages/Customer/Statistic/index.tsx @@ -0,0 +1,294 @@ +import { HistoryOrder } from '@/pages/Statistics/Order'; +import { + customercontrollerAddtag, + customercontrollerDeltag, + customercontrollerGetcustomerlist, + customercontrollerGettags, + customercontrollerSetrate, +} from '@/servers/api/customer'; +import { + ActionType, + ModalForm, + PageContainer, + ProColumns, + ProFormSelect, + ProTable, +} from '@ant-design/pro-components'; +import { App, Button, Rate, Space, Tag } from 'antd'; +import dayjs from 'dayjs'; +import { useRef, useState } from 'react'; + +const ListPage: React.FC = () => { + const actionRef = useRef(); + const { message } = App.useApp(); + const columns: ProColumns[] = [ + { + title: '用户名', + dataIndex: 'username', + hideInSearch: true, + render: (_, record) => { + if (record.billing.first_name || record.billing.last_name) + return record.billing.first_name + ' ' + record.billing.last_name; + return record.shipping.first_name + ' ' + record.shipping.last_name; + }, + }, + { + title: '邮箱', + dataIndex: 'email', + }, + { + title: '客户编号', + dataIndex: 'customerId', + render: (_, record) => { + if (!record.customerId) return '-'; + return String(record.customerId).padStart(6, 0); + }, + sorter: true, + }, + { + title: '首单时间', + dataIndex: 'first_purchase_date', + valueType: 'dateMonth', + sorter: true, + render: (_, record) => + record.first_purchase_date + ? dayjs(record.first_purchase_date).format('YYYY-MM-DD HH:mm:ss') + : '-', + // search: { + // transform: (value: string) => { + // return { month: value }; + // }, + // }, + }, + { + title: '尾单时间', + hideInSearch: true, + dataIndex: 'last_purchase_date', + valueType: 'dateTime', + sorter: true, + }, + { + title: '订单数', + dataIndex: 'orders', + hideInSearch: true, + sorter: true, + }, + { + title: '金额', + dataIndex: 'total', + hideInSearch: true, + sorter: true, + }, + { + title: 'YOONE订单数', + dataIndex: 'yoone_orders', + hideInSearch: true, + sorter: true, + }, + { + title: 'YOONE金额', + dataIndex: 'yoone_total', + hideInSearch: true, + sorter: true, + }, + { + title: '等级', + hideInSearch: true, + render: (_, record) => { + if (!record.yoone_orders || !record.yoone_total) return '-'; + if (Number(record.yoone_orders) === 1 && Number(record.yoone_total) > 0) + return 'B'; + return '-'; + }, + }, + { + title: '评星', + dataIndex: 'rate', + width: 200, + render: (_, record) => { + return ( + { + try { + const { success, message: msg } = + await customercontrollerSetrate({ + id: record.customerId, + rate: val, + }); + if (success) { + message.success(msg); + actionRef.current?.reload(); + } + } catch (e) { + message.error(e.message); + } + }} + value={record.rate} + /> + ); + }, + }, + { + title: 'phone', + dataIndex: 'phone', + hideInSearch: true, + render: (_, record) => record?.billing.phone || record?.shipping.phone, + }, + { + title: 'state', + dataIndex: 'state', + render: (_, record) => record?.billing.state || record?.shipping.state, + }, + { + title: 'city', + dataIndex: 'city', + hideInSearch: true, + render: (_, record) => record?.billing.city || record?.shipping.city, + }, + { + title: '标签', + dataIndex: 'tags', + render: (_, record) => { + return ( + + {(record.tags || []).map((tag) => { + return ( + { + const { success, message: msg } = + await customercontrollerDeltag({ + email: record.email, + tag, + }); + return false; + }} + > + {tag} + + ); + })} + + ); + }, + }, + { + title: '操作', + dataIndex: 'option', + valueType: 'option', + fixed: 'right', + render: (_, record) => { + return ( + + + + + ); + }, + }, + ]; + return ( + + { + const key = Object.keys(sorter)[0]; + const { data, success } = await customercontrollerGetcustomerlist({ + ...params, + ...(key ? { sorterKey: key, sorterValue: sorter[key] } : {}), + }); + + return { + total: data?.total || 0, + data: data?.items || [], + success, + }; + }} + columns={columns} + /> + + ); +}; + +export const AddTag: React.FC<{ + email: string; + tags?: string[]; + tableRef: React.MutableRefObject; +}> = ({ email, tags, tableRef }) => { + const { message } = App.useApp(); + const [tagList, setTagList] = useState([]); + + return ( + 修改标签} + width={800} + modalProps={{ + destroyOnHidden: true, + }} + submitter={false} + > + { + const { data, success } = await customercontrollerGettags(); + if (!success) return []; + setTagList(tags || []); + return data + .filter((tag) => { + return !(tags || []).includes(tag); + }) + .map((tag) => ({ label: tag, value: tag })); + }} + fieldProps={{ + value: tagList, // 当前值 + onChange: async (newValue) => { + const added = newValue.filter((x) => !tagList.includes(x)); + const removed = tagList.filter((x) => !newValue.includes(x)); + + if (added.length) { + const { success, message: msg } = await customercontrollerAddtag({ + email, + tag: added[0], + }); + if (!success) { + message.error(msg); + return; + } + } + if (removed.length) { + const { success, message: msg } = await customercontrollerDeltag({ + email, + tag: removed[0], + }); + if (!success) { + message.error(msg); + return; + } + } + tableRef?.current?.reload(); + + setTagList(newValue); + }, + }} + > + + ); +}; + +export default ListPage; diff --git a/src/pages/Dict/List/index.tsx b/src/pages/Dict/List/index.tsx index 6e27a21..54e96f9 100644 --- a/src/pages/Dict/List/index.tsx +++ b/src/pages/Dict/List/index.tsx @@ -1,10 +1,10 @@ +import * as dictApi from '@/servers/api/dict'; import { UploadOutlined } from '@ant-design/icons'; import { ActionType, PageContainer, ProTable, } from '@ant-design/pro-components'; -import { request } from '@umijs/max'; import { Button, Form, @@ -26,14 +26,22 @@ const DictPage: React.FC = () => { const [loadingDicts, setLoadingDicts] = useState(false); const [searchText, setSearchText] = useState(''); const [selectedDict, setSelectedDict] = useState(null); + + // 添加字典 modal 状态 const [isAddDictModalVisible, setIsAddDictModalVisible] = useState(false); - const [editingDict, setEditingDict] = useState(null); - const [newDictName, setNewDictName] = useState(''); - const [newDictTitle, setNewDictTitle] = useState(''); + const [addDictName, setAddDictName] = useState(''); + const [addDictTitle, setAddDictTitle] = useState(''); + + // 编辑字典 modal 状态 + const [isEditDictModalVisible, setIsEditDictModalVisible] = useState(false); + const [editDictData, setEditDictData] = useState(null); // 右侧字典项列表的状态 - const [isDictItemModalVisible, setIsDictItemModalVisible] = useState(false); - const [editingDictItem, setEditingDictItem] = useState(null); + const [isAddDictItemModalVisible, setIsAddDictItemModalVisible] = + useState(false); + const [isEditDictItemModalVisible, setIsEditDictItemModalVisible] = + useState(false); + const [editDictItemData, setEditDictItemData] = useState(null); const [dictItemForm] = Form.useForm(); const actionRef = useRef(); @@ -41,7 +49,7 @@ const DictPage: React.FC = () => { const fetchDicts = async (name = '') => { setLoadingDicts(true); try { - const res = await request('/dict/list', { params: { name } }); + const res = await dictApi.dictcontrollerGetdicts({ name }); setDicts(res); } catch (error) { message.error('获取字典列表失败'); @@ -55,60 +63,66 @@ const DictPage: React.FC = () => { fetchDicts(value); }; - // 添加或编辑字典 + // 添加字典 const handleAddDict = async () => { - const values = { name: newDictName, title: newDictTitle }; + const values = { name: addDictName, title: addDictTitle }; try { - if (editingDict) { - await request(`/dict/${editingDict.id}`, { - method: 'PUT', - data: values, - }); - message.success('更新成功'); - } else { - await request('/dict', { method: 'POST', data: values }); - message.success('添加成功'); - } + await dictApi.dictcontrollerCreatedict(values); + message.success('添加成功'); setIsAddDictModalVisible(false); - setEditingDict(null); - setNewDictName(''); - setNewDictTitle(''); + setAddDictName(''); + setAddDictTitle(''); fetchDicts(); // 重新获取列表 } catch (error) { - message.error(editingDict ? '更新失败' : '添加失败'); + message.error('添加失败'); + } + }; + + // 编辑字典 + const handleEditDict = async () => { + if (!editDictData) return; + const values = { name: editDictData.name, title: editDictData.title }; + try { + await dictApi.dictcontrollerUpdatedict({ id: editDictData.id }, values); + message.success('更新成功'); + setIsEditDictModalVisible(false); + setEditDictData(null); + fetchDicts(); // 重新获取列表 + } catch (error) { + message.error('更新失败'); } }; // 删除字典 const handleDeleteDict = async (id: number) => { try { - await request(`/dict/${id}`, { method: 'DELETE' }); + const result = await dictApi.dictcontrollerDeletedict({ id }); + if (!result.success) { + throw new Error(result.message || '删除失败'); + } message.success('删除成功'); fetchDicts(); if (selectedDict?.id === id) { setSelectedDict(null); } - } catch (error) { - message.error('删除失败'); + } catch (error: any) { + message.error(`删除失败,原因为:${error.message}`); } }; - // 编辑字典 - const handleEditDict = (record: any) => { - setEditingDict(record); - setNewDictName(record.name); - setNewDictTitle(record.title); - setIsAddDictModalVisible(true); + // 打开编辑字典 modal + const openEditDictModal = (record: any) => { + setEditDictData(record); + setIsEditDictModalVisible(true); }; // 下载字典导入模板 const handleDownloadDictTemplate = async () => { try { - // 使用 request 函数获取带认证的文件数据 - const response = await request('/dict/template', { - method: 'GET', - responseType: 'blob', // 指定响应类型为 blob - skipErrorHandler: true, // 跳过默认错误处理,自己处理错误 + // 使用 dictApi.dictcontrollerDownloaddicttemplate 获取字典模板 + const response = await dictApi.dictcontrollerDownloaddicttemplate({ + responseType: 'blob', + skipErrorHandler: true, }); // 创建 blob 对象和下载链接 @@ -130,46 +144,82 @@ const DictPage: React.FC = () => { // 添加字典项 const handleAddDictItem = () => { - setEditingDictItem(null); dictItemForm.resetFields(); - setIsDictItemModalVisible(true); + setIsAddDictItemModalVisible(true); }; // 编辑字典项 const handleEditDictItem = (record: any) => { - setEditingDictItem(record); + setEditDictItemData(record); dictItemForm.setFieldsValue(record); - setIsDictItemModalVisible(true); + setIsEditDictItemModalVisible(true); }; // 删除字典项 const handleDeleteDictItem = async (id: number) => { try { - await request(`/dict/item/${id}`, { method: 'DELETE' }); + const result = await dictApi.dictcontrollerDeletedictitem({ id }); + if (!result.success) { + throw new Error(result.message || '删除失败'); + } message.success('删除成功'); - actionRef.current?.reload(); - } catch (error) { - message.error('删除失败'); + + // 强制刷新字典项列表 + setTimeout(() => { + actionRef.current?.reload(); + }, 100); + } catch (error: any) { + message.error(`删除失败,原因为:${error.message}`); } }; - // 字典项表单提交 - const handleDictItemFormSubmit = async (values: any) => { - const url = editingDictItem - ? `/dict/item/${editingDictItem.id}` - : '/dict/item'; - const method = editingDictItem ? 'PUT' : 'POST'; - const data = editingDictItem - ? { ...values } - : { ...values, dict: { id: selectedDict.id } }; - + // 添加字典项表单提交 + const handleAddDictItemFormSubmit = async (values: any) => { try { - await request(url, { method, data }); - message.success(editingDictItem ? '更新成功' : '添加成功'); - setIsDictItemModalVisible(false); - actionRef.current?.reload(); - } catch (error) { - message.error(editingDictItem ? '更新失败' : '添加失败'); + const result = await dictApi.dictcontrollerCreatedictitem({ + ...values, + dictId: selectedDict.id, + }); + + if (!result.success) { + throw new Error(result.message || '添加失败'); + } + + message.success('添加成功'); + setIsAddDictItemModalVisible(false); + + // 强制刷新字典项列表 + setTimeout(() => { + actionRef.current?.reload(); + }, 100); + } catch (error: any) { + message.error(`添加失败:${error.message || '未知错误'}`); + } + }; + + // 编辑字典项表单提交 + const handleEditDictItemFormSubmit = async (values: any) => { + if (!editDictItemData) return; + try { + const result = await dictApi.dictcontrollerUpdatedictitem( + { id: editDictItemData.id }, + values, + ); + + if (!result.success) { + throw new Error(result.message || '更新失败'); + } + + message.success('更新成功'); + setIsEditDictItemModalVisible(false); + setEditDictItemData(null); + + // 强制刷新字典项列表 + setTimeout(() => { + actionRef.current?.reload(); + }, 100); + } catch (error: any) { + message.error(`更新失败:${error.message || '未知错误'}`); } }; @@ -182,9 +232,8 @@ const DictPage: React.FC = () => { try { // 获取当前字典的所有数据 - const response = await request('/dict/items', { - method: 'GET', - params: { dictId: selectedDict.id }, + const response = await dictApi.dictcontrollerGetdictitems({ + dictId: selectedDict.id, }); if (!response || response.length === 0) { @@ -257,7 +306,7 @@ const DictPage: React.FC = () => { @@ -331,7 +380,7 @@ const DictPage: React.FC = () => { { enterButton allowClear /> - + { if (info.file.status === 'done') { @@ -401,53 +450,6 @@ const DictPage: React.FC = () => { -
- - { - if (info.file.status === 'done') { - message.success(`${info.file.name} 文件上传成功`); - actionRef.current?.reload(); - } else if (info.file.status === 'error') { - message.error(`${info.file.name} 文件上传失败`); - } - }} - > - - - -
{ @@ -459,15 +461,24 @@ const DictPage: React.FC = () => { }; } const { name, title } = params; - const res = await request('/dict/items', { - params: { - dictId: selectedDict?.id, - name, - title, - }, + const res = await dictApi.dictcontrollerGetdictitems({ + dictId: selectedDict?.id, + name, + title, }); + + // 适配新的响应格式,检查是否有 successResponse 包裹 + if (res && res.success !== undefined) { + return { + data: res.data || [], + success: res.success, + total: res.data?.length || 0, + }; + } + + // 兼容旧的响应格式(直接返回数组) return { - data: res, + data: res || [], success: true, }; }} @@ -476,25 +487,86 @@ const DictPage: React.FC = () => { layout: 'vertical', }} pagination={false} - options={false} + options={{ + reload: true, + density: true, + setting: true, + }} size="small" key={selectedDict?.id} + toolBarRender={() => [ + , + { + const { file, onSuccess, onError } = options; + try { + const result = + await dictApi.dictcontrollerImportdictitems( + { dictId: selectedDict?.id }, + [file as File], + ); + onSuccess?.(result); + } catch (error) { + onError?.(error as Error); + } + }} + showUploadList={false} + disabled={!selectedDict} + onChange={(info) => { + console.log(`info`, info); + if (info.file.status === 'done') { + message.success(`${info.file.name} 文件上传成功`); + // 重新加载字典项列表 + setTimeout(() => { + actionRef.current?.reload(); + }, 100); + // 重新加载字典列表以更新字典项数量 + fetchDicts(); + } else if (info.file.status === 'error') { + message.error(`${info.file.name} 文件上传失败`); + } + }} + key="import" + > + + , + , + ]} />
+ {/* 添加字典项 Modal */} dictItemForm.submit()} - onCancel={() => setIsDictItemModalVisible(false)} - destroyOnClose + onCancel={() => setIsAddDictItemModalVisible(false)} >
{
+ {/* 编辑字典项 Modal */} dictItemForm.submit()} + onCancel={() => { + setIsEditDictItemModalVisible(false); + setEditDictItemData(null); + }} + > +
+ + + + + + + + + + + + + + + + + + + +
+ + {/* 添加字典 Modal */} + setIsAddDictModalVisible(false)} + onCancel={() => { + setIsAddDictModalVisible(false); + setAddDictName(''); + setAddDictTitle(''); + }} >
setNewDictName(e.target.value)} + value={addDictName} + onChange={(e) => setAddDictName(e.target.value)} /> setNewDictTitle(e.target.value)} + value={addDictTitle} + onChange={(e) => setAddDictTitle(e.target.value)} + /> + + +
+ + {/* 编辑字典 Modal */} + { + setIsEditDictModalVisible(false); + setEditDictData(null); + }} + > +
+ + + setEditDictData({ ...editDictData, name: e.target.value }) + } + /> + + + + setEditDictData({ ...editDictData, title: e.target.value }) + } /> diff --git a/src/pages/Order/List/index.tsx b/src/pages/Order/List/index.tsx index 6c3b0f9..c325835 100644 --- a/src/pages/Order/List/index.tsx +++ b/src/pages/Order/List/index.tsx @@ -26,7 +26,6 @@ import { import { productcontrollerSearchproducts } from '@/servers/api/product'; import { sitecontrollerAll } from '@/servers/api/site'; import { stockcontrollerGetallstockpoints } from '@/servers/api/stock'; -import { wpproductcontrollerSearchproducts } from '@/servers/api/wpProduct'; import { formatShipmentState, formatSource } from '@/utils/format'; import { CodeSandboxOutlined, @@ -453,7 +452,6 @@ const ListPage: React.FC = () => { selectedRowKeys, onChange: (keys) => setSelectedRowKeys(keys), }} - rowClassName={(record) => { return record.id === activeLine ? styles['selected-line-order-protable'] @@ -482,9 +480,9 @@ const ListPage: React.FC = () => { }} tableRef={actionRef} />, - // - + , ]} request={async ({ date, ...param }: any) => { if (param.status === 'all') { @@ -605,33 +603,33 @@ const Detail: React.FC<{ ) ? [] : [ - , - , - ]), + }} + > + 同步订单 + , + ]), // ...(['processing', 'pending_reshipment'].includes(record.orderStatus) // ? [ // , @@ -650,152 +648,152 @@ const Detail: React.FC<{ 'pending_refund', ].includes(record.orderStatus) ? [ - , - { - try { - if (!record.id) { - message.error('订单ID不存在'); - return; + , + { + try { + if (!record.id) { + message.error('订单ID不存在'); + return; + } + const { success, message: errMsg } = + await ordercontrollerChangestatus( + { + id: record.id, + }, + { + status: 'after_sale_pending', + }, + ); + if (!success) { + throw new Error(errMsg); + } + tableRef.current?.reload(); + } catch (error: any) { + message.error(error.message); } - const { success, message: errMsg } = - await ordercontrollerChangestatus( - { - id: record.id, - }, - { - status: 'after_sale_pending', - }, - ); - if (!success) { - throw new Error(errMsg); - } - tableRef.current?.reload(); - } catch (error: any) { - message.error(error.message); - } - }} - > - - , - ] + }} + > + + , + ] : []), ...(record.orderStatus === 'after_sale_pending' ? [ - , - { - try { - if (!record.id) { - message.error('订单ID不存在'); - return; - } - const { success, message: errMsg } = - await ordercontrollerCancelorder({ - id: record.id, - }); - if (!success) { - throw new Error(errMsg); - } - tableRef.current?.reload(); - } catch (error: any) { - message.error(error.message); - } - }} - > - - , - , - { - try { - if (!record.id) { - message.error('订单ID不存在'); - return; - } - const { success, message: errMsg } = - await ordercontrollerRefundorder({ - id: record.id, - }); - if (!success) { - throw new Error(errMsg); - } - tableRef.current?.reload(); - } catch (error: any) { - message.error(error.message); - } - }} - > - - , - , - { - try { - if (!record.id) { - message.error('订单ID不存在'); - return; - } - const { success, message: errMsg } = - await ordercontrollerCompletedorder({ - id: record.id, - }); - if (!success) { - throw new Error(errMsg); - } - tableRef.current?.reload(); - } catch (error: any) { - message.error(error.message); - } - }} - > - - , - , - { - try { - const { success, message: errMsg } = - await ordercontrollerChangestatus( - { + , + { + try { + if (!record.id) { + message.error('订单ID不存在'); + return; + } + const { success, message: errMsg } = + await ordercontrollerCancelorder({ id: record.id, - }, - { - status: 'pending_reshipment', - }, - ); - if (!success) { - throw new Error(errMsg); + }); + if (!success) { + throw new Error(errMsg); + } + tableRef.current?.reload(); + } catch (error: any) { + message.error(error.message); } - tableRef.current?.reload(); - } catch (error: any) { - message.error(error.message); - } - }} - > - - , - ] + }} + > + + , + , + { + try { + if (!record.id) { + message.error('订单ID不存在'); + return; + } + const { success, message: errMsg } = + await ordercontrollerRefundorder({ + id: record.id, + }); + if (!success) { + throw new Error(errMsg); + } + tableRef.current?.reload(); + } catch (error: any) { + message.error(error.message); + } + }} + > + + , + , + { + try { + if (!record.id) { + message.error('订单ID不存在'); + return; + } + const { success, message: errMsg } = + await ordercontrollerCompletedorder({ + id: record.id, + }); + if (!success) { + throw new Error(errMsg); + } + tableRef.current?.reload(); + } catch (error: any) { + message.error(error.message); + } + }} + > + + , + , + { + try { + const { success, message: errMsg } = + await ordercontrollerChangestatus( + { + id: record.id, + }, + { + status: 'pending_reshipment', + }, + ); + if (!success) { + throw new Error(errMsg); + } + tableRef.current?.reload(); + } catch (error: any) { + message.error(error.message); + } + }} + > + + , + ] : []), ]} > @@ -1057,31 +1055,31 @@ const Detail: React.FC<{ } actions={ v.state === 'waiting-for-scheduling' || - v.state === 'waiting-for-transit' + v.state === 'waiting-for-transit' ? [ - { - try { - const { success, message: errMsg } = - await logisticscontrollerDelshipment({ - id: v.id, - }); - if (!success) { - throw new Error(errMsg); + { + try { + const { success, message: errMsg } = + await logisticscontrollerDelshipment({ + id: v.id, + }); + if (!success) { + throw new Error(errMsg); + } + tableRef.current?.reload(); + initRequest(); + } catch (error: any) { + message.error(error.message); } - tableRef.current?.reload(); - initRequest(); - } catch (error: any) { - message.error(error.message); - } - }} - > - - 取消运单 - , - ] + }} + > + + 取消运单 + , + ] : [] } > @@ -1469,16 +1467,16 @@ const Shipping: React.FC<{ - // value && value.length > 0 - // ? Promise.resolve() - // : Promise.reject('至少需要一个商品'), - // }, - // ]} + // rules={[ + // { + // required: true, + // message: '至少需要一个商品', + // validator: (_, value) => + // value && value.length > 0 + // ? Promise.resolve() + // : Promise.reject('至少需要一个商品'), + // }, + // ]} > diff --git a/src/pages/Product/Attribute/consts.ts b/src/pages/Product/Attribute/consts.ts index 23d769b..b7c66ac 100644 --- a/src/pages/Product/Attribute/consts.ts +++ b/src/pages/Product/Attribute/consts.ts @@ -1,8 +1 @@ -// 限定允许管理的字典名称集合 -export const attributes = new Set([ - 'brand', - 'strength', - 'flavor', - 'size', - 'humidity', -]); +export const notAttributes = new Set(['zh-cn', 'en-us', 'category']); diff --git a/src/pages/Product/Attribute/index.tsx b/src/pages/Product/Attribute/index.tsx index cd57d58..f7cce34 100644 --- a/src/pages/Product/Attribute/index.tsx +++ b/src/pages/Product/Attribute/index.tsx @@ -20,7 +20,7 @@ import React, { useEffect, useRef, useState } from 'react'; const { Sider, Content } = Layout; -import { attributes } from './consts'; +import { notAttributes } from './consts'; const AttributePage: React.FC = () => { // 左侧字典列表状态 @@ -41,11 +41,14 @@ const AttributePage: React.FC = () => { setLoadingDicts(true); try { const res = await request('/dict/list', { params: { title } }); - // 条件判断,过滤只保留 allowedDictNames 中的字典 - const filtered = (res || []).filter((d: any) => attributes.has(d?.name)); + // 条件判断,确保res是数组再进行过滤 + const dataList = Array.isArray(res) ? res : res?.data || []; + const filtered = dataList.filter((d: any) => !notAttributes.has(d?.name)); setDicts(filtered); } catch (error) { + console.error('获取字典列表失败:', error); message.error('获取字典列表失败'); + setDicts([]); } setLoadingDicts(false); }; @@ -114,16 +117,23 @@ const AttributePage: React.FC = () => { return; } if (selectedDict?.id) { - const list = await request('/dict/items', { - params: { - dictId: selectedDict.id, - }, - }); - const exists = - Array.isArray(list) && list.some((it: any) => it.id === itemId); - if (exists) { - message.error('删除失败'); - } else { + try { + const list = await request('/dict/items', { + params: { + dictId: selectedDict.id, + }, + }); + // 确保list是数组再进行some操作 + const dataList = Array.isArray(list) ? list : list?.data || []; + const exists = dataList.some((it: any) => it.id === itemId); + if (exists) { + message.error('删除失败'); + } else { + message.success('删除成功'); + actionRef.current?.reload(); + } + } catch (error) { + console.error('验证删除结果失败:', error); message.success('删除成功'); actionRef.current?.reload(); } @@ -245,24 +255,44 @@ const AttributePage: React.FC = () => { }; } const { name, title } = params; - const res = await request('/dict/items', { - params: { - dictId: selectedDict.id, - name, - title, - }, - }); - return { - data: res, - success: true, - }; + try { + const res = await request('/dict/items', { + params: { + dictId: selectedDict.id, + name, + title, + }, + }); + // 确保返回的是数组 + const data = Array.isArray(res) ? res : res?.data || []; + return { + data: data, + success: true, + }; + } catch (error) { + console.error('获取字典项失败:', error); + return { + data: [], + success: false, + }; + } }} rowKey="id" search={{ layout: 'vertical', }} pagination={false} - options={false} + options={{ + reload: true, + density: false, + setting: { + draggable: true, + checkable: true, + checkedReset: false, + }, + search: false, + fullScreen: false, + }} size="small" key={selectedDict?.id} headerTitle={ diff --git a/src/pages/Product/Category/index.tsx b/src/pages/Product/Category/index.tsx index 8411b75..7c516cf 100644 --- a/src/pages/Product/Category/index.tsx +++ b/src/pages/Product/Category/index.tsx @@ -23,7 +23,7 @@ import { message, } from 'antd'; import React, { useEffect, useState } from 'react'; -import { attributes } from '../Attribute/consts'; +import { notAttributes } from '../Attribute/consts'; const { Sider, Content } = Layout; @@ -116,7 +116,7 @@ const CategoryPage: React.FC = () => { const res = await request('/dict/list'); // Defensive check for response structure: handle both raw array and wrapped response const list = Array.isArray(res) ? res : res?.data || []; - const filtered = list.filter((d: any) => attributes.has(d.name)); + const filtered = list.filter((d: any) => !notAttributes.has(d.name)); // Filter out already added attributes const existingDictIds = new Set( categoryAttributes.map((ca: any) => ca.dictId), @@ -244,7 +244,10 @@ const CategoryPage: React.FC = () => { , ]} > - + )} /> @@ -310,16 +313,18 @@ const CategoryPage: React.FC = () => { onFinish={handleCategorySubmit} layout="vertical" > - + - + + + + + + +
diff --git a/src/pages/Product/List/index.tsx b/src/pages/Product/List/index.tsx index c74b31c..3f3bd9f 100644 --- a/src/pages/Product/List/index.tsx +++ b/src/pages/Product/List/index.tsx @@ -10,10 +10,6 @@ import { } from '@/servers/api/product'; import { sitecontrollerAll } from '@/servers/api/site'; import { siteapicontrollerGetproducts } from '@/servers/api/siteApi'; -import { - wpproductcontrollerBatchsynctosite, - wpproductcontrollerSynctoproduct, -} from '@/servers/api/wpProduct'; import { ActionType, ModalForm, @@ -462,9 +458,9 @@ const List: React.FC = () => { dataIndex: 'siteSkus', render: (_, record) => ( <> - {record.siteSkus?.map((code, index) => ( + {record.siteSkus?.map((siteSku, index) => ( - {code} + {siteSku.siteSku} ))} @@ -609,55 +605,6 @@ const List: React.FC = () => { toolBarRender={() => [ // 新建按钮 , - // 批量编辑按钮 - , - // 批量同步按钮 - , - // 批量删除按钮 - , - // 导出 CSV(后端返回 text/csv,直接新窗口下载) - , // 导入 CSV(使用 customRequest 以支持 request 拦截器和鉴权) { } }} > - + , + // 批量编辑按钮 + , + // 批量同步按钮 + , + // 批量删除按钮 + , + // 导出 CSV(后端返回 text/csv,直接新窗口下载) + , ]} request={async (params, sort) => { let sortField = undefined; diff --git a/src/pages/Product/Permutation/index.tsx b/src/pages/Product/Permutation/index.tsx index 2edd26f..9f655b9 100644 --- a/src/pages/Product/Permutation/index.tsx +++ b/src/pages/Product/Permutation/index.tsx @@ -112,12 +112,18 @@ const PermutationPage: React.FC = () => { // 2. Fetch Attribute Values (Dict Items) const valuesMap: Record = {}; for (const attr of attrs) { - const dictId = attr.dict?.id || attr.dictId; - if (dictId) { - const itemsRes = await request('/dict/items', { - params: { dictId }, - }); - valuesMap[attr.name] = itemsRes || []; + // 使用属性中直接包含的items,而不是额外请求 + if (attr.items && Array.isArray(attr.items)) { + valuesMap[attr.name] = attr.items; + } else { + // 如果没有items,尝试通过dictId获取 + const dictId = attr.dict?.id || attr.dictId; + if (dictId) { + const itemsRes = await request('/dict/items', { + params: { dictId }, + }); + valuesMap[attr.name] = itemsRes || []; + } } } setAttributeValues(valuesMap); @@ -206,7 +212,7 @@ const PermutationPage: React.FC = () => { const valB = b[attr.name]?.name || ''; return valA.localeCompare(valB); }, - filters: attributeValues[attr.name]?.map((v: any) => ({ + filters: attributeValues?.[attr.name]?.map?.((v: any) => ({ text: v.name, value: v.name, })), diff --git a/src/pages/Product/Sync/index.tsx b/src/pages/Product/Sync/index.tsx index 3bfaee7..bd6c9ae 100644 --- a/src/pages/Product/Sync/index.tsx +++ b/src/pages/Product/Sync/index.tsx @@ -9,7 +9,16 @@ import { ProTable, } from '@ant-design/pro-components'; import { request } from '@umijs/max'; -import { Button, Card, Spin, Tag, message, Select, Progress, Modal } from 'antd'; +import { + Button, + Card, + message, + Modal, + Progress, + Select, + Spin, + Tag, +} from 'antd'; import React, { useEffect, useRef, useState } from 'react'; import EditForm from '../List/EditForm'; @@ -89,7 +98,13 @@ const ProductSyncPage: React.FC = () => { const [batchSyncModalVisible, setBatchSyncModalVisible] = useState(false); const [syncProgress, setSyncProgress] = useState(0); const [syncing, setSyncing] = useState(false); - const [syncResults, setSyncResults] = useState<{ success: number; failed: number; errors: string[] }>({ success: 0, failed: 0, errors: [] }); + const [syncResults, setSyncResults] = useState<{ + success: number; + failed: number; + errors: string[]; + }>({ success: 0, failed: 0, errors: [] }); + const [selectedRowKeys, setSelectedRowKeys] = useState([]); + const [selectedRows, setSelectedRows] = useState([]); // 初始化数据:获取站点和所有 WP 产品 useEffect(() => { @@ -197,41 +212,51 @@ const ProductSyncPage: React.FC = () => { }; // 批量同步产品到指定站点 - const batchSyncProducts = async () => { + const batchSyncProducts = async (productsToSync?: ProductWithWP[]) => { if (!selectedSiteId) { message.error('请选择要同步到的站点'); return; } - const targetSite = sites.find(site => site.id === selectedSiteId); + const targetSite = sites.find((site) => site.id === selectedSiteId); if (!targetSite) { message.error('选择的站点不存在'); return; } + // 如果没有传入产品列表,则使用选中的产品 + let products = productsToSync || selectedRows; + + // 如果既没有传入产品也没有选中产品,则同步所有产品 + if (!products || products.length === 0) { + try { + const { data, success } = await productcontrollerGetproductlist({ + current: 1, + pageSize: 10000, // 获取所有产品 + } as any); + + if (!success || !data?.items) { + message.error('获取产品列表失败'); + return; + } + products = data.items as ProductWithWP[]; + } catch (error) { + message.error('获取产品列表失败'); + return; + } + } + setSyncing(true); setSyncProgress(0); setSyncResults({ success: 0, failed: 0, errors: [] }); + const totalProducts = products.length; + let processed = 0; + let successCount = 0; + let failedCount = 0; + const errors: string[] = []; + try { - // 获取所有产品 - const { data, success } = await productcontrollerGetproductlist({ - current: 1, - pageSize: 10000, // 获取所有产品 - } as any); - - if (!success || !data?.items) { - message.error('获取产品列表失败'); - return; - } - - const products = data.items as ProductWithWP[]; - const totalProducts = products.length; - let processed = 0; - let successCount = 0; - let failedCount = 0; - const errors: string[] = []; - // 逐个同步产品 for (const product of products) { try { @@ -239,7 +264,10 @@ const ProductSyncPage: React.FC = () => { let siteProductSku = ''; if (product.siteSkus && product.siteSkus.length > 0) { const siteSkuInfo = product.siteSkus.find((sku: any) => { - return sku.siteSku && sku.siteSku.includes(targetSite.skuPrefix || targetSite.name); + return ( + sku.siteSku && + sku.siteSku.includes(targetSite.skuPrefix || targetSite.name) + ); }); if (siteSkuInfo) { siteProductSku = siteSkuInfo.siteSku; @@ -247,15 +275,15 @@ const ProductSyncPage: React.FC = () => { } // 如果没有找到实际的siteSku,则根据模板生成 - const expectedSku = siteProductSku || ( - skuTemplate - ? renderSku(skuTemplate, { site: targetSite, product }) - : `${targetSite.skuPrefix || ''}-${product.sku}` - ); + const expectedSku = + siteProductSku || + (skuTemplate + ? renderSiteSku(skuTemplate, { site: targetSite, product }) + : `${targetSite.skuPrefix || ''}-${product.sku}`); // 检查是否已存在 const existingProduct = wpProductMap.get(expectedSku); - + // 准备同步数据 const syncData = { name: product.name, @@ -272,10 +300,13 @@ const ProductSyncPage: React.FC = () => { let res; if (existingProduct?.externalProductId) { // 更新现有产品 - res = await request(`/site-api/${targetSite.id}/products/${existingProduct.externalProductId}`, { - method: 'PUT', - data: syncData, - }); + res = await request( + `/site-api/${targetSite.id}/products/${existingProduct.externalProductId}`, + { + method: 'PUT', + data: syncData, + }, + ); } else { // 创建新产品 res = await request(`/site-api/${targetSite.id}/products`, { @@ -300,7 +331,6 @@ const ProductSyncPage: React.FC = () => { failedCount++; errors.push(`产品 ${product.sku}: ${res.message || '同步失败'}`); } - } catch (error: any) { failedCount++; errors.push(`产品 ${product.sku}: ${error.message || '未知错误'}`); @@ -311,16 +341,17 @@ const ProductSyncPage: React.FC = () => { } setSyncResults({ success: successCount, failed: failedCount, errors }); - + if (failedCount === 0) { message.success(`批量同步完成,成功同步 ${successCount} 个产品`); } else { - message.warning(`批量同步完成,成功 ${successCount} 个,失败 ${failedCount} 个`); + message.warning( + `批量同步完成,成功 ${successCount} 个,失败 ${failedCount} 个`, + ); } // 刷新表格 actionRef.current?.reload(); - } catch (error: any) { message.error('批量同步失败: ' + (error.message || error.toString())); } finally { @@ -329,7 +360,7 @@ const ProductSyncPage: React.FC = () => { }; // 简单的模板渲染函数 - const renderSku = (template: string, data: any) => { + const renderSiteSku = (template: string, data: any) => { if (!template) return ''; // 支持 <%= it.path %> (Eta) 和 {{ path }} (Mustache/Handlebars) return template.replace( @@ -453,7 +484,9 @@ const ProductSyncPage: React.FC = () => { const siteSkuInfo = record.siteSkus.find((sku: any) => { // 这里假设可以根据站点名称或其他标识来匹配 // 如果需要更精确的匹配逻辑,可以根据实际需求调整 - return sku.siteSku && sku.siteSku.includes(site.skuPrefix || site.name); + return ( + sku.siteSku && sku.siteSku.includes(site.skuPrefix || site.name) + ); }); if (siteSkuInfo) { siteProductSku = siteSkuInfo.siteSku; @@ -461,18 +494,21 @@ const ProductSyncPage: React.FC = () => { } // 如果没有找到实际的siteSku,则根据模板或默认规则生成期望的SKU - const expectedSku = siteProductSku || ( - skuTemplate - ? renderSku(skuTemplate, { site, product: record }) - : `${site.skuPrefix || ''}-${record.sku}` - ); + const expectedSku = + siteProductSku || + (skuTemplate + ? renderSiteSku(skuTemplate, { site, product: record }) + : `${site.skuPrefix || ''}-${record.sku}`); // 尝试用确定的SKU获取WP产品 let wpProduct = wpProductMap.get(expectedSku); // 如果根据实际SKU没找到,再尝试用模板生成的SKU查找 if (!wpProduct && siteProductSku && skuTemplate) { - const templateSku = renderSku(skuTemplate, { site, product: record }); + const templateSku = renderSiteSku(skuTemplate, { + site, + product: record, + }); wpProduct = wpProductMap.get(templateSku); } @@ -490,12 +526,12 @@ const ProductSyncPage: React.FC = () => { return await syncProductToSite(values, record, site); }} initialValues={{ - sku: siteProductSku || ( - skuTemplate - ? renderSku(skuTemplate, { site, product: record }) - : `${site.skuPrefix || ''}-${record.sku}` - ), - }} + sku: + siteProductSku || + (skuTemplate + ? renderSiteSku(skuTemplate, { site, product: record }) + : `${site.skuPrefix || ''}-${record.sku}`), + }} > { } return ( - - ({ + label: site.name, + value: site.id, + }))} + />, + , + ]} request={async (params, sort, filter) => { // 调用本地获取产品列表 API const { data, success } = await productcontrollerGetproductlist({ @@ -650,7 +696,7 @@ const ProductSyncPage: React.FC = () => { }} dateFormatter="string" /> - + {/* 批量同步模态框 */} { maskClosable={!syncing} >
-

目标站点:{sites.find(s => s.id === selectedSiteId)?.name}

-

此操作将同步所有库存产品到指定站点,请确认是否继续?

+

+ 目标站点: + {sites.find((s) => s.id === selectedSiteId)?.name} +

+ {selectedRows.length > 0 ? ( +

+ 已选择 {selectedRows.length} 个产品进行同步 +

+ ) : ( +

此操作将同步所有库存产品到指定站点,请确认是否继续?

+ )}
- + {syncing && (
同步进度:
@@ -674,32 +729,37 @@ const ProductSyncPage: React.FC = () => {
)} - + {syncResults.errors.length > 0 && (
错误详情:
{syncResults.errors.slice(0, 10).map((error, index) => ( -
+
{error}
))} {syncResults.errors.length > 10 && ( -
...还有 {syncResults.errors.length - 10} 个错误
+
+ ...还有 {syncResults.errors.length - 10} 个错误 +
)}
)} - +
- -
-
{ { key: 'media', label: '媒体管理' }, { key: 'customers', label: '客户管理' }, { key: 'reviews', label: '评论管理' }, + { key: 'webhooks', label: 'Webhooks管理' }, + { key: 'links', label: '链接管理' }, ]} />
- +
{siteId ? :
请选择店铺
} diff --git a/src/pages/Site/Shop/Links/index.tsx b/src/pages/Site/Shop/Links/index.tsx new file mode 100644 index 0000000..62acdee --- /dev/null +++ b/src/pages/Site/Shop/Links/index.tsx @@ -0,0 +1,99 @@ +import { LinkOutlined } from '@ant-design/icons'; +import { PageHeader } from '@ant-design/pro-layout'; +import { request, useParams } from '@umijs/max'; +import { App, Button, Card, List } from 'antd'; +import React, { useEffect, useState } from 'react'; + +// 定义链接项的类型 +interface LinkItem { + title: string; + url: string; +} + +const LinksPage: React.FC = () => { + const { siteId } = useParams<{ siteId: string }>(); + const { message: antMessage } = App.useApp(); + const [links, setLinks] = useState([]); + const [loading, setLoading] = useState(true); + + // 获取链接列表的函数 + const fetchLinks = async () => { + if (!siteId) return; + + setLoading(true); + try { + const response = await request(`/site-api/${siteId}/links`); + if (response.success && response.data) { + setLinks(response.data); + } else { + antMessage.error(response.message || '获取链接列表失败'); + } + } catch (error) { + antMessage.error('获取链接列表失败'); + } finally { + setLoading(false); + } + }; + + // 页面加载时获取链接列表 + useEffect(() => { + fetchLinks(); + }, [siteId]); + + // 处理链接点击事件,在新标签页打开 + const handleLinkClick = (url: string) => { + window.open(url, '_blank', 'noopener,noreferrer'); + }; + + return ( +
+ + + 刷新列表 + + } + > + ( + } + onClick={() => handleLinkClick(item.url)} + target="_blank" + > + 访问 + , + ]} + > + + {item.url} + + } + /> + + )} + /> + +
+ ); +}; + +export default LinksPage; diff --git a/src/pages/Site/Shop/Orders/index.tsx b/src/pages/Site/Shop/Orders/index.tsx index 08b8506..e92ac31 100644 --- a/src/pages/Site/Shop/Orders/index.tsx +++ b/src/pages/Site/Shop/Orders/index.tsx @@ -186,6 +186,54 @@ const OrdersPage: React.FC = () => { > + {record.status === 'completed' && ( + { + try { + const res = await request( + `/site-api/${siteId}/orders/${record.id}/cancel-ship`, + { method: 'POST' }, + ); + if (res.success) { + message.success('取消发货成功'); + actionRef.current?.reload(); + } else { + message.error(res.message || '取消发货失败'); + } + } catch (e) { + message.error('取消发货失败'); + } + }} + > + + + )} { diff --git a/src/pages/Site/Shop/Products/index.tsx b/src/pages/Site/Shop/Products/index.tsx index fbaeebb..aa243f2 100644 --- a/src/pages/Site/Shop/Products/index.tsx +++ b/src/pages/Site/Shop/Products/index.tsx @@ -188,9 +188,9 @@ const ProductsPage: React.FC = () => { 分类: {record.erpProduct.category.name} )} -
- 库存: {record.erpProduct.stock_quantity ?? '-'} -
+
+ 库存: {record.erpProduct.stock_quantity ?? '-'} +
); } diff --git a/src/pages/Site/Shop/Reviews/ReviewForm.tsx b/src/pages/Site/Shop/Reviews/ReviewForm.tsx index c6e577a..3f38d4d 100644 --- a/src/pages/Site/Shop/Reviews/ReviewForm.tsx +++ b/src/pages/Site/Shop/Reviews/ReviewForm.tsx @@ -1,4 +1,12 @@ -import React from 'react'; +import { + siteapicontrollerCreatereview, + siteapicontrollerUpdatereview, +} from '@/servers/api/siteApi'; +import { Form, Input, InputNumber, Modal, Select, message } from 'antd'; +import React, { useEffect } from 'react'; + +const { TextArea } = Input; +const { Option } = Select; interface ReviewFormProps { open: boolean; @@ -15,19 +23,161 @@ const ReviewForm: React.FC = ({ onClose, onSuccess, }) => { - // // 这是一个临时的占位符组件 - // // 你可以在这里实现表单逻辑 - if (!open) { - return null; - } + const [form] = Form.useForm(); + + // 当编辑状态改变时,重置表单数据 + useEffect(() => { + if (editing) { + form.setFieldsValue({ + product_id: editing.product_id, + author: editing.author, + email: editing.email, + content: editing.content, + rating: editing.rating, + status: editing.status, + }); + } else { + form.resetFields(); + } + }, [editing, form]); + + // 处理表单提交 + const handleSubmit = async (values: any) => { + try { + let response; + + if (editing) { + // 更新评论 + response = await siteapicontrollerUpdatereview( + { + siteId, + id: editing.id, + }, + { + review: values.content, + rating: values.rating, + status: values.status, + }, + ); + } else { + // 创建新评论 + response = await siteapicontrollerCreatereview( + { + siteId, + }, + { + product_id: values.product_id, + review: values.content, + rating: values.rating, + author: values.author, + author_email: values.email, + }, + ); + } + + if (response.success) { + message.success(editing ? '更新成功' : '创建成功'); + onSuccess(); + onClose(); + form.resetFields(); + } else { + message.error(response.message || '操作失败'); + } + } catch (error) { + console.error('提交评论表单失败:', error); + message.error('提交失败,请重试'); + } + }; return ( -
-

Review Form

-

Site ID: {siteId}

-

Editing: {editing ? 'Yes' : 'No'}

- -
+ form.submit()} + okText="保存" + cancelText="取消" + width={600} + > +
+ {!editing && ( + <> + + + + + + + + + + + )} + + +