zksu
/
WEB
forked from yoone/WEB
1
0
Fork 0

feat(站点管理): 添加批量编辑功能并优化区域选择

增加站点批量编辑功能,支持同时修改多个站点的区域、关联仓库和禁用状态
使用i18n-iso-countries库替换原有区域选择逻辑,支持中文显示国家名称
优化站点列表查询参数处理,添加分页和排序支持
修复产品创建表单中属性组装逻辑,根据产品类型决定是否组装attributes
This commit is contained in:
tikkhun 2026-01-04 21:01:18 +08:00
parent 6d97ecbcc7
commit e23b0b3e39
8 changed files with 326 additions and 127 deletions

View File

@ -91,7 +91,9 @@ const HistoryOrders: React.FC<HistoryOrdersProps> = ({ customer, siteId }) => {
setLoading(true); setLoading(true);
try { try {
const response = await ordercontrollerGetorders({ const response = await ordercontrollerGetorders({
where: {
customer_email: customer.email, customer_email: customer.email,
},
}); });
if (response) { if (response) {

View File

@ -152,6 +152,7 @@ const CustomerList: React.FC = () => {
{ {
title: '原始 ID', title: '原始 ID',
dataIndex: 'origin_id', dataIndex: 'origin_id',
sorter: true,
}, },
{ {
title: '站点', title: '站点',
@ -172,10 +173,6 @@ const CustomerList: React.FC = () => {
return []; return [];
} }
}, },
render(_, record) {
// console.log(`siteId`, record.site_id);
return <span>{getSiteName(record.site_id) || '-'}</span>;
},
}, },
{ {
title: '头像', title: '头像',
@ -209,16 +206,17 @@ const CustomerList: React.FC = () => {
title: '用户名', title: '用户名',
dataIndex: 'username', dataIndex: 'username',
hideInSearch: true, hideInSearch: true,
sorter: true,
}, },
{ {
title: '邮箱', title: '邮箱',
dataIndex: 'email', dataIndex: 'email',
copyable: true, copyable: true,
sorter: true,
}, },
{ {
title: '电话', title: '电话',
dataIndex: 'phone', dataIndex: 'phone',
hideInSearch: true,
copyable: true, copyable: true,
}, },
{ {
@ -240,6 +238,7 @@ const CustomerList: React.FC = () => {
dataIndex: 'rate', dataIndex: 'rate',
width: 120, width: 120,
hideInSearch: true, hideInSearch: true,
sorter: true,
render: (_, record) => { render: (_, record) => {
return ( return (
<Rate <Rate
@ -345,55 +344,25 @@ const CustomerList: React.FC = () => {
headerTitle="查询表格" headerTitle="查询表格"
actionRef={actionRef} actionRef={actionRef}
rowKey="id" rowKey="id"
request={async (params, sorter) => { request={async (params, sorter,filter) => {
// 获取排序字段和排序方向 console.log('custoemr request',params, sorter,filter)
const key = Object.keys(sorter)[0]; const { current, pageSize, ...restParams } = params;
const orderBy:any = {}
// 构建过滤条件对象 Object.entries(sorter).forEach(([key, value]) => {
const where: any = {}; orderBy[key] = value === 'ascend' ? 'asc' : 'desc';
})
// 添加邮箱过滤
if (params.email) {
where.email = params.email;
}
// 添加站点ID过滤
if (params.site_id) {
where.site_id = params.site_id;
}
// 添加用户名过滤
if (params.username) {
where.username = params.username;
}
// 添加电话过滤
if (params.phone) {
where.phone = params.phone;
}
// 构建查询参数 // 构建查询参数
const queryParams: any = { const queryParams: any = {
page: params.current || 1, page: current || 1,
per_page: params.pageSize || 20, per_page: pageSize || 20,
where: {
...filter,
...restParams
},
orderBy
}; };
// 添加搜索关键词 const result = await customercontrollerGetcustomerlist({params: queryParams});
if (params.fullname) {
queryParams.search = params.fullname;
}
// 添加过滤条件(只有在有过滤条件时才添加)
if (Object.keys(where).length > 0) {
queryParams.where = where;
}
// 添加排序
if (key) {
queryParams.orderBy = { [key]: sorter[key] as 'asc' | 'desc' };
}
const result = await customercontrollerGetcustomerlist(queryParams);
console.log(queryParams, result); console.log(queryParams, result);
return { return {
total: result?.data?.total || 0, total: result?.data?.total || 0,

View File

@ -3,6 +3,7 @@ import {
productcontrollerCreateproduct, productcontrollerCreateproduct,
productcontrollerGetcategoriesall, productcontrollerGetcategoriesall,
productcontrollerGetcategoryattributes, productcontrollerGetcategoryattributes,
productcontrollerGetproductlist,
} from '@/servers/api/product'; } from '@/servers/api/product';
import { stockcontrollerGetstocks as getStocks } from '@/servers/api/stock'; import { stockcontrollerGetstocks as getStocks } from '@/servers/api/stock';
import { templatecontrollerRendertemplate } from '@/servers/api/template'; import { templatecontrollerRendertemplate } from '@/servers/api/template';
@ -200,8 +201,12 @@ const CreateForm: React.FC<{
} }
}} }}
onFinish={async (values: any) => { onFinish={async (values: any) => {
// 组装 attributes(根据 activeAttributes 动态组装) // 根据产品类型决定是否组装 attributes
const attributes = activeAttributes.flatMap((attr: any) => { // 如果产品类型为 bundle则 attributes 为空数组
// 如果产品类型为 single则根据 activeAttributes 动态组装 attributes
const attributes = values.type === 'bundle'
? []
: activeAttributes.flatMap((attr: any) => {
const dictName = attr.name; const dictName = attr.name;
const key = `${dictName}Values`; const key = `${dictName}Values`;
const vals = values[key]; const vals = values[key];
@ -315,15 +320,18 @@ const CreateForm: React.FC<{
rules={[{ required: true, message: '请输入子产品SKU' }]} rules={[{ required: true, message: '请输入子产品SKU' }]}
request={async ({ keyWords }) => { request={async ({ keyWords }) => {
const params = keyWords const params = keyWords
? { sku: keyWords, name: keyWords } ? { sku: keyWords, name: keyWords, type: 'single' }
: { pageSize: 9999 }; : { pageSize: 9999, type: 'single' };
const { data } = await getStocks(params as any); const { data } = await productcontrollerGetproductlist(
params as any,
);
if (!data || !data.items) { if (!data || !data.items) {
return []; return [];
} }
// 只返回类型为单品的产品
return data.items return data.items
.filter((item) => item.sku) .filter((item: any) => item.type === 'single' && item.sku)
.map((item) => ({ .map((item: any) => ({
label: `${item.sku} - ${item.name}`, label: `${item.sku} - ${item.name}`,
value: item.sku, value: item.sku,
})); }));

View File

@ -6,10 +6,13 @@ import {
sitecontrollerUpdate, sitecontrollerUpdate,
} from '@/servers/api/site'; } from '@/servers/api/site';
import { subscriptioncontrollerSync } from '@/servers/api/subscription'; import { subscriptioncontrollerSync } from '@/servers/api/subscription';
import { ActionType, ProColumns, ProTable } from '@ant-design/pro-components'; import { stockcontrollerGetallstockpoints } from '@/servers/api/stock';
import { Button, message, notification, Popconfirm, Space, Tag } from 'antd'; import { ActionType, ProColumns, ProTable, DrawerForm, ProFormSelect, ProFormSwitch } from '@ant-design/pro-components';
import React, { useRef, useState } from 'react'; import { Button, message, notification, Popconfirm, Space, Tag, Form } from 'antd';
import React, { useRef, useState, useEffect } from 'react';
import EditSiteForm from '../Shop/EditSiteForm'; // 引入重构后的表单组件 import EditSiteForm from '../Shop/EditSiteForm'; // 引入重构后的表单组件
import * as countries from 'i18n-iso-countries';
import zhCN from 'i18n-iso-countries/langs/zh';
// 区域数据项类型 // 区域数据项类型
interface AreaItem { interface AreaItem {
@ -42,6 +45,10 @@ const SiteList: React.FC = () => {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [editing, setEditing] = useState<SiteItem | null>(null); const [editing, setEditing] = useState<SiteItem | null>(null);
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]); const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
const [batchEditOpen, setBatchEditOpen] = useState(false);
const [batchEditForm] = Form.useForm();
countries.registerLocale(zhCN);
const handleSync = async (ids: number[]) => { const handleSync = async (ids: number[]) => {
if (!ids.length) return; if (!ids.length) return;
@ -96,6 +103,68 @@ const SiteList: React.FC = () => {
} }
}; };
// 获取所有国家/地区的选项
const getCountryOptions = () => {
// 获取所有国家的 ISO 代码
const countryCodes = countries.getAlpha2Codes();
// 将国家代码转换为选项数组
return Object.keys(countryCodes).map((code) => ({
label: countries.getName(code, 'zh') || code, // 使用中文名称, 如果没有则使用代码
value: code,
}));
};
// 处理批量编辑提交
const handleBatchEditFinish = async (values: any) => {
if (!selectedRowKeys.length) return;
const hide = message.loading('正在批量更新...', 0);
try {
// 遍历所有选中的站点 ID
for (const id of selectedRowKeys) {
// 构建更新数据对象,只包含用户填写了值的字段
const updateData: any = {};
// 如果用户选择了区域,则更新区域
if (values.areas && values.areas.length > 0) {
updateData.areas = values.areas;
}
// 如果用户选择了仓库,则更新仓库
if (values.stockPointIds && values.stockPointIds.length > 0) {
updateData.stockPointIds = values.stockPointIds;
}
// 如果用户设置了禁用状态,则更新状态
if (values.isDisabled !== undefined) {
updateData.isDisabled = values.isDisabled;
}
// 如果有需要更新的字段,则调用更新接口
if (Object.keys(updateData).length > 0) {
await sitecontrollerUpdate({ id: String(id) }, updateData);
}
}
hide();
message.success('批量更新成功');
setBatchEditOpen(false);
setSelectedRowKeys([]);
batchEditForm.resetFields();
actionRef.current?.reload();
} catch (error: any) {
hide();
message.error(error.message || '批量更新失败');
}
};
// 当批量编辑弹窗打开时,重置表单
useEffect(() => {
if (batchEditOpen) {
batchEditForm.resetFields();
}
}, [batchEditOpen, batchEditForm]);
// 表格列定义 // 表格列定义
const columns: ProColumns<SiteItem>[] = [ const columns: ProColumns<SiteItem>[] = [
{ {
@ -122,23 +191,42 @@ const SiteList: React.FC = () => {
{ {
title: 'SKU 前缀', title: 'SKU 前缀',
dataIndex: 'skuPrefix', dataIndex: 'skuPrefix',
width: 160,
hideInSearch: true, hideInSearch: true,
}, },
{ {
title: '平台', title: '平台',
dataIndex: 'type', dataIndex: 'type',
width: 140,
valueType: 'select', valueType: 'select',
request: async () => [ request: async () => [
{ label: 'WooCommerce', value: 'woocommerce' }, { label: 'WooCommerce', value: 'woocommerce' },
{ label: 'Shopyy', value: 'shopyy' }, { label: 'Shopyy', value: 'shopyy' },
], ],
}, },
{
// 地区列配置
title: "地区",
dataIndex: "areas",
hideInSearch: true,
render: (_, row) => {
// 如果没有关联地区,显示"全局"标签
if (!row.areas || row.areas.length === 0) {
return <Tag color="default"></Tag>;
}
// 遍历显示所有关联的地区名称
return (
<Space wrap>
{row.areas.map((area) => (
<Tag color="geekblue" key={area.code}>
{area.name}
</Tag>
))}
</Space>
);
},
},
{ {
title: '关联仓库', title: '关联仓库',
dataIndex: 'stockPoints', dataIndex: 'stockPoints',
width: 200,
hideInSearch: true, hideInSearch: true,
render: (_, row) => { render: (_, row) => {
if (!row.stockPoints || row.stockPoints.length === 0) { if (!row.stockPoints || row.stockPoints.length === 0) {
@ -267,6 +355,12 @@ const SiteList: React.FC = () => {
selectedRowKeys, selectedRowKeys,
onChange: setSelectedRowKeys, onChange: setSelectedRowKeys,
}} }}
pagination={{
defaultPageSize: 20,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (total) => `${total}`,
}}
toolBarRender={() => [ toolBarRender={() => [
<Button <Button
type="primary" type="primary"
@ -277,6 +371,12 @@ const SiteList: React.FC = () => {
> >
</Button>, </Button>,
<Button
disabled={!selectedRowKeys.length}
onClick={() => setBatchEditOpen(true)}
>
</Button>,
<Button <Button
disabled={!selectedRowKeys.length} disabled={!selectedRowKeys.length}
onClick={() => handleSync(selectedRowKeys as number[])} onClick={() => handleSync(selectedRowKeys as number[])}
@ -298,6 +398,51 @@ const SiteList: React.FC = () => {
isEdit={!!editing} isEdit={!!editing}
onFinish={handleFinish} onFinish={handleFinish}
/> />
{/* 批量编辑弹窗 */}
<DrawerForm
title={`批量编辑站点 (${selectedRowKeys.length} 个)`}
form={batchEditForm}
open={batchEditOpen}
onOpenChange={setBatchEditOpen}
onFinish={handleBatchEditFinish}
layout="vertical"
>
<ProFormSelect
name="areas"
label="区域"
mode="multiple"
placeholder="请选择区域(留空表示不修改)"
showSearch
filterOption={(input, option) =>
(option?.label ?? '').toLowerCase().includes(input.toLowerCase())
}
options={getCountryOptions()}
/>
<ProFormSelect
name="stockPointIds"
label="关联仓库"
mode="multiple"
placeholder="请选择关联仓库(留空表示不修改)"
request={async () => {
// 从后端接口获取仓库数据
const res = await stockcontrollerGetallstockpoints();
// 使用可选链和空值合并运算符来安全地处理可能未定义的数据
return (
res?.data?.map((sp: any) => ({ label: sp.name, value: sp.id })) ??
[]
);
}}
/>
<ProFormSwitch
name="isDisabled"
label="是否禁用"
fieldProps={{
checkedChildren: '是',
unCheckedChildren: '否',
}}
/>
</DrawerForm>
</> </>
); );
}; };

View File

@ -10,6 +10,8 @@ import {
} from '@ant-design/pro-components'; } from '@ant-design/pro-components';
import { Form } from 'antd'; import { Form } from 'antd';
import React, { useEffect } from 'react'; import React, { useEffect } from 'react';
import * as countries from 'i18n-iso-countries';
import zhCN from 'i18n-iso-countries/langs/zh';
// 定义组件的 props 类型 // 定义组件的 props 类型
interface EditSiteFormProps { interface EditSiteFormProps {
@ -29,6 +31,9 @@ const EditSiteForm: React.FC<EditSiteFormProps> = ({
}) => { }) => {
const [form] = Form.useForm(); const [form] = Form.useForm();
// 初始化中文语言包
countries.registerLocale(zhCN);
// 当 initialValues 或 open 状态变化时, 更新表单的值 // 当 initialValues 或 open 状态变化时, 更新表单的值
useEffect(() => { useEffect(() => {
// 如果抽屉是打开的 // 如果抽屉是打开的
@ -36,8 +41,10 @@ const EditSiteForm: React.FC<EditSiteFormProps> = ({
// 如果是编辑模式并且有初始值 // 如果是编辑模式并且有初始值
if (isEdit && initialValues) { if (isEdit && initialValues) {
// 编辑模式下, 设置表单值为初始值 // 编辑模式下, 设置表单值为初始值
const { token, consumerKey, consumerSecret, ...safeInitialValues } = initialValues;
// 清空敏感字段, 让用户输入最新的数据
form.setFieldsValue({ form.setFieldsValue({
...initialValues, ...safeInitialValues,
isDisabled: initialValues.isDisabled === 1, // 将后端的 1/0 转换成 true/false isDisabled: initialValues.isDisabled === 1, // 将后端的 1/0 转换成 true/false
}); });
} else { } else {
@ -47,6 +54,17 @@ const EditSiteForm: React.FC<EditSiteFormProps> = ({
} }
}, [initialValues, isEdit, open, form]); }, [initialValues, isEdit, open, form]);
// 获取所有国家/地区的选项
const getCountryOptions = () => {
// 获取所有国家的 ISO 代码
const countryCodes = countries.getAlpha2Codes();
// 将国家代码转换为选项数组
return Object.keys(countryCodes).map((code) => ({
label: countries.getName(code, 'zh') || code, // 使用中文名称, 如果没有则使用代码
value: code,
}));
};
return ( return (
<DrawerForm <DrawerForm
title={isEdit ? '编辑站点' : '新建站点'} title={isEdit ? '编辑站点' : '新建站点'}
@ -146,15 +164,11 @@ const EditSiteForm: React.FC<EditSiteFormProps> = ({
label="区域" label="区域"
mode="multiple" mode="multiple"
placeholder="请选择区域" placeholder="请选择区域"
request={async () => { showSearch
// 从后端接口获取区域数据 filterOption={(input, option) =>
const res = await areacontrollerGetarealist({ pageSize: 1000 }); (option?.label ?? '').toLowerCase().includes(input.toLowerCase())
// areacontrollerGetarealist 直接返回数组, 所以不需要 .data.list }
return res.map((area: any) => ({ options={getCountryOptions()}
label: area.name,
value: area.code,
}));
}}
/> />
<ProFormSelect <ProFormSelect
name="stockPointIds" name="stockPointIds"

View File

@ -15,9 +15,13 @@ import {
ProFormText, ProFormText,
ProTable, ProTable,
} from '@ant-design/pro-components'; } from '@ant-design/pro-components';
import { request } from '@umijs/max';
import { App, Button, Divider, Popconfirm, Space, Tag } from 'antd'; import { App, Button, Divider, Popconfirm, Space, Tag } from 'antd';
import { useRef } from 'react'; import { useRef } from 'react';
import * as countries from 'i18n-iso-countries';
import zhCN from 'i18n-iso-countries/langs/zh';
// 初始化中文语言包
countries.registerLocale(zhCN);
// 区域数据项类型 // 区域数据项类型
interface AreaItem { interface AreaItem {
@ -25,6 +29,17 @@ interface AreaItem {
name: string; name: string;
} }
// 获取所有国家/地区的选项
const getCountryOptions = () => {
// 获取所有国家的 ISO 代码
const countryCodes = countries.getAlpha2Codes();
// 将国家代码转换为选项数组
return Object.keys(countryCodes).map((code) => ({
label: countries.getName(code, 'zh') || code, // 使用中文名称, 如果没有则使用代码
value: code,
}));
};
const ListPage: React.FC = () => { const ListPage: React.FC = () => {
const { message } = App.useApp(); const { message } = App.useApp();
const actionRef = useRef<ActionType>(); const actionRef = useRef<ActionType>();
@ -190,23 +205,11 @@ const CreateForm: React.FC<{
width="lg" width="lg"
mode="multiple" mode="multiple"
placeholder="留空表示全球" placeholder="留空表示全球"
request={async () => { showSearch
try { filterOption={(input, option) =>
const resp = await request('/area', { (option?.label ?? '').toLowerCase().includes(input.toLowerCase())
method: 'GET',
params: { pageSize: 1000 },
});
if (resp.success) {
return resp.data.list.map((area: AreaItem) => ({
label: area.name,
value: area.code,
}));
} }
return []; options={getCountryOptions()}
} catch (e) {
return [];
}
}}
/> />
</ProForm.Group> </ProForm.Group>
</DrawerForm> </DrawerForm>
@ -289,23 +292,11 @@ const UpdateForm: React.FC<{
width="lg" width="lg"
mode="multiple" mode="multiple"
placeholder="留空表示全球" placeholder="留空表示全球"
request={async () => { showSearch
try { filterOption={(input, option) =>
const resp = await request('/area', { (option?.label ?? '').toLowerCase().includes(input.toLowerCase())
method: 'GET',
params: { pageSize: 1000 },
});
if (resp.success) {
return resp.data.list.map((area: AreaItem) => ({
label: area.name,
value: area.code,
}));
} }
return []; options={getCountryOptions()}
} catch (e) {
return [];
}
}}
/> />
</ProForm.Group> </ProForm.Group>
</DrawerForm> </DrawerForm>

View File

@ -59,9 +59,16 @@ export async function sitecontrollerGet(
} }
/** 此处后端没有提供注释 GET /site/list */ /** 此处后端没有提供注释 GET /site/list */
export async function sitecontrollerList(options?: { [key: string]: any }) { export async function sitecontrollerList(
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
params: API.sitecontrollerListParams,
options?: { [key: string]: any },
) {
return request<any>('/site/list', { return request<any>('/site/list', {
method: 'GET', method: 'GET',
params: {
...params,
},
...(options || {}), ...(options || {}),
}); });
} }

View File

@ -302,6 +302,24 @@ declare namespace API {
}; };
type CreateSiteDTO = { type CreateSiteDTO = {
/** 站点 API URL */
apiUrl?: string;
/** 站点网站 URL */
websiteUrl?: string;
/** 站点 REST Key */
consumerKey?: string;
/** 站点 REST 秘钥 */
consumerSecret?: string;
/** 访问令牌 */
token?: string;
/** 站点名称 */
name?: string;
/** 站点描述 */
description?: string;
/** 平台类型 */
type?: 'woocommerce' | 'shopyy';
/** SKU 前缀 */
skuPrefix?: string;
/** 区域 */ /** 区域 */
areas?: any; areas?: any;
/** 绑定仓库ID列表 */ /** 绑定仓库ID列表 */
@ -426,7 +444,10 @@ declare namespace API {
id: number; id: number;
}; };
type DisableSiteDTO = {}; type DisableSiteDTO = {
/** 是否禁用 */
disabled?: boolean;
};
type FulfillmentDTO = { type FulfillmentDTO = {
/** 物流单号 */ /** 物流单号 */
@ -1399,7 +1420,18 @@ declare namespace API {
isActive?: boolean; isActive?: boolean;
}; };
type QuerySiteDTO = {}; type QuerySiteDTO = {
/** 当前页码 */
current?: number;
/** 每页数量 */
pageSize?: number;
/** 搜索关键词 */
keyword?: string;
/** 是否禁用 */
isDisabled?: boolean;
/** 站点ID列表逗号分隔 */
ids?: string;
};
type QueryStockDTO = { type QueryStockDTO = {
/** 页码 */ /** 页码 */
@ -2050,6 +2082,19 @@ declare namespace API {
id: string; id: string;
}; };
type sitecontrollerListParams = {
/** 当前页码 */
current?: number;
/** 每页数量 */
pageSize?: number;
/** 搜索关键词 */
keyword?: string;
/** 是否禁用 */
isDisabled?: boolean;
/** 站点ID列表逗号分隔 */
ids?: string;
};
type sitecontrollerUpdateParams = { type sitecontrollerUpdateParams = {
id: string; id: string;
}; };
@ -3080,6 +3125,24 @@ declare namespace API {
}; };
type UpdateSiteDTO = { type UpdateSiteDTO = {
/** 站点 API URL */
apiUrl?: string;
/** 站点 REST Key */
consumerKey?: string;
/** 站点 REST 秘钥 */
consumerSecret?: string;
/** 访问令牌 */
token?: string;
/** 站点名称 */
name?: string;
/** 站点描述 */
description?: string;
/** 是否禁用 */
isDisabled?: boolean;
/** 平台类型 */
type?: 'woocommerce' | 'shopyy';
/** SKU 前缀 */
skuPrefix?: string;
/** 区域 */ /** 区域 */
areas?: any; areas?: any;
/** 绑定仓库ID列表 */ /** 绑定仓库ID列表 */