diff --git a/src/pages/Site/List/index.tsx b/src/pages/Site/List/index.tsx index b365ce0..e138ac1 100644 --- a/src/pages/Site/List/index.tsx +++ b/src/pages/Site/List/index.tsx @@ -10,7 +10,7 @@ import { ProTable, } from '@ant-design/pro-components'; import { request } from '@umijs/max'; -import { Button, message, Popconfirm, Space, Tag } from 'antd'; +import { Button, message, notification, Popconfirm, Space, Tag } from 'antd'; import React, { useEffect, useRef, useState } from 'react'; // 区域数据项类型 @@ -58,6 +58,62 @@ const SiteList: React.FC = () => { const formRef = useRef(); const [open, setOpen] = useState(false); const [editing, setEditing] = useState(null); + const [selectedRowKeys, setSelectedRowKeys] = useState([]); + + const handleSync = async (ids: number[]) => { + if (!ids.length) return; + const hide = message.loading('正在同步...', 0); + + const stats = { + products: { success: 0, fail: 0 }, + orders: { success: 0, fail: 0 }, + subscriptions: { success: 0, fail: 0 }, + }; + + try { + for (const id of ids) { + // 同步产品 + const prodRes = await request(`/wp_product/sync/${id}`, { method: 'POST' }); + if (prodRes) { + stats.products.success += prodRes.successCount || 0; + stats.products.fail += prodRes.failureCount || 0; + } + + // 同步订单 + const orderRes = await request(`/order/syncOrder/${id}`, { method: 'POST' }); + if (orderRes) { + stats.orders.success += orderRes.successCount || 0; + stats.orders.fail += orderRes.failureCount || 0; + } + + // 同步订阅 + const subRes = await request(`/subscription/sync/${id}`, { method: 'POST' }); + if (subRes) { + stats.subscriptions.success += subRes.successCount || 0; + stats.subscriptions.fail += subRes.failureCount || 0; + } + } + hide(); + + notification.success({ + message: '同步完成', + description: ( +
+

产品: 成功 {stats.products.success}, 失败 {stats.products.fail}

+

订单: 成功 {stats.orders.success}, 失败 {stats.orders.fail}

+

订阅: 成功 {stats.subscriptions.success}, 失败 {stats.subscriptions.fail}

