From 9c35ada7b1f791a0f31d334155539f06e4a2aa74 Mon Sep 17 00:00:00 2001 From: tikkhun Date: Mon, 1 Dec 2025 15:24:32 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E5=AD=97=E5=85=B8):=20=E9=87=8D=E6=9E=84?= =?UTF-8?q?=E5=AD=97=E5=85=B8=E7=AE=A1=E7=90=86=E9=A1=B5=E9=9D=A2=E4=BD=BF?= =?UTF-8?q?=E7=94=A8ProTable=E5=B9=B6=E4=BC=98=E5=8C=96=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit refactor(站点): 统一站点名称字段从siteName改为name fix(产品列表): 修复操作列固定显示问题 refactor(产品工具): 动态加载字典配置并移除硬编码默认值 feat(属性管理): 使用ProTable重构字典项列表并添加复制功能 --- src/pages/Dict/List/index.tsx | 305 +++++++++++--------------- src/pages/Product/Attribute/index.tsx | 153 ++++++------- src/pages/Product/List/index.tsx | 1 + src/pages/Product/WpList/index.tsx | 19 +- src/pages/Product/WpTool/index.tsx | 64 +++--- src/pages/Site/List/index.tsx | 16 +- src/services/api/dict.ts | 13 ++ 7 files changed, 273 insertions(+), 298 deletions(-) create mode 100644 src/services/api/dict.ts diff --git a/src/pages/Dict/List/index.tsx b/src/pages/Dict/List/index.tsx index 93b925d..6482ab4 100644 --- a/src/pages/Dict/List/index.tsx +++ b/src/pages/Dict/List/index.tsx @@ -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(null); - - // 右侧字典项列表的状态 - const [dictItems, setDictItems] = useState([]); - const [loadingDictItems, setLoadingDictItems] = useState(false); - - // 控制添加字典模态框的显示 const [isAddDictModalVisible, setIsAddDictModalVisible] = useState(false); + const [editingDict, setEditingDict] = useState(null); const [newDictName, setNewDictName] = useState(''); const [newDictTitle, setNewDictTitle] = useState(''); - const [editingDict, setEditingDict] = useState(null); - // 控制字典项模态框的显示 + // 右侧字典项列表的状态 const [isDictItemModalVisible, setIsDictItemModalVisible] = useState(false); const [editingDictItem, setEditingDictItem] = useState(null); const [dictItemForm] = Form.useForm(); + const actionRef = useRef(); // 获取字典列表 - 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) => ( - @@ -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 = () => { > { 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 = () => { 下载模板 - { + // 当没有选择字典时,不发起请求 + 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} /> diff --git a/src/pages/Product/Attribute/index.tsx b/src/pages/Product/Attribute/index.tsx index cd84b59..3580bd8 100644 --- a/src/pages/Product/Attribute/index.tsx +++ b/src/pages/Product/Attribute/index.tsx @@ -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(null); - // 中文注释:右侧字典项状态 - const [dictItems, setDictItems] = useState([]); - const [loadingDictItems, setLoadingDictItems] = useState(false); + // 中文注释:右侧字典项 ProTable 的引用 + const actionRef = useRef(); // 中文注释:字典项新增/编辑模态框控制 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) => ( - { - // 中文注释:条件判断,上传状态处理 - if (info.file.status === 'done') { - message.success(`${info.file.name} 文件上传成功`); - fetchDictItems(selectedDict.id); - } else if (info.file.status === 'error') { - message.error(`${info.file.name} 文件上传失败`); - } - }} - > + { + // 中文注释:当没有选择字典时,不发起请求 + 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={ + - - -
- + { + // 中文注释:条件判断,上传状态处理 + if (info.file.status === 'done') { + message.success(`${info.file.name} 文件上传成功`); + actionRef.current?.reload(); + } else if (info.file.status === 'error') { + message.error(`${info.file.name} 文件上传失败`); + } + }} + > + + + + } + /> diff --git a/src/pages/Product/List/index.tsx b/src/pages/Product/List/index.tsx index 4137825..c6d04c1 100644 --- a/src/pages/Product/List/index.tsx +++ b/src/pages/Product/List/index.tsx @@ -219,6 +219,7 @@ const List: React.FC = () => { title: '操作', dataIndex: 'option', valueType: 'option', + fixed: true, render: (_, record) => ( <> diff --git a/src/pages/Product/WpList/index.tsx b/src/pages/Product/WpList/index.tsx index 7aa6d80..6c1bfa4 100644 --- a/src/pages/Product/WpList/index.tsx +++ b/src/pages/Product/WpList/index.tsx @@ -31,15 +31,7 @@ import { useRef } from 'react'; const List: React.FC = () => { const actionRef = useRef(); const columns: ProColumns[] = [ - { - 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: '产品状态', diff --git a/src/pages/Product/WpTool/index.tsx b/src/pages/Product/WpTool/index.tsx index b401748..b1d4860 100644 --- a/src/pages/Product/WpTool/index.tsx +++ b/src/pages/Product/WpTool/index.tsx @@ -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([]); // 解析后的 CSV 数据 const [processedData, setProcessedData] = useState([]); // 处理后待下载的数据 const [isProcessing, setIsProcessing] = useState(false); // 是否正在处理中 + const [config, setConfig] = useState({ // 动态配置 + 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 = () => { diff --git a/src/pages/Site/List/index.tsx b/src/pages/Site/List/index.tsx index 7a7f870..9883df1 100644 --- a/src/pages/Site/List/index.tsx +++ b/src/pages/Site/List/index.tsx @@ -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) => { 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 = { // 仅提交存在的字段,避免覆盖为 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, diff --git a/src/services/api/dict.ts b/src/services/api/dict.ts new file mode 100644 index 0000000..4ef5d84 --- /dev/null +++ b/src/services/api/dict.ts @@ -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, + }); +}