diff --git a/.umirc.ts b/.umirc.ts index 5610920..e98fa55 100644 --- a/.umirc.ts +++ b/.umirc.ts @@ -310,6 +310,11 @@ export default defineConfig({ changeOrigin: true, pathRewrite: { '^/api': '' }, }, + '/site-api': { + target: UMI_APP_API_URL, + changeOrigin: true, + pathRewrite: { '^/site-api': '/site-api' }, + }, }, npmClient: 'pnpm', }); diff --git a/src/pages/Product/Attribute/index.tsx b/src/pages/Product/Attribute/index.tsx index 351ccb3..1e4562b 100644 --- a/src/pages/Product/Attribute/index.tsx +++ b/src/pages/Product/Attribute/index.tsx @@ -102,12 +102,33 @@ const AttributePage: React.FC = () => { // 删除字典项 const handleDeleteDictItem = async (itemId: number) => { try { - const success = await request(`/dict/item/${itemId}`, { method: 'DELETE' }); - if (success) { - message.success('删除成功'); - actionRef.current?.reload(); // 刷新 ProTable - } else { + const res = await request(`/dict/item/${itemId}`, { method: 'DELETE' }); + const isOk = + typeof res === 'boolean' + ? res + : res && res.code === 0 + ? res.data === true || res.data === null + : false; + if (!isOk) { message.error('删除失败'); + 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 { + message.success('删除成功'); + actionRef.current?.reload(); + } + } else { + message.success('删除成功'); + actionRef.current?.reload(); } } catch (error) { message.error('删除失败'); diff --git a/src/pages/Product/List/EditForm.tsx b/src/pages/Product/List/EditForm.tsx index 04abe1b..bdfd1ce 100644 --- a/src/pages/Product/List/EditForm.tsx +++ b/src/pages/Product/List/EditForm.tsx @@ -2,8 +2,10 @@ import { productcontrollerGetcategoriesall, productcontrollerGetcategoryattributes, productcontrollerGetproductcomponents, + productcontrollerGetproductsiteskus, productcontrollerUpdateproduct, } from '@/servers/api/product'; +import { sitecontrollerAll } from '@/servers/api/site'; import { stockcontrollerGetstocks as getStocks } from '@/servers/api/stock'; import { ActionType, @@ -33,6 +35,8 @@ const EditForm: React.FC<{ const [stockStatus, setStockStatus] = useState< 'in-stock' | 'out-of-stock' | null >(null); + const [siteSkuEntries, setSiteSkuEntries] = useState([]); + const [sites, setSites] = useState([]); const [categories, setCategories] = useState([]); const [activeAttributes, setActiveAttributes] = useState([]); @@ -41,6 +45,10 @@ const EditForm: React.FC<{ productcontrollerGetcategoriesall().then((res: any) => { setCategories(res?.data || []); }); + // 获取站点列表用于站点SKU选择 + sitecontrollerAll().then((res: any) => { + setSites(res?.data || []); + }); }, []); useEffect(() => { @@ -86,6 +94,9 @@ const EditForm: React.FC<{ const { data: componentsData } = await productcontrollerGetproductcomponents({ id: record.id }); setComponents(componentsData || []); + // 获取站点SKU详细信息 + const { data: siteSkusData } = await productcontrollerGetproductsiteskus({ id: record.id }); + setSiteSkuEntries(siteSkusData || []); })(); }, [record]); @@ -106,9 +117,10 @@ const EditForm: React.FC<{ components: components, type: type, categoryId: (record as any).categoryId || (record as any).category?.id, - siteSkus: (record as any).siteSkus?.map((s: any) => s.code) || [], + // 初始化站点SKU列表为对象形式 + siteSkus: siteSkuEntries && siteSkuEntries.length ? siteSkuEntries : [], }; - }, [record, components, type]); + }, [record, components, type, siteSkuEntries]); return ( @@ -211,13 +223,41 @@ const EditForm: React.FC<{ )} - + label="站点SKU" + creatorButtonProps={{ position: 'bottom', creatorButtonText: '新增站点SKU' }} + itemRender={({ listDom, action }) => ( +
+ {listDom} + {action} +
+ )} + > + + ({ label: site.name, value: site.id }))} + placeholder="请选择站点" + rules={[{ required: true, message: '请选择站点' }]} + /> + + + + void; productIds: number[]; + productRows: API.Product[]; onSuccess: () => void; -}> = ({ visible, onClose, productIds, onSuccess }) => { +}> = ({ visible, onClose, productIds, productRows, onSuccess }) => { const { message } = App.useApp(); const [sites, setSites] = useState([]); + const formRef = useRef(); useEffect(() => { if (visible) { @@ -186,6 +190,22 @@ const SyncToSiteModal: React.FC<{ open={visible} onOpenChange={(open) => !open && onClose()} modalProps={{ destroyOnClose: true }} + formRef={formRef} + onValuesChange={(changedValues) => { + if ('siteId' in changedValues && changedValues.siteId) { + const siteId = changedValues.siteId; + const site = sites.find((s: any) => s.id === siteId) || {}; + const prefix = site.skuPrefix || ''; + const map: Record = {}; + productRows.forEach((p) => { + map[p.id] = { + code: `${prefix}${p.sku || ''}`, + quantity: undefined, + }; + }); + formRef.current?.setFieldsValue({ productSiteSkus: map }); + } + }} onFinish={async (values) => { if (!values.siteId) return false; try { @@ -193,6 +213,16 @@ const SyncToSiteModal: React.FC<{ { siteId: values.siteId }, { productIds } ); + const map = values.productSiteSkus || {}; + for (const currentProductId of productIds) { + const entry = map?.[currentProductId]; + if (entry && entry.code) { + await productcontrollerBindproductsiteskus( + { id: currentProductId }, + { siteSkus: [{ siteId: values.siteId, code: entry.code, quantity: entry.quantity }] } + ); + } + } message.success('同步任务已提交'); onSuccess(); return true; @@ -208,6 +238,21 @@ const SyncToSiteModal: React.FC<{ options={sites.map((site) => ({ label: site.name, value: site.id }))} rules={[{ required: true, message: '请选择站点' }]} /> + {productRows.map((row) => ( +
+
原始SKU: {row.sku || '-'}
+ + +
+ ))} ); }; @@ -237,37 +282,46 @@ const WpProductInfo: React.FC<{ skus: string[]; record: API.Product; parentTable , ]} request={async () => { + // 判断是否存在站点SKU列表 if (!skus || skus.length === 0) return { data: [] }; - const { data } = await wpproductcontrollerGetwpproducts( - { - skus, - pageSize: 100, - current: 1, - }, - { - paramsSerializer: (params: any) => { - const searchParams = new URLSearchParams(); - Object.keys(params).forEach((key) => { - const value = params[key]; - if (Array.isArray(value)) { - value.forEach((v) => searchParams.append(key, v)); - } else if (value !== undefined && value !== null) { - searchParams.append(key, value); - } + try { + // 获取所有站点列表用于遍历查询 + const { data: siteResponse } = await sitecontrollerAll(); + const siteList = siteResponse || []; + // 聚合所有站点的产品数据 + const aggregatedProducts: any[] = []; + // 遍历每一个站点 + for (const siteItem of siteList) { + // 遍历每一个SKU在当前站点进行搜索 + for (const skuCode of skus) { + // 直接调用站点API根据搜索关键字获取产品列表 + const { data: productPage } = await siteapicontrollerGetproducts({ + siteId: siteItem.id, + per_page: 100, + search: skuCode, }); - return searchParams.toString(); - }, - }, - ); - return { - data: data?.items || [], - success: true, - }; + const siteProducts = productPage?.items || []; + // 将站点信息附加到产品数据中便于展示 + siteProducts.forEach((p: any) => { + aggregatedProducts.push({ + ...p, + siteId: siteItem.id, + siteName: siteItem.name, + }); + }); + } + } + return { data: aggregatedProducts, success: true }; + } catch (error: any) { + // 请求失败进行错误提示 + message.error(error?.message || '获取站点产品失败'); + return { data: [], success: false }; + } }} columns={[ { title: '站点', - dataIndex: ['site', 'name'], + dataIndex: 'siteName', }, { title: 'SKU', @@ -699,6 +753,7 @@ const List: React.FC = () => { visible={syncModalVisible} onClose={() => setSyncModalVisible(false)} productIds={syncProductIds} + productRows={selectedRows} onSuccess={() => { setSyncModalVisible(false); setSelectedRows([]); diff --git a/src/pages/Product/Permutation/index.tsx b/src/pages/Product/Permutation/index.tsx index 033ccc9..06f40ca 100644 --- a/src/pages/Product/Permutation/index.tsx +++ b/src/pages/Product/Permutation/index.tsx @@ -296,7 +296,45 @@ const PermutationPage: React.FC = () => { }} scroll={{ x: 'max-content' }} search={false} - toolBarRender={false} + toolBarRender={() => [ + + ]} /> )} diff --git a/src/pages/Site/Shop/Customers/index.tsx b/src/pages/Site/Shop/Customers/index.tsx index 96c7297..7d6e414 100644 --- a/src/pages/Site/Shop/Customers/index.tsx +++ b/src/pages/Site/Shop/Customers/index.tsx @@ -1,6 +1,6 @@ import { ActionType, DrawerForm, ModalForm, PageContainer, ProColumns, ProFormText, ProFormTextArea, ProTable } from '@ant-design/pro-components'; import { request, useParams } from '@umijs/max'; -import { App, Avatar, Button, Popconfirm, Space, Tag } from 'antd'; +import { App, Avatar, Button, Modal, Popconfirm, Space, Tag } from 'antd'; import React, { useRef, useState } from 'react'; import { DeleteFilled, EditOutlined, PlusOutlined, UserOutlined } from '@ant-design/icons'; @@ -62,6 +62,8 @@ const CustomerPage: React.FC = () => { const [editing, setEditing] = useState(null); const [selectedRowKeys, setSelectedRowKeys] = useState([]); const actionRef = useRef(); + const [ordersVisible, setOrdersVisible] = useState(false); + const [ordersCustomer, setOrdersCustomer] = useState(null); const handleDelete = async (id: number) => { if (!siteId) return; @@ -90,9 +92,25 @@ const CustomerPage: React.FC = () => { return } size="large" />; }, }, + { + title: '姓名', + dataIndex: 'name', + hideInTable: true, + }, + { + title: 'ID', + dataIndex: 'id', + hideInSearch: true, + width: 120, + copyable: true, + render: (_, record) => { + return record?.id ?? '-'; + } + }, { title: '姓名', dataIndex: 'username', + hideInSearch: true, render: (_, record) => { // DTO中有first_name和last_name字段,username可能从raw数据中获取 const username = record.username || record.raw?.username || 'N/A'; @@ -126,6 +144,18 @@ const CustomerPage: React.FC = () => { return {role}; }, }, + { + title: '订单数', + dataIndex: 'orders', + sorter: true, + hideInSearch: true, + }, + { + title: '总花费', + dataIndex: 'total_spend', + sorter: true, + hideInSearch: true, + }, { title: '账单地址', dataIndex: 'billing', @@ -143,6 +173,23 @@ const CustomerPage: React.FC = () => { ); }, }, + { + title: '收货地址', + dataIndex: 'shipping', + hideInSearch: true, + render: (_, record) => { + const { shipping } = record; + if (!shipping) return '-'; + return ( +
+
{shipping.address_1} {shipping.address_2}
+
{shipping.city}, {shipping.state}, {shipping.postcode}
+
{shipping.country}
+
{shipping.phone}
+
+ ); + }, + }, { title: '注册时间', dataIndex: 'date_created', @@ -153,12 +200,16 @@ const CustomerPage: React.FC = () => { title: '操作', valueType: 'option', width: 120, + fixed:"right", render: (_, record) => ( ), }, @@ -175,17 +226,36 @@ const CustomerPage: React.FC = () => { { + request={async (params, sort, filter) => { if (!siteId) return { data: [], total: 0, success: true }; + const { current, pageSize, name, email, ...rest } = params || {}; + const where = { ...rest, ...(filter || {}) }; + if (email) { + (where as any).email = email; + } + let orderObj: Record | undefined = undefined; + if (sort && typeof sort === 'object') { + const [field, dir] = Object.entries(sort)[0] || []; + if (field && dir) { + orderObj = { [field]: dir === 'descend' ? 'desc' : 'asc' }; + } + } const response = await request(`/site-api/${siteId}/customers`, { - params: { page: params.current, per_page: params.pageSize }, + params: { + page: current, + page_size: pageSize, + where, + ...(orderObj ? { order: orderObj } : {}), + ...((name || email) ? { search: name || email } : {}), + }, }); if (!response.success) { @@ -198,9 +268,21 @@ const CustomerPage: React.FC = () => { } const data = response.data; + let items = (data?.items || []) as any[]; + if (sort && typeof sort === 'object') { + const [field, dir] = Object.entries(sort)[0] || []; + if (field === 'orders' || field === 'total_spend') { + const isDesc = dir === 'descend'; + items = items.slice().sort((a, b) => { + const av = Number(a?.[field] ?? 0); + const bv = Number(b?.[field] ?? 0); + return isDesc ? bv - av : av - bv; + }); + } + } return { total: data?.total || 0, - data: data?.items || [], + data: items, success: true, }; }} @@ -236,7 +318,8 @@ const CustomerPage: React.FC = () => { title="批量导出" onClick={async () => { if (!siteId) return; - const res = await request(`/site-api/${siteId}/customers/export`, { params: {} }); + const idsParam = selectedRowKeys.length ? (selectedRowKeys as any[]).join(',') : undefined; + const res = await request(`/site-api/${siteId}/customers/export`, { params: { ids: idsParam } }); if (res?.success && res?.data?.csv) { const blob = new Blob([res.data.csv], { type: 'text/csv;charset=utf-8;' }); const url = URL.createObjectURL(blob); @@ -315,6 +398,47 @@ const CustomerPage: React.FC = () => { + { setOrdersVisible(false); setOrdersCustomer(null); }} + footer={null} + width={1000} + title="客户订单" + destroyOnClose + > + { + if (!siteId || !ordersCustomer?.id) return { data: [], total: 0, success: true }; + const res = await request(`/site-api/${siteId}/customers/${ordersCustomer.id}/orders`, { + params: { + page: params.current, + page_size: params.pageSize, + }, + }); + if (!res?.success) { + message.error(res?.message || '获取订单失败'); + return { data: [], total: 0, success: false }; + } + const data = res.data || {}; + return { + data: data.items || [], + total: data.total || 0, + success: true, + }; + }} + /> + ); }; diff --git a/src/pages/Site/Shop/Logistics/index.tsx b/src/pages/Site/Shop/Logistics/index.tsx index 228fa9f..70789c4 100644 --- a/src/pages/Site/Shop/Logistics/index.tsx +++ b/src/pages/Site/Shop/Logistics/index.tsx @@ -209,28 +209,41 @@ const LogisticsPage: React.FC = () => { - + + ); }} diff --git a/src/pages/Site/Shop/Media/index.tsx b/src/pages/Site/Shop/Media/index.tsx index a780ad5..ac820cd 100644 --- a/src/pages/Site/Shop/Media/index.tsx +++ b/src/pages/Site/Shop/Media/index.tsx @@ -54,6 +54,16 @@ const MediaPage: React.FC = () => { }; const columns: ProColumns[] = [ + { + title: 'ID', + dataIndex: 'id', + hideInSearch: true, + width: 120, + copyable: true, + render: (_, record) => { + return record?.id ?? '-'; + } + }, { title: '展示', dataIndex: 'source_url', @@ -136,13 +146,22 @@ const MediaPage: React.FC = () => { actionRef={actionRef} columns={columns} rowSelection={{ selectedRowKeys, onChange: setSelectedRowKeys }} - request={async (params) => { + scroll={{ x: 'max-content' }} + request={async (params, sort) => { if (!siteId) return { data: [], total: 0 }; - + const { current, pageSize } = params || {}; + let orderObj: Record | undefined = undefined; + if (sort && typeof sort === 'object') { + const [field, dir] = Object.entries(sort)[0] || []; + if (field && dir) { + orderObj = { [field]: dir === 'descend' ? 'desc' : 'asc' }; + } + } const response = await request(`/site-api/${siteId}/media`, { params: { - page: params.current, - per_page: params.pageSize, + page: current, + page_size: pageSize, + ...(orderObj ? { order: orderObj } : {}), }, }); @@ -164,7 +183,7 @@ const MediaPage: React.FC = () => { }; }} search={false} - options={false} + options={{ reload: true }} toolBarRender={() => [ { title="批量导出" onClick={async () => { if (!siteId) return; - const res = await request(`/site-api/${siteId}/media/export`, { params: {} }); + const idsParam = selectedRowKeys.length ? (selectedRowKeys as any[]).join(',') : undefined; + const res = await request(`/site-api/${siteId}/media/export`, { params: { ids: idsParam } }); if (res?.success && res?.data?.csv) { const blob = new Blob([res.data.csv], { type: 'text/csv;charset=utf-8;' }); const url = URL.createObjectURL(blob); @@ -233,23 +253,35 @@ const MediaPage: React.FC = () => { } }} />, - - +