+
+ ), + duration: null, // 不自动关闭 + }); + + setSelectedRowKeys([]); + actionRef.current?.reload(); + } catch (error: any) { + hide(); + message.error(error.message || '同步失败'); + } + }; useEffect(() => { if (!open) return; @@ -174,6 +230,13 @@ const SiteList: React.FC = () => { hideInSearch: true, render: (_, row) => ( + , // 同步包括 orders subscriptions 等等 - + , ]} /> diff --git a/src/pages/Site/Shop/Customers/index.tsx b/src/pages/Site/Shop/Customers/index.tsx index 0b520cb..e3148ca 100644 --- a/src/pages/Site/Shop/Customers/index.tsx +++ b/src/pages/Site/Shop/Customers/index.tsx @@ -1,9 +1,60 @@ -import { ActionType, DrawerForm, PageContainer, ProColumns, ProFormText, ProTable } from '@ant-design/pro-components'; +import { ActionType, DrawerForm, ModalForm, PageContainer, ProColumns, ProFormText, ProTable } from '@ant-design/pro-components'; import { request, useParams } from '@umijs/max'; import { App, Avatar, Button, Popconfirm, Space, Tag } from 'antd'; import React, { useRef, useState } from 'react'; import { DeleteFilled, EditOutlined, PlusOutlined, UserOutlined } from '@ant-design/icons'; +const BatchEditCustomers: React.FC<{ + tableRef: React.MutableRefObject; + selectedRowKeys: React.Key[]; + setSelectedRowKeys: (keys: React.Key[]) => void; + siteId?: string; +}> = ({ tableRef, selectedRowKeys, setSelectedRowKeys, siteId }) => { + const { message } = App.useApp(); + return ( + } + > + 批量编辑 + + } + width={400} + modalProps={{ destroyOnHidden: true }} + onFinish={async (values) => { + if (!siteId) return false; + let ok = 0, fail = 0; + for (const id of selectedRowKeys) { + try { + // Remove undefined values + const data = Object.fromEntries(Object.entries(values).filter(([_, v]) => v !== undefined && v !== '')); + if (Object.keys(data).length === 0) continue; + + const res = await request(`/site-api/${siteId}/customers/${id}`, { + method: 'PUT', + data: data, + }); + if (res.success) ok++; + else fail++; + } catch (e) { + fail++; + } + } + message.success(`成功 ${ok}, 失败 ${fail}`); + tableRef.current?.reload(); + setSelectedRowKeys([]); + return true; + }} + > + + + ); +}; + const CustomerPage: React.FC = () => { const { message } = App.useApp(); const { siteId } = useParams<{ siteId: string }>(); @@ -167,10 +218,11 @@ const CustomerPage: React.FC = () => { , - + + + + ); }} /> diff --git a/src/pages/Site/Shop/Orders/index.tsx b/src/pages/Site/Shop/Orders/index.tsx index bf59b00..e51b05e 100644 --- a/src/pages/Site/Shop/Orders/index.tsx +++ b/src/pages/Site/Shop/Orders/index.tsx @@ -27,7 +27,7 @@ import { Tag, } from 'antd'; import React, { useEffect, useMemo, useRef, useState } from 'react'; -import { CreateOrder, Detail, OrderNote, Shipping } from '../components/Order/Forms'; +import { BatchEditOrders, CreateOrder, EditOrder, OrderNote, Shipping } from '../components/Order/Forms'; import { DeleteFilled } from '@ant-design/icons'; const OrdersPage: React.FC = () => { @@ -122,9 +122,20 @@ const OrdersPage: React.FC = () => { render: (_, record) => record.shipping?.phone || record.billing?.phone, }, { - title: '州', + title: '账单地址', + dataIndex: 'billing_full_address', hideInSearch: true, - render: (_, record) => record.shipping?.state || record.billing?.state, + width: 200, + ellipsis: true, + copyable: true, + }, + { + title: '收货地址', + dataIndex: 'shipping_full_address', + hideInSearch: true, + width: 200, + ellipsis: true, + copyable: true, }, { title: '操作', @@ -135,7 +146,7 @@ const OrdersPage: React.FC = () => { render: (_, record) => { return ( <> - { key: 'history', label: ( ), @@ -165,10 +176,8 @@ const OrdersPage: React.FC = () => { }} > e.preventDefault()}> - - 更多 - - + @@ -217,6 +226,12 @@ const OrdersPage: React.FC = () => { }} toolBarRender={() => [ , + , + } onFinish={async (values: any) => { if (!siteId) { message.error('缺少站点ID'); @@ -236,12 +242,14 @@ export const Shipping: React.FC<{ trigger={ } request={async () => { if (!siteId) return {}; @@ -569,7 +577,11 @@ export const CreateOrder: React.FC<{ body: { maxHeight: '65vh', overflowY: 'auto', overflowX: 'hidden' }, }, }} - trigger={ + } params={{ source_type: 'admin', }} @@ -618,94 +630,194 @@ export const CreateOrder: React.FC<{ ); }; -export const Detail: React.FC<{ +export const BatchEditOrders: React.FC<{ + tableRef: React.MutableRefObject; + selectedRowKeys: React.Key[]; + setSelectedRowKeys: (keys: React.Key[]) => void; + siteId?: string; +}> = ({ tableRef, selectedRowKeys, setSelectedRowKeys, siteId }) => { + const { message } = App.useApp(); + return ( + } + > + 批量编辑 + + } + width={400} + modalProps={{ destroyOnHidden: true }} + onFinish={async (values) => { + if (!siteId) return false; + let ok = 0, fail = 0; + for (const id of selectedRowKeys) { + try { + // Remove undefined values + const data = Object.fromEntries(Object.entries(values).filter(([_, v]) => v !== undefined && v !== '')); + if (Object.keys(data).length === 0) continue; + + const res = await request(`/site-api/${siteId}/orders/${id}`, { + method: 'PUT', + data: data, + }); + if (res.success) ok++; + else fail++; + } catch (e) { + fail++; + } + } + message.success(`成功 ${ok}, 失败 ${fail}`); + tableRef.current?.reload(); + setSelectedRowKeys([]); + return true; + }} + > + + + ); +}; + +export const EditOrder: React.FC<{ tableRef: React.MutableRefObject; orderId: number; record: API.Order; setActiveLine: Function; siteId?: string; }> = ({ tableRef, orderId, record, setActiveLine, siteId }) => { - const [visiable, setVisiable] = useState(false); const { message } = App.useApp(); - const ref = useRef(); - - const initRequest = async () => { - if (!siteId) return { data: {} }; - - // Fetch detail from site-api - const { data, success } = await request(`/site-api/${siteId}/orders/${orderId}`); - - if (!success || !data) return { data: {} }; - - // Merge sales logic - const sales = data.sales || []; - const mergedSales = sales.reduce( - (acc: any[], cur: any) => { - let idx = acc.findIndex((v: any) => v.productId === cur.productId); - if (idx === -1) { - acc.push(cur); - } else { - acc[idx].quantity += cur.quantity; - } - return acc; - }, - [], - ); - data.sales = mergedSales; - - return { - data, - }; - }; + const formRef = useRef(); return ( - <> - + } + drawerProps={{ + destroyOnHidden: true, + width: '60vw', }} - /> - setVisiable(false)} - footer={[ - , - // ... Removed Sync Button and Status change buttons (they used local controller) - // We can re-enable them if we implement status change in site-api - // I didn't implement status change (updateOrder) in controller yet. - // Wait, I implemented `updateOrder`? No, I implemented `getOrders`, `getOrder`. - // I missed `updateOrder`! - // But I implemented `updateProduct`. - // I should add `updateOrder` if I want to support status change. - // The user said "Proxy unified forwarding (various CRUD)". - // So I should have added `updateOrder`. - // But time is tight. - // I will disable the buttons for now or leave them (they will fail). - // I'll disable them to be safe. - ]} - > - { + if (!siteId) return {}; + const { data, success } = await request(`/site-api/${siteId}/orders/${orderId}`); + if (!success || !data) return {}; + + const sales = data.sales || []; + const mergedSales = sales.reduce( + (acc: any[], cur: any) => { + let idx = acc.findIndex((v: any) => v.productId === cur.productId); + if (idx === -1) { + acc.push(cur); + } else { + acc[idx].quantity += cur.quantity; + } + return acc; + }, + [], + ); + data.sales = mergedSales; + + return data; + }} + onFinish={async (values) => { + if (!siteId) return false; + try { + const res = await request(`/site-api/${siteId}/orders/${orderId}`, { + method: 'PUT', + data: values + }); + if (res.success) { + message.success('更新成功'); + tableRef.current?.reload(); + return true; + } + message.error(res.message || '更新失败'); + return false; + } catch(e: any) { + message.error(e.message || '更新失败'); + return false; + } + }} + > + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + []} > - {/* ... Fields ... */} - - {/* ... */} - - - + + + + + + + + + + + ); }; diff --git a/src/pages/Site/Shop/temp.json b/src/pages/Site/Shop/temp.json new file mode 100644 index 0000000..0ba435c --- /dev/null +++ b/src/pages/Site/Shop/temp.json @@ -0,0 +1,53 @@ +{ + admin_id: 0, + admin_name: "", + birthday: 0, + contact: "", + country_id: 14, + created_at: 1765351077, + domain: "auspouches.com", + email: "daniel.waring81@gmail.com", + first_name: "Dan", + first_pay_at: 1765351308, + gender: 0, + id: 44898147, + ip: "1.146.111.163", + is_cart: 0, + is_event_sub: 1, + is_sub: 1, + is_verified: 1, + last_name: "Waring", + last_order_id: 236122, + login_at: 1765351340, + note: "", + order_at: 1765351224, + orders_count: 1, + pay_at: 1765351308, + source_device: "phone", + tags: [ + ], + total_spent: "203.81", + updated_at: 1765351515, + utm_medium: "referral", + utm_source: "checkout.cartadicreditopay.com", + visit_at: 1765351513, + country: { + chinese_name: "澳大利亚", + country_code2: "AU", + country_name: "Australia", + }, + sysinfo: { + user_agent: "Mozilla/5.0 (Linux; Android 16; Pixel 8 Pro Build/BP3A.251105.015; ) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/142.0.7444.212 Mobile Safari/537.36 MetaIAB Facebook", + timezone: "Etc/GMT-10", + os: "Android", + browser: "Pixel 8", + language: "en-GB", + screen_size: "528X1174", + viewport_size: "527X1026", + ip: "1.146.111.163", + }, + default_address: [ + ], + addresses: [ + ], +} \ No newline at end of file