feat(字典): 重构字典管理页面使用ProTable并优化功能

refactor(站点): 统一站点名称字段从siteName改为name

fix(产品列表): 修复操作列固定显示问题

refactor(产品工具): 动态加载字典配置并移除硬编码默认值

feat(属性管理): 使用ProTable重构字典项列表并添加复制功能
This commit is contained in:
tikkhun 2025-12-01 15:24:32 +08:00
parent 52e982ba42
commit 9c35ada7b1
7 changed files with 273 additions and 298 deletions

View File

@ -1,5 +1,5 @@
import { UploadOutlined } from '@ant-design/icons';
import { PageContainer } from '@ant-design/pro-components';
import { ActionType, PageContainer, ProTable } from '@ant-design/pro-components';
import { request } from '@umijs/max';
import {
Button,
@ -12,7 +12,7 @@ import {
Upload,
message,
} from 'antd';
import React, { useEffect, useState } from 'react';
import React, { useEffect, useRef, useState } from 'react';
const { Sider, Content } = Layout;
@ -22,250 +22,168 @@ const DictPage: React.FC = () => {
const [loadingDicts, setLoadingDicts] = useState(false);
const [searchText, setSearchText] = useState('');
const [selectedDict, setSelectedDict] = useState<any>(null);
// 右侧字典项列表的状态
const [dictItems, setDictItems] = useState<any[]>([]);
const [loadingDictItems, setLoadingDictItems] = useState(false);
// 控制添加字典模态框的显示
const [isAddDictModalVisible, setIsAddDictModalVisible] = useState(false);
const [editingDict, setEditingDict] = useState<any>(null);
const [newDictName, setNewDictName] = useState('');
const [newDictTitle, setNewDictTitle] = useState('');
const [editingDict, setEditingDict] = useState<any>(null);
// 控制字典项模态框的显示
// 右侧字典项列表的状态
const [isDictItemModalVisible, setIsDictItemModalVisible] = useState(false);
const [editingDictItem, setEditingDictItem] = useState<any>(null);
const [dictItemForm] = Form.useForm();
const actionRef = useRef<ActionType>();
// 获取字典列表
const fetchDicts = async (title?: string) => {
const fetchDicts = async (name = '') => {
setLoadingDicts(true);
try {
const res = await request('/dict/list', { params: { title } });
if (res) {
setDicts(res);
}
const res = await request('/dict/list', { params: { name } });
setDicts(res);
} catch (error) {
message.error('获取字典列表失败');
} finally {
setLoadingDicts(false);
}
setLoadingDicts(false);
};
// 获取字典项列表
const fetchDictItems = async (dictId?: number) => {
setLoadingDictItems(true);
try {
const res = await request('/dict/items', { params: { dictId } });
if (res) {
setDictItems(res);
}
} catch (error) {
message.error('获取字典项列表失败');
}
setLoadingDictItems(false);
};
// 组件加载时获取数据
useEffect(() => {
fetchDicts();
fetchDictItems();
}, []);
// 当选择的字典变化时,重新获取字典项
useEffect(() => {
fetchDictItems(selectedDict?.id);
}, [selectedDict]);
// 处理字典搜索
// 搜索字典
const handleSearch = (value: string) => {
fetchDicts(value);
};
// 处理添加字典
// 添加或编辑字典
const handleAddDict = async () => {
if (!newDictName || !newDictTitle) {
message.warning('请输入字典名称和标题');
return;
}
const values = { name: newDictName, title: newDictTitle };
try {
if (editingDict) {
await request(`/dict/${editingDict.id}`, {
method: 'PUT',
data: { name: newDictName, title: newDictTitle },
data: values,
});
message.success('更新成功');
} else {
await request('/dict', {
method: 'POST',
data: { name: newDictName, title: newDictTitle },
});
await request('/dict', { method: 'POST', data: values });
message.success('添加成功');
}
setIsAddDictModalVisible(false);
setEditingDict(null);
setNewDictName('');
setNewDictTitle('');
setEditingDict(null);
fetchDicts(); // 重新获取字典列表
fetchDicts(); // 重新获取列表
} catch (error) {
message.error(editingDict ? '更新失败' : '添加失败');
}
};
const handleEditDict = (dict: any) => {
setEditingDict(dict);
setNewDictName(dict.name);
setNewDictTitle(dict.title);
// 删除字典
const handleDeleteDict = async (id: number) => {
try {
await request(`/dict/${id}`, { method: 'DELETE' });
message.success('删除成功');
fetchDicts();
if (selectedDict?.id === id) {
setSelectedDict(null);
}
} catch (error) {
message.error('删除失败');
}
};
// 编辑字典
const handleEditDict = (record: any) => {
setEditingDict(record);
setNewDictName(record.name);
setNewDictTitle(record.title);
setIsAddDictModalVisible(true);
};
// 打开添加字典项模态框
// 下载字典导入模板
const handleDownloadDictTemplate = () => {
// 创建一个空的 a 标签用于下载
const link = document.createElement('a');
link.href = '/dict/template'; // 指向后端的模板下载接口
link.setAttribute('download', 'dict_template.xlsx');
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
};
// 添加字典项
const handleAddDictItem = () => {
setEditingDictItem(null);
dictItemForm.resetFields();
setIsDictItemModalVisible(true);
};
// 打开编辑字典项模态框
const handleEditDictItem = (item: any) => {
setEditingDictItem(item);
dictItemForm.setFieldsValue(item);
// 编辑字典项
const handleEditDictItem = (record: any) => {
setEditingDictItem(record);
dictItemForm.setFieldsValue(record);
setIsDictItemModalVisible(true);
};
// 处理字典项表单提交(添加/编辑)
const handleDictItemFormSubmit = async (values: any) => {
// 删除字典项
const handleDeleteDictItem = async (id: number) => {
try {
if (editingDictItem) {
// 编辑
await request(`/dict/item/${editingDictItem.id}`, {
method: 'PUT',
data: values,
});
message.success('更新成功');
} else {
// 添加
await request('/dict/item', {
method: 'POST',
data: { ...values, dictId: selectedDict.id },
});
message.success('添加成功');
}
await request(`/dict/item/${id}`, { method: 'DELETE' });
message.success('删除成功');
actionRef.current?.reload();
} catch (error) {
message.error('删除失败');
}
};
// 字典项表单提交
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 } };
try {
await request(url, { method, data });
message.success(editingDictItem ? '更新成功' : '添加成功');
setIsDictItemModalVisible(false);
fetchDictItems(selectedDict.id);
actionRef.current?.reload();
} catch (error) {
message.error(editingDictItem ? '更新失败' : '添加失败');
}
};
// 处理删除字典项
const handleDeleteDictItem = async (itemId: number) => {
try {
await request(`/dict/item/${itemId}`, { method: 'DELETE' });
message.success('删除成功');
fetchDictItems(selectedDict.id);
} catch (error) {
message.error('删除失败');
}
// 下载字典项导入模板
const handleDownloadDictItemTemplate = () => {
const link = document.createElement('a');
link.href = '/dict/item/template';
link.setAttribute('download', 'dict_item_template.xlsx');
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
};
// 处理删除字典
const handleDeleteDict = async (dictId: number) => {
try {
await request(`/dict/${dictId}`, { method: 'DELETE' });
message.success('删除成功');
fetchDicts(); // 重新获取字典列表
// 如果删除的是当前选中的字典,则清空右侧列表
if (selectedDict?.id === dictId) {
setSelectedDict(null);
setDictItems([]);
}
} catch (error) {
message.error('删除失败');
}
};
// Effects
useEffect(() => {
fetchDicts();
}, []);
/**
* @description
*/
const handleDownloadDictTemplate = async () => {
try {
// 使用带有认证拦截的 request 发起下载请求(后端鉴权通过)
const blob = await request('/dict/template', { responseType: 'blob' });
// 创建临时链接并触发下载
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'dict-template.xlsx';
document.body.appendChild(a);
a.click();
a.remove();
window.URL.revokeObjectURL(url);
} catch (error) {
// 错误处理:认证失败或网络错误
message.error('下载模板失败');
}
};
/**
* @description
*/
const handleDownloadDictItemTemplate = async () => {
try {
// 使用带有认证拦截的 request 发起下载请求(后端鉴权通过)
const blob = await request('/dict/item/template', {
responseType: 'blob',
});
// 创建临时链接并触发下载
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'dict-item-template.xlsx';
document.body.appendChild(a);
a.click();
a.remove();
window.URL.revokeObjectURL(url);
} catch (error) {
// 错误处理:认证失败或网络错误
message.error('下载模板失败');
}
};
// 左侧字典列表的列定义
// 左侧字典表格的列定义
const dictColumns = [
{
title: '名称',
dataIndex: 'name',
key: 'name',
},
{
title: '标题',
dataIndex: 'title',
key: 'title',
},
{ title: '字典名称', dataIndex: 'name', key: 'name' },
{
title: '操作',
key: 'action',
render: (_: any, record: any) => (
<Space size="small">
<Button
size="small"
type="link"
onClick={(e) => {
e.stopPropagation();
handleEditDict(record);
}}
>
<Button type="link" size="small" onClick={() => handleEditDict(record)}>
</Button>
<Button
size="small"
type="link"
size="small"
danger
onClick={(e) => {
e.stopPropagation();
handleDeleteDict(record.id);
}}
onClick={() => handleDeleteDict(record.id)}
>
</Button>
@ -280,16 +198,19 @@ const DictPage: React.FC = () => {
title: '名称',
dataIndex: 'name',
key: 'name',
copyable: true,
},
{
title: '标题',
dataIndex: 'title',
key: 'title',
copyable: true,
},
{
title: '中文标题',
dataIndex: 'titleCN',
key: 'titleCN',
copyable: true,
},
{
title: '操作',
@ -324,7 +245,6 @@ const DictPage: React.FC = () => {
>
<Space direction="vertical" style={{ width: '100%' }}>
<Input.Search
size="small"
size="small"
placeholder="搜索字典"
onSearch={handleSearch}
@ -411,7 +331,7 @@ const DictPage: React.FC = () => {
onChange={(info) => {
if (info.file.status === 'done') {
message.success(`${info.file.name} 文件上传成功`);
fetchDictItems(selectedDict.id);
actionRef.current?.reload();
} else if (info.file.status === 'error') {
message.error(`${info.file.name} 文件上传失败`);
}
@ -433,12 +353,37 @@ const DictPage: React.FC = () => {
</Button>
</div>
<Table
dataSource={dictItems}
<ProTable
columns={dictItemColumns}
request={async (params) => {
// 当没有选择字典时,不发起请求
if (!selectedDict?.id) {
return {
data: [],
success: true,
};
}
const { name, title } = params;
const res = await request('/dict/items', {
params: {
dictId: selectedDict?.id,
name,
title,
},
});
return {
data: res,
success: true,
};
}}
rowKey="id"
loading={loadingDictItems}
search={{
layout: 'vertical',
}}
pagination={false}
options={false}
size="small"
key={selectedDict?.id}
/>
</Space>
</Content>

View File

@ -1,5 +1,9 @@
import { UploadOutlined } from '@ant-design/icons';
import { PageContainer } from '@ant-design/pro-components';
import {
ActionType,
PageContainer,
ProTable,
} from '@ant-design/pro-components';
import { request } from '@umijs/max';
import {
Button,
@ -12,7 +16,7 @@ import {
Upload,
message,
} from 'antd';
import React, { useEffect, useState } from 'react';
import React, { useEffect, useRef, useState } from 'react';
const { Sider, Content } = Layout;
@ -25,9 +29,8 @@ const AttributePage: React.FC = () => {
const [searchText, setSearchText] = useState('');
const [selectedDict, setSelectedDict] = useState<any>(null);
// 中文注释:右侧字典项状态
const [dictItems, setDictItems] = useState<any[]>([]);
const [loadingDictItems, setLoadingDictItems] = useState(false);
// 中文注释:右侧字典项 ProTable 的引用
const actionRef = useRef<ActionType>();
// 中文注释:字典项新增/编辑模态框控制
const [isDictItemModalVisible, setIsDictItemModalVisible] = useState(false);
@ -48,29 +51,11 @@ const AttributePage: React.FC = () => {
setLoadingDicts(false);
};
// 中文注释:获取字典项列表
const fetchDictItems = async (dictId?: number) => {
setLoadingDictItems(true);
try {
const res = await request('/dict/items', { params: { dictId } });
setDictItems(res || []);
} catch (error) {
message.error('获取字典项列表失败');
}
setLoadingDictItems(false);
};
// 中文注释:组件挂载时初始化数据
useEffect(() => {
fetchDicts();
fetchDictItems();
}, []);
// 中文注释:当选择的字典发生变化时刷新右侧字典项
useEffect(() => {
fetchDictItems(selectedDict?.id);
}, [selectedDict]);
// 中文注释:搜索触发过滤
const handleSearch = (value: string) => {
fetchDicts(value);
@ -109,7 +94,7 @@ const AttributePage: React.FC = () => {
message.success('添加成功');
}
setIsDictItemModalVisible(false);
fetchDictItems(selectedDict.id);
actionRef.current?.reload(); // 中文注释:刷新 ProTable
} catch (error) {
message.error(editingDictItem ? '更新失败' : '添加失败');
}
@ -120,7 +105,7 @@ const AttributePage: React.FC = () => {
try {
await request(`/dict/item/${itemId}`, { method: 'DELETE' });
message.success('删除成功');
fetchDictItems(selectedDict.id);
actionRef.current?.reload(); // 中文注释:刷新 ProTable
} catch (error) {
message.error('删除失败');
}
@ -133,13 +118,14 @@ const AttributePage: React.FC = () => {
];
// 中文注释:右侧字典项列表列定义(紧凑样式)
const dictItemColumns = [
{ title: '名称', dataIndex: 'name', key: 'name' },
{ title: '标题', dataIndex: 'title', key: 'title' },
{ title: '中文标题', dataIndex: 'titleCN', key: 'titleCN' },
const dictItemColumns: any[] = [
{ title: '名称', dataIndex: 'name', key: 'name', copyable: true },
{ title: '标题', dataIndex: 'title', key: 'title', copyable: true },
{ title: '中文标题', dataIndex: 'titleCN', key: 'titleCN', copyable: true },
{
title: '操作',
key: 'action',
valueType: 'option',
render: (_: any, record: any) => (
<Space size="small">
<Button
@ -206,56 +192,75 @@ const AttributePage: React.FC = () => {
</Space>
</Sider>
<Content style={{ padding: '8px' }}>
<Space direction="vertical" style={{ width: '100%' }} size="small">
<div
style={{
width: '100%',
display: 'flex',
flexDirection: 'row',
gap: '4px',
}}
>
<Button
type="primary"
size="small"
onClick={handleAddDictItem}
disabled={!selectedDict}
>
</Button>
<Upload
name="file"
action={`/dict/item/import`}
data={{ dictId: selectedDict?.id }}
showUploadList={false}
disabled={!selectedDict}
onChange={(info) => {
// 中文注释:条件判断,上传状态处理
if (info.file.status === 'done') {
message.success(`${info.file.name} 文件上传成功`);
fetchDictItems(selectedDict.id);
} else if (info.file.status === 'error') {
message.error(`${info.file.name} 文件上传失败`);
}
}}
>
<ProTable
columns={dictItemColumns}
actionRef={actionRef}
request={async (params) => {
// 中文注释:当没有选择字典时,不发起请求
if (!selectedDict?.id) {
return {
data: [],
success: true,
};
}
const { name, title } = params;
const res = await request('/dict/items', {
params: {
dictId: selectedDict.id,
name,
title,
},
});
return {
data: res,
success: true,
};
}}
rowKey="id"
search={{
layout: 'vertical',
}}
pagination={false}
options={false}
size="small"
key={selectedDict?.id}
headerTitle={
<Space>
<Button
type="primary"
size="small"
icon={<UploadOutlined />}
onClick={handleAddDictItem}
disabled={!selectedDict}
>
</Button>
</Upload>
</div>
<Table
dataSource={dictItems}
columns={dictItemColumns}
rowKey="id"
loading={loadingDictItems}
size="small"
/>
</Space>
<Upload
name="file"
action={`/dict/item/import`}
data={{ dictId: selectedDict?.id }}
showUploadList={false}
disabled={!selectedDict}
onChange={(info) => {
// 中文注释:条件判断,上传状态处理
if (info.file.status === 'done') {
message.success(`${info.file.name} 文件上传成功`);
actionRef.current?.reload();
} else if (info.file.status === 'error') {
message.error(`${info.file.name} 文件上传失败`);
}
}}
>
<Button
size="small"
icon={<UploadOutlined />}
disabled={!selectedDict}
>
</Button>
</Upload>
</Space>
}
/>
</Content>
</Layout>

View File

@ -219,6 +219,7 @@ const List: React.FC = () => {
title: '操作',
dataIndex: 'option',
valueType: 'option',
fixed: true,
render: (_, record) => (
<>
<EditForm record={record} tableRef={actionRef} />

View File

@ -31,15 +31,7 @@ import { useRef } from 'react';
const List: React.FC = () => {
const actionRef = useRef<ActionType>();
const columns: ProColumns<API.WpProductDTO>[] = [
{
title: 'sku',
dataIndex: 'sku',
},
{
title: '名称',
dataIndex: 'name',
},
{
{
title: '站点',
dataIndex: 'siteId',
valueType: 'select',
@ -54,6 +46,15 @@ const List: React.FC = () => {
return record.site?.name;
},
},
{
title: 'sku',
dataIndex: 'sku',
},
{
title: '名称',
dataIndex: 'name',
},
{
title: '产品状态',

View File

@ -5,8 +5,9 @@ import {
ProFormSelect,
} from '@ant-design/pro-components';
import { Button, Card, Col, Input, message, Row, Upload } from 'antd';
import React, { useState } from 'react';
import React, { useEffect, useState } from 'react';
import * as XLSX from 'xlsx';
import { request } from '@umijs/max';
// 定义配置接口
interface TagConfig {
@ -163,31 +164,6 @@ const computeTags = (name: string, sku: string, config: TagConfig): string => {
return finalTags.join(', ');
};
// 从 Python 脚本中提取的默认配置
const DEFAULT_CONFIG = {
brands: ['YOONE', 'ZYN', 'ZEX', 'JUX', 'WHITE FOX'],
fruitKeys: [
'apple',
'blueberry',
'citrus',
'mango',
'peach',
'grape',
'cherry',
'strawberry',
'watermelon',
'orange',
'lemon',
'lemonade',
'razz',
'pineapple',
'berry',
'fruit',
],
mintKeys: ['mint', 'wintergreen', 'peppermint', 'spearmint', 'menthol'],
nonFlavorTokens: ['slim', 'pouches', 'pouch', 'mini', 'dry'],
};
/**
* @description WordPress CSV Tags
*/
@ -198,6 +174,40 @@ const WpToolPage: React.FC = () => {
const [csvData, setCsvData] = useState<any[]>([]); // 解析后的 CSV 数据
const [processedData, setProcessedData] = useState<any[]>([]); // 处理后待下载的数据
const [isProcessing, setIsProcessing] = useState(false); // 是否正在处理中
const [config, setConfig] = useState<TagConfig>({ // 动态配置
brands: [],
fruitKeys: [],
mintKeys: [],
nonFlavorTokens: [],
});
// 在组件加载时获取字典数据
useEffect(() => {
const fetchDictItems = async (name: string) => {
try {
const response = await request('/api/dict/items-by-name', {
params: { name },
});
return response.data.map((item: any) => item.name);
} catch (error) {
message.error(`获取字典 ${name} 失败`);
return [];
}
};
const fetchAllConfigs = async () => {
const [brands, fruitKeys, mintKeys, nonFlavorTokens] = await Promise.all([
fetchDictItems('brand'),
fetchDictItems('fruit'),
fetchDictItems('mint'),
fetchDictItems('non_flavor_tokens'),
]);
setConfig({ brands, fruitKeys, mintKeys, nonFlavorTokens });
form.setFieldsValue({ brands, fruitKeys, mintKeys, nonFlavorTokens });
};
fetchAllConfigs();
}, [form]);
/**
* @description
@ -341,7 +351,7 @@ const WpToolPage: React.FC = () => {
<Card title="1. 配置映射规则">
<ProForm
form={form}
initialValues={DEFAULT_CONFIG}
initialValues={config}
onFinish={handleProcessData}
submitter={false}
>

View File

@ -15,7 +15,7 @@ import React, { useEffect, useRef, useState } from 'react';
// 站点数据项类型(前端不包含密钥字段,后端列表不返回密钥)
interface SiteItem {
id: number;
siteName: string;
name: string;
apiUrl?: string;
type?: 'woocommerce' | 'shopyy';
skuPrefix?: string;
@ -24,7 +24,7 @@ interface SiteItem {
// 创建/更新表单的值类型,包含可选的密钥字段
interface SiteFormValues {
siteName: string;
name: string;
apiUrl?: string;
type?: 'woocommerce' | 'shopyy';
isDisabled?: boolean;
@ -43,7 +43,7 @@ const SiteList: React.FC = () => {
if (!open) return;
if (editing) {
formRef.current?.setFieldsValue({
siteName: editing.siteName,
name: editing.name,
apiUrl: editing.apiUrl,
type: editing.type,
skuPrefix: editing.skuPrefix,
@ -53,7 +53,7 @@ const SiteList: React.FC = () => {
});
} else {
formRef.current?.setFieldsValue({
siteName: undefined,
name: undefined,
apiUrl: undefined,
type: 'woocommerce',
skuPrefix: undefined,
@ -148,13 +148,13 @@ const SiteList: React.FC = () => {
// 表格数据请求
const tableRequest = async (params: Record<string, any>) => {
try {
const { current = 1, pageSize = 10, siteName, type } = params;
const { current = 1, pageSize = 10, name, type } = params;
const resp = await request('/site/list', {
method: 'GET',
params: {
current,
pageSize,
keyword: siteName || undefined,
keyword: name || undefined,
type: type || undefined,
},
});
@ -177,7 +177,7 @@ const SiteList: React.FC = () => {
if (editing) {
const payload: Record<string, any> = {
// 仅提交存在的字段,避免覆盖为 null/空
...(values.siteName ? { siteName: values.siteName } : {}),
...(values.name ? { name: values.name } : {}),
...(values.apiUrl ? { apiUrl: values.apiUrl } : {}),
...(values.type ? { type: values.type } : {}),
...(typeof values.isDisabled === 'boolean'
@ -204,7 +204,7 @@ const SiteList: React.FC = () => {
await request('/site/create', {
method: 'POST',
data: {
siteName: values.siteName,
name: values.name,
apiUrl: values.apiUrl,
type: values.type || 'woocommerce',
consumerKey: values.consumerKey,

13
src/services/api/dict.ts Normal file
View File

@ -0,0 +1,13 @@
// @ts-ignore
import { request } from '@umijs/max';
/**
*
* @param params
*/
export async function getDictItems(params: { name: string }) {
return request('/api/dict/items-by-name', {
method: 'GET',
params,
});
}