feat(产品): 新增分类管理页面并优化产品属性关联
refactor(属性): 移除category属性并优化表单组件 feat(同步): 添加商品同步页面和接口调整 refactor(区域): 简化区域API和类型定义 style(布局): 调整属性页面布局和滚动样式
This commit is contained in:
parent
ce23b66885
commit
c66db9b984
|
|
@ -114,7 +114,12 @@ export default defineConfig({
|
|||
path: '/product/attribute',
|
||||
component: './Product/Attribute',
|
||||
},
|
||||
|
||||
// sync
|
||||
{
|
||||
name: '同步商品',
|
||||
path: '/product/sync',
|
||||
component: './Product/Sync',
|
||||
},
|
||||
{
|
||||
name: 'WP商品列表',
|
||||
path: '/product/wp_list',
|
||||
|
|
|
|||
|
|
@ -0,0 +1,291 @@
|
|||
import { PlusOutlined } from '@ant-design/icons';
|
||||
import {
|
||||
PageContainer,
|
||||
} from '@ant-design/pro-components';
|
||||
import { request } from '@umijs/max';
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
Form,
|
||||
Input,
|
||||
Layout,
|
||||
List,
|
||||
Modal,
|
||||
Popconfirm,
|
||||
Select,
|
||||
message,
|
||||
} from 'antd';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import {
|
||||
productcontrollerCreatecategory,
|
||||
productcontrollerCreatecategoryattribute,
|
||||
productcontrollerDeletecategory,
|
||||
productcontrollerDeletecategoryattribute,
|
||||
productcontrollerGetcategoriesall,
|
||||
productcontrollerGetcategoryattributes,
|
||||
productcontrollerUpdatecategory,
|
||||
} from '@/servers/api/product';
|
||||
import { attributes } from '../Product/Attribute/consts';
|
||||
|
||||
const { Sider, Content } = Layout;
|
||||
|
||||
const CategoryPage: React.FC = () => {
|
||||
const [categories, setCategories] = useState<any[]>([]);
|
||||
const [loadingCategories, setLoadingCategories] = useState(false);
|
||||
const [selectedCategory, setSelectedCategory] = useState<any>(null);
|
||||
const [categoryAttributes, setCategoryAttributes] = useState<any[]>([]);
|
||||
const [loadingAttributes, setLoadingAttributes] = useState(false);
|
||||
|
||||
const [isCategoryModalVisible, setIsCategoryModalVisible] = useState(false);
|
||||
const [categoryForm] = Form.useForm();
|
||||
const [editingCategory, setEditingCategory] = useState<any>(null);
|
||||
|
||||
const [isAttributeModalVisible, setIsAttributeModalVisible] = useState(false);
|
||||
const [availableDicts, setAvailableDicts] = useState<any[]>([]);
|
||||
const [selectedDictIds, setSelectedDictIds] = useState<number[]>([]);
|
||||
|
||||
const fetchCategories = async () => {
|
||||
setLoadingCategories(true);
|
||||
try {
|
||||
const res = await productcontrollerGetcategoriesall();
|
||||
setCategories(res || []);
|
||||
} catch (error) {
|
||||
message.error('获取分类列表失败');
|
||||
}
|
||||
setLoadingCategories(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchCategories();
|
||||
}, []);
|
||||
|
||||
const fetchCategoryAttributes = async (categoryId: number) => {
|
||||
setLoadingAttributes(true);
|
||||
try {
|
||||
const res = await productcontrollerGetcategoryattributes({ categoryItemId: categoryId });
|
||||
setCategoryAttributes(res || []);
|
||||
} catch (error) {
|
||||
message.error('获取分类属性失败');
|
||||
}
|
||||
setLoadingAttributes(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedCategory) {
|
||||
fetchCategoryAttributes(selectedCategory.id);
|
||||
} else {
|
||||
setCategoryAttributes([]);
|
||||
}
|
||||
}, [selectedCategory]);
|
||||
|
||||
const handleCategorySubmit = async (values: any) => {
|
||||
try {
|
||||
if (editingCategory) {
|
||||
await productcontrollerUpdatecategory({ id: editingCategory.id }, values);
|
||||
message.success('更新成功');
|
||||
} else {
|
||||
await productcontrollerCreatecategory(values);
|
||||
message.success('创建成功');
|
||||
}
|
||||
setIsCategoryModalVisible(false);
|
||||
fetchCategories();
|
||||
} catch (error: any) {
|
||||
message.error(error.message || '操作失败');
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteCategory = async (id: number) => {
|
||||
try {
|
||||
await productcontrollerDeletecategory({ id });
|
||||
message.success('删除成功');
|
||||
if (selectedCategory?.id === id) {
|
||||
setSelectedCategory(null);
|
||||
}
|
||||
fetchCategories();
|
||||
} catch (error: any) {
|
||||
message.error(error.message || '删除失败');
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddAttribute = async () => {
|
||||
// Fetch all dicts and filter those that are allowed attributes
|
||||
try {
|
||||
const res = await request('/dict/list');
|
||||
const filtered = (res || []).filter((d: any) => attributes.has(d.name));
|
||||
// Filter out already added attributes
|
||||
const existingDictIds = new Set(categoryAttributes.map((ca: any) => ca.dict.id));
|
||||
const available = filtered.filter((d: any) => !existingDictIds.has(d.id));
|
||||
setAvailableDicts(available);
|
||||
setSelectedDictIds([]);
|
||||
setIsAttributeModalVisible(true);
|
||||
} catch (error) {
|
||||
message.error('获取属性字典失败');
|
||||
}
|
||||
};
|
||||
|
||||
const handleAttributeSubmit = async () => {
|
||||
if (selectedDictIds.length === 0) {
|
||||
message.warning('请选择属性');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await productcontrollerCreatecategoryattribute({
|
||||
categoryItemId: selectedCategory.id,
|
||||
attributeDictIds: selectedDictIds,
|
||||
});
|
||||
message.success('添加属性成功');
|
||||
setIsAttributeModalVisible(false);
|
||||
fetchCategoryAttributes(selectedCategory.id);
|
||||
} catch (error: any) {
|
||||
message.error(error.message || '添加失败');
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteAttribute = async (id: number) => {
|
||||
try {
|
||||
await productcontrollerDeletecategoryattribute({ id });
|
||||
message.success('移除属性成功');
|
||||
fetchCategoryAttributes(selectedCategory.id);
|
||||
} catch (error: any) {
|
||||
message.error(error.message || '移除失败');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
<Layout style={{ background: '#fff', height: 'calc(100vh - 200px)' }}>
|
||||
<Sider width={300} style={{ background: '#fff', borderRight: '1px solid #f0f0f0', padding: '16px' }}>
|
||||
<div style={{ marginBottom: 16, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<span style={{ fontWeight: 'bold' }}>分类列表</span>
|
||||
<Button
|
||||
type="primary"
|
||||
size="small"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => {
|
||||
setEditingCategory(null);
|
||||
categoryForm.resetFields();
|
||||
setIsCategoryModalVisible(true);
|
||||
}}
|
||||
>
|
||||
新增
|
||||
</Button>
|
||||
</div>
|
||||
<List
|
||||
loading={loadingCategories}
|
||||
dataSource={categories}
|
||||
renderItem={(item) => (
|
||||
<List.Item
|
||||
className={selectedCategory?.id === item.id ? 'ant-list-item-active' : ''}
|
||||
style={{
|
||||
cursor: 'pointer',
|
||||
background: selectedCategory?.id === item.id ? '#e6f7ff' : 'transparent',
|
||||
padding: '8px 12px',
|
||||
borderRadius: '4px',
|
||||
}}
|
||||
onClick={() => setSelectedCategory(item)}
|
||||
actions={[
|
||||
<a
|
||||
key="edit"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setEditingCategory(item);
|
||||
categoryForm.setFieldsValue(item);
|
||||
setIsCategoryModalVisible(true);
|
||||
}}
|
||||
>
|
||||
编辑
|
||||
</a>,
|
||||
<Popconfirm
|
||||
key="delete"
|
||||
title="确定删除该分类吗?"
|
||||
onConfirm={(e) => {
|
||||
e?.stopPropagation();
|
||||
handleDeleteCategory(item.id);
|
||||
}}
|
||||
onCancel={(e) => e?.stopPropagation()}
|
||||
>
|
||||
<a onClick={(e) => e.stopPropagation()} style={{ color: 'red' }}>删除</a>
|
||||
</Popconfirm>,
|
||||
]}
|
||||
>
|
||||
<List.Item.Meta
|
||||
title={item.title}
|
||||
description={item.name}
|
||||
/>
|
||||
</List.Item>
|
||||
)}
|
||||
/>
|
||||
</Sider>
|
||||
<Content style={{ padding: '24px' }}>
|
||||
{selectedCategory ? (
|
||||
<Card title={`分类:${selectedCategory.title} (${selectedCategory.name})`} extra={<Button type="primary" onClick={handleAddAttribute}>添加关联属性</Button>}>
|
||||
<List
|
||||
loading={loadingAttributes}
|
||||
dataSource={categoryAttributes}
|
||||
renderItem={(item) => (
|
||||
<List.Item
|
||||
actions={[
|
||||
<Popconfirm
|
||||
title="确定移除该属性吗?"
|
||||
onConfirm={() => handleDeleteAttribute(item.id)}
|
||||
>
|
||||
<Button type="link" danger>移除</Button>
|
||||
</Popconfirm>
|
||||
]}
|
||||
>
|
||||
<List.Item.Meta
|
||||
title={item.dict.title}
|
||||
description={`Code: ${item.dict.name}`}
|
||||
/>
|
||||
</List.Item>
|
||||
)}
|
||||
/>
|
||||
</Card>
|
||||
) : (
|
||||
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '100%', color: '#999' }}>
|
||||
请选择左侧分类
|
||||
</div>
|
||||
)}
|
||||
</Content>
|
||||
</Layout>
|
||||
|
||||
<Modal
|
||||
title={editingCategory ? '编辑分类' : '新增分类'}
|
||||
open={isCategoryModalVisible}
|
||||
onOk={() => categoryForm.submit()}
|
||||
onCancel={() => setIsCategoryModalVisible(false)}
|
||||
>
|
||||
<Form form={categoryForm} onFinish={handleCategorySubmit} layout="vertical">
|
||||
<Form.Item name="title" label="标题" rules={[{ required: true }]}>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item name="name" label="标识 (Code)" rules={[{ required: true }]}>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
title="添加关联属性"
|
||||
open={isAttributeModalVisible}
|
||||
onOk={handleAttributeSubmit}
|
||||
onCancel={() => setIsAttributeModalVisible(false)}
|
||||
>
|
||||
<Form layout="vertical">
|
||||
<Form.Item label="选择属性">
|
||||
<Select
|
||||
mode="multiple"
|
||||
style={{ width: '100%' }}
|
||||
placeholder="请选择要关联的属性"
|
||||
value={selectedDictIds}
|
||||
onChange={setSelectedDictIds}
|
||||
options={availableDicts.map(d => ({ label: d.title, value: d.id }))}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</PageContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default CategoryPage;
|
||||
|
|
@ -17,6 +17,8 @@ const fetchDictOptions = async (dictName: string, keyword?: string) => {
|
|||
return (data?.items || []).map((item: any) => ({
|
||||
label: item.name,
|
||||
value: item.name,
|
||||
id: item.id,
|
||||
item,
|
||||
}));
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -5,5 +5,4 @@ export const attributes = new Set([
|
|||
'flavor',
|
||||
'size',
|
||||
'humidity',
|
||||
'category',
|
||||
]);
|
||||
|
|
|
|||
|
|
@ -37,7 +37,6 @@ const AttributePage: React.FC = () => {
|
|||
const [editingDictItem, setEditingDictItem] = useState<any>(null);
|
||||
const [dictItemForm] = Form.useForm();
|
||||
|
||||
// 中文注释:获取字典列表,仅保留允许的名称
|
||||
const fetchDicts = async (title?: string) => {
|
||||
setLoadingDicts(true);
|
||||
try {
|
||||
|
|
@ -168,6 +167,8 @@ const AttributePage: React.FC = () => {
|
|||
allowClear
|
||||
size="small"
|
||||
/>
|
||||
</Space>
|
||||
<div style={{ marginTop: '8px', overflowY: 'auto', height: 'calc(100vh - 150px)' }}>
|
||||
<Table
|
||||
dataSource={dicts}
|
||||
columns={dictColumns}
|
||||
|
|
@ -189,7 +190,7 @@ const AttributePage: React.FC = () => {
|
|||
}
|
||||
pagination={false}
|
||||
/>
|
||||
</Space>
|
||||
</div>
|
||||
</Sider>
|
||||
<Content style={{ padding: '8px' }}>
|
||||
<ProTable
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
import {
|
||||
productcontrollerCreateproduct,
|
||||
productcontrollerDeleteproduct,
|
||||
productcontrollerGetcategoriesall,
|
||||
productcontrollerGetcategoryattributes,
|
||||
productcontrollerGetproductcomponents,
|
||||
productcontrollerGetproductlist,
|
||||
productcontrollerSetproductcomponents,
|
||||
productcontrollerUpdateproduct,
|
||||
productcontrollerUpdatenamecn,
|
||||
productcontrollerUpdateproduct,
|
||||
} from '@/servers/api/product';
|
||||
import { stockcontrollerGetstocks as getStocks } from '@/servers/api/stock';
|
||||
import { templatecontrollerRendertemplate } from '@/servers/api/template';
|
||||
|
|
@ -29,7 +31,7 @@ import { App, Button, Popconfirm, Tag, Upload } from 'antd';
|
|||
|
||||
import AttributeFormItem from '@/pages/Product/Attribute/components/AttributeFormItem';
|
||||
|
||||
import React, { useMemo, useRef, useState } from 'react';
|
||||
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||
const capitalize = (s: string) => s.charAt(0).toLocaleUpperCase() + s.slice(1);
|
||||
|
||||
const NameCn: React.FC<{
|
||||
|
|
@ -175,6 +177,13 @@ const List: React.FC = () => {
|
|||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '商品类型',
|
||||
dataIndex: 'category',
|
||||
render: (_, record: any) => {
|
||||
return record.category?.title || record.category?.name || '-';
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '价格',
|
||||
dataIndex: 'price',
|
||||
|
|
@ -314,6 +323,28 @@ const CreateForm: React.FC<{
|
|||
'in-stock' | 'out-of-stock' | null
|
||||
>(null);
|
||||
|
||||
const [categories, setCategories] = useState<any[]>([]);
|
||||
const [activeAttributes, setActiveAttributes] = useState<any[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
productcontrollerGetcategoriesall().then((res) => {
|
||||
setCategories(res || []);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleCategoryChange = async (categoryId: number) => {
|
||||
if (!categoryId) {
|
||||
setActiveAttributes([]);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const attrs = await productcontrollerGetcategoryattributes({ categoryItemId: categoryId });
|
||||
setActiveAttributes(attrs || []);
|
||||
} catch (error) {
|
||||
message.error('获取分类属性失败');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @description 生成 SKU
|
||||
*/
|
||||
|
|
@ -324,21 +355,14 @@ const CreateForm: React.FC<{
|
|||
const { humidityValues, brandValues, strengthValues, flavorValues } =
|
||||
formValues;
|
||||
// 检查是否所有必需的字段都已选择
|
||||
if (
|
||||
!brandValues?.length ||
|
||||
!strengthValues?.length ||
|
||||
!flavorValues?.length ||
|
||||
!humidityValues?.length
|
||||
) {
|
||||
message.warning('请先选择品牌、强度、口味和干湿');
|
||||
return;
|
||||
}
|
||||
// 注意:这里仅检查标准属性,如果当前分类没有这些属性,可能需要调整逻辑
|
||||
// 暂时保持原样,假设常用属性会被配置
|
||||
|
||||
// 所选值(用于 SKU 模板传入 name)
|
||||
const brandName: string = String(brandValues[0]);
|
||||
const strengthName: string = String(strengthValues[0]);
|
||||
const flavorName: string = String(flavorValues[0]);
|
||||
const humidityName: string = String(humidityValues[0]);
|
||||
const brandName: string = String(brandValues?.[0] || '');
|
||||
const strengthName: string = String(strengthValues?.[0] || '');
|
||||
const flavorName: string = String(flavorValues?.[0] || '');
|
||||
const humidityName: string = String(humidityValues?.[0] || '');
|
||||
|
||||
// 调用模板渲染API来生成SKU
|
||||
const {
|
||||
|
|
@ -374,21 +398,12 @@ const CreateForm: React.FC<{
|
|||
const formValues = formRef.current?.getFieldsValue();
|
||||
const { humidityValues, brandValues, strengthValues, flavorValues } =
|
||||
formValues;
|
||||
// 检查是否所有必需的字段都已选择
|
||||
if (
|
||||
!brandValues?.length ||
|
||||
!strengthValues?.length ||
|
||||
!flavorValues?.length ||
|
||||
!humidityValues?.length
|
||||
) {
|
||||
message.warning('请先选择品牌、强度、口味和干湿');
|
||||
return;
|
||||
}
|
||||
|
||||
const brandName: string = String(brandValues[0]);
|
||||
const strengthName: string = String(strengthValues[0]);
|
||||
const flavorName: string = String(flavorValues[0]);
|
||||
const humidityName: string = String(humidityValues[0]);
|
||||
const brandName: string = String(brandValues?.[0] || '');
|
||||
const strengthName: string = String(strengthValues?.[0] || '');
|
||||
const flavorName: string = String(flavorValues?.[0] || '');
|
||||
const humidityName: string = String(humidityValues?.[0] || '');
|
||||
|
||||
const brandTitle = brandName;
|
||||
const strengthTitle = strengthName;
|
||||
const flavorTitle = flavorName;
|
||||
|
|
@ -438,6 +453,11 @@ const CreateForm: React.FC<{
|
|||
destroyOnHidden: true,
|
||||
}}
|
||||
onValuesChange={async (changedValues) => {
|
||||
// 当 Category 发生变化时
|
||||
if ('categoryId' in changedValues) {
|
||||
handleCategoryChange(changedValues.categoryId);
|
||||
}
|
||||
|
||||
// 当 SKU 发生变化时
|
||||
if ('sku' in changedValues) {
|
||||
const sku = changedValues.sku;
|
||||
|
|
@ -467,33 +487,20 @@ const CreateForm: React.FC<{
|
|||
}
|
||||
}}
|
||||
onFinish={async (values: any) => {
|
||||
// 中文注释:组装 attributes(支持输入新值,按标题传入创建/绑定)
|
||||
// 中文注释:组装 attributes(根据 activeAttributes 动态组装)
|
||||
const attributes = activeAttributes.flatMap((attr: any) => {
|
||||
const dictName = attr.dict.name;
|
||||
const key = `${dictName}Values`;
|
||||
const vals = values[key];
|
||||
if (vals && Array.isArray(vals)) {
|
||||
return vals.map((v: string) => ({
|
||||
dictName: dictName,
|
||||
name: v,
|
||||
}));
|
||||
}
|
||||
return [];
|
||||
});
|
||||
|
||||
const attributes = [
|
||||
...(values.brandValues || []).map((v: string) => ({
|
||||
dictName: 'brand',
|
||||
name: v,
|
||||
})),
|
||||
...(values.strengthValues || []).map((v: string) => ({
|
||||
dictName: 'strength',
|
||||
name: v,
|
||||
})),
|
||||
...(values.flavorValues || []).map((v: string) => ({
|
||||
dictName: 'flavor',
|
||||
name: v,
|
||||
})),
|
||||
...(values.humidityValues || []).map((v: string) => ({
|
||||
dictName: 'humidity',
|
||||
name: v,
|
||||
})),
|
||||
...(values.sizeValues || []).map((v: string) => ({
|
||||
dictName: 'size',
|
||||
name: v,
|
||||
})),
|
||||
...(values.category
|
||||
? [{ dictName: 'category', name: values.category }]
|
||||
: []),
|
||||
];
|
||||
const payload: any = {
|
||||
name: (values as any).name,
|
||||
description: (values as any).description,
|
||||
|
|
@ -503,6 +510,7 @@ const CreateForm: React.FC<{
|
|||
attributes,
|
||||
type: values.type, // 直接使用 type
|
||||
components: values.components,
|
||||
categoryId: values.categoryId,
|
||||
};
|
||||
const { success, message: errMsg } =
|
||||
await productcontrollerCreateproduct(payload);
|
||||
|
|
@ -608,37 +616,26 @@ const CreateForm: React.FC<{
|
|||
) : null
|
||||
}
|
||||
</ProForm.Item>
|
||||
<AttributeFormItem
|
||||
dictName="brand"
|
||||
name="brandValues"
|
||||
label="产品品牌"
|
||||
isTag
|
||||
/>
|
||||
<AttributeFormItem
|
||||
dictName="strength"
|
||||
name="strengthValues"
|
||||
label="强度"
|
||||
isTag
|
||||
/>
|
||||
<AttributeFormItem
|
||||
dictName="flavor"
|
||||
name="flavorValues"
|
||||
label="口味"
|
||||
isTag
|
||||
/>
|
||||
<AttributeFormItem
|
||||
dictName="humidity"
|
||||
name="humidityValues"
|
||||
label="干湿"
|
||||
isTag
|
||||
/>
|
||||
<AttributeFormItem dictName="size" name="sizeValues" label="大小" isTag />
|
||||
<AttributeFormItem
|
||||
dictName="category"
|
||||
name="category"
|
||||
|
||||
<ProFormSelect
|
||||
name="categoryId"
|
||||
label="分类"
|
||||
isTag
|
||||
width="md"
|
||||
options={categories.map(c => ({ label: c.title, value: c.id }))}
|
||||
placeholder="请选择分类"
|
||||
rules={[{ required: true, message: '请选择分类' }]}
|
||||
/>
|
||||
|
||||
{activeAttributes.map((attr: any) => (
|
||||
<AttributeFormItem
|
||||
key={attr.id}
|
||||
dictName={attr.dict.name}
|
||||
name={`${attr.dict.name}Values`}
|
||||
label={attr.dict.title}
|
||||
isTag
|
||||
/>
|
||||
))}
|
||||
|
||||
<ProFormText
|
||||
name="price"
|
||||
label="价格"
|
||||
|
|
@ -679,6 +676,39 @@ const EditForm: React.FC<{
|
|||
'in-stock' | 'out-of-stock' | null
|
||||
>(null);
|
||||
|
||||
const [categories, setCategories] = useState<any[]>([]);
|
||||
const [activeAttributes, setActiveAttributes] = useState<any[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
productcontrollerGetcategoriesall().then((res) => {
|
||||
setCategories(res || []);
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const categoryId = (record as any).categoryId || (record as any).category?.id;
|
||||
if (categoryId) {
|
||||
productcontrollerGetcategoryattributes({ categoryItemId: categoryId }).then(res => {
|
||||
setActiveAttributes(res || []);
|
||||
});
|
||||
} else {
|
||||
setActiveAttributes([]);
|
||||
}
|
||||
}, [record]);
|
||||
|
||||
const handleCategoryChange = async (categoryId: number) => {
|
||||
if (!categoryId) {
|
||||
setActiveAttributes([]);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const attrs = await productcontrollerGetcategoryattributes({ categoryItemId: categoryId });
|
||||
setActiveAttributes(attrs || []);
|
||||
} catch (error) {
|
||||
message.error('获取分类属性失败');
|
||||
}
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
(async () => {
|
||||
const { data: stockData } = await getStocks({
|
||||
|
|
@ -717,6 +747,7 @@ const EditForm: React.FC<{
|
|||
}, {} as any),
|
||||
components: components,
|
||||
type: type,
|
||||
categoryId: (record as any).categoryId || (record as any).category?.id,
|
||||
};
|
||||
}, [record, components, type]);
|
||||
|
||||
|
|
@ -731,6 +762,11 @@ const EditForm: React.FC<{
|
|||
}}
|
||||
initialValues={initialValues}
|
||||
onValuesChange={async (changedValues) => {
|
||||
// 当 Category 发生变化时
|
||||
if ('categoryId' in changedValues) {
|
||||
handleCategoryChange(changedValues.categoryId);
|
||||
}
|
||||
|
||||
// 当 SKU 发生变化时
|
||||
if ('sku' in changedValues) {
|
||||
const sku = changedValues.sku;
|
||||
|
|
@ -755,31 +791,19 @@ const EditForm: React.FC<{
|
|||
}
|
||||
}}
|
||||
onFinish={async (values) => {
|
||||
const attributes = [
|
||||
...(values.brandValues || []).map((v: string) => ({
|
||||
dictName: 'brand',
|
||||
name: v,
|
||||
})),
|
||||
...(values.strengthValues || []).map((v: string) => ({
|
||||
dictName: 'strength',
|
||||
name: v,
|
||||
})),
|
||||
...(values.flavorValues || []).map((v: string) => ({
|
||||
dictName: 'flavor',
|
||||
name: v,
|
||||
})),
|
||||
...(values.humidityValues || []).map((v: string) => ({
|
||||
dictName: 'humidity',
|
||||
name: v,
|
||||
})),
|
||||
...(values.sizeValues || []).map((v: string) => ({
|
||||
dictName: 'size',
|
||||
name: v,
|
||||
})),
|
||||
...(values.category
|
||||
? [{ dictName: 'category', name: values.category }]
|
||||
: []),
|
||||
];
|
||||
// 中文注释:组装 attributes
|
||||
const attributes = activeAttributes.flatMap((attr: any) => {
|
||||
const dictName = attr.dict.name;
|
||||
const key = `${dictName}Values`;
|
||||
const vals = values[key];
|
||||
if (vals && Array.isArray(vals)) {
|
||||
return vals.map((v: string) => ({
|
||||
dictName: dictName,
|
||||
name: v,
|
||||
}));
|
||||
}
|
||||
return [];
|
||||
});
|
||||
|
||||
const payload: any = {
|
||||
name: (values as any).name,
|
||||
|
|
@ -789,6 +813,7 @@ const EditForm: React.FC<{
|
|||
promotionPrice: (values as any).promotionPrice,
|
||||
attributes,
|
||||
type: values.type, // 直接使用 type
|
||||
categoryId: values.categoryId,
|
||||
};
|
||||
|
||||
const { success, message: errMsg } =
|
||||
|
|
@ -939,32 +964,25 @@ const EditForm: React.FC<{
|
|||
placeholder="请输入促销价"
|
||||
/>
|
||||
</ProForm.Group>
|
||||
<AttributeFormItem
|
||||
dictName="brand"
|
||||
name="brandValues"
|
||||
label="产品品牌"
|
||||
isTag
|
||||
|
||||
<ProFormSelect
|
||||
name="categoryId"
|
||||
label="分类"
|
||||
width="md"
|
||||
options={categories.map(c => ({ label: c.title, value: c.id }))}
|
||||
placeholder="请选择分类"
|
||||
rules={[{ required: true, message: '请选择分类' }]}
|
||||
/>
|
||||
<AttributeFormItem
|
||||
dictName="strength"
|
||||
name="strengthValues"
|
||||
label="强度"
|
||||
isTag
|
||||
/>
|
||||
<AttributeFormItem
|
||||
dictName="flavor"
|
||||
name="flavorValues"
|
||||
label="口味"
|
||||
isTag
|
||||
/>
|
||||
<AttributeFormItem
|
||||
dictName="humidity"
|
||||
name="humidityValues"
|
||||
label="干湿度"
|
||||
isTag
|
||||
/>
|
||||
<AttributeFormItem dictName="size" name="sizeValues" label="尺寸" isTag />
|
||||
<AttributeFormItem dictName="category" name="category" label="分类" />
|
||||
|
||||
{activeAttributes.map((attr: any) => (
|
||||
<AttributeFormItem
|
||||
key={attr.id}
|
||||
dictName={attr.dict.name}
|
||||
name={`${attr.dict.name}Values`}
|
||||
label={attr.dict.title}
|
||||
isTag
|
||||
/>
|
||||
))}
|
||||
</DrawerForm>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import { ProTable, ProColumns } from '@ant-design/pro-components';
|
||||
import { Card, Spin, Empty, message } from 'antd';
|
||||
import request from '@@/plugin-request/request';
|
||||
import { request } from '@umijs/max';
|
||||
|
||||
// 定义站点接口
|
||||
interface Site {
|
||||
|
|
@ -37,13 +37,22 @@ interface ApiResponse<T> {
|
|||
|
||||
// 模拟API请求函数
|
||||
const getSites = async (): Promise<ApiResponse<Site>> => {
|
||||
return request('/api/sites', {
|
||||
const res = await request('/site/list', {
|
||||
method: 'GET',
|
||||
params: {
|
||||
current: 1,
|
||||
pageSize: 1000
|
||||
}
|
||||
});
|
||||
return {
|
||||
data: res.data?.items || [],
|
||||
success: res.success,
|
||||
message: res.message
|
||||
};
|
||||
};
|
||||
|
||||
const getWPProducts = async (): Promise<ApiResponse<WpProduct>> => {
|
||||
return request('/api/wp-products', {
|
||||
return request('/product/wp-products', {
|
||||
method: 'GET',
|
||||
});
|
||||
};
|
||||
|
|
@ -137,9 +146,9 @@ const ProductSyncPage: React.FC = () => {
|
|||
key: 'name',
|
||||
width: 200,
|
||||
fixed: 'left',
|
||||
render: (name: string, record: ProductBase) => (
|
||||
render: (dom: React.ReactNode, record: ProductBase) => (
|
||||
<div>
|
||||
<div>{name}</div>
|
||||
<div>{dom}</div>
|
||||
<div style={{ marginTop: 4, fontSize: 12, color: '#666' }}>
|
||||
{Object.entries(record.attributes || {})
|
||||
.map(([key, value]) => `${key}: ${value}`)
|
||||
|
|
@ -198,7 +207,9 @@ const ProductSyncPage: React.FC = () => {
|
|||
options={{
|
||||
density: true,
|
||||
}}
|
||||
emptyText="暂无商品数据"
|
||||
locale={{
|
||||
emptyText: '暂无商品数据',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Card>
|
||||
|
|
|
|||
|
|
@ -2,27 +2,31 @@
|
|||
/* eslint-disable */
|
||||
import { request } from 'umi';
|
||||
|
||||
/** 获取区域列表(分页) GET /api/area/ */
|
||||
/** 获取区域列表(分页) GET /area/ */
|
||||
export async function areacontrollerGetarealist(
|
||||
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
|
||||
params: API.areacontrollerGetarealistParams,
|
||||
options?: { [key: string]: any },
|
||||
) {
|
||||
return request<API.Area[]>('/api/area/', {
|
||||
return request<API.Area[]>('/area/', {
|
||||
method: 'GET',
|
||||
params: {
|
||||
// currentPage has a default value: 1
|
||||
currentPage: '1',
|
||||
// pageSize has a default value: 10
|
||||
pageSize: '10',
|
||||
...params,
|
||||
},
|
||||
...(options || {}),
|
||||
});
|
||||
}
|
||||
|
||||
/** 创建区域 POST /api/area/ */
|
||||
/** 创建区域 POST /area/ */
|
||||
export async function areacontrollerCreatearea(
|
||||
body: API.CreateAreaDTO,
|
||||
options?: { [key: string]: any },
|
||||
) {
|
||||
return request<API.Area>('/api/area/', {
|
||||
return request<API.Area>('/area/', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
|
|
@ -32,21 +36,21 @@ export async function areacontrollerCreatearea(
|
|||
});
|
||||
}
|
||||
|
||||
/** 根据ID获取区域详情 GET /api/area/${param0} */
|
||||
/** 根据ID获取区域详情 GET /area/${param0} */
|
||||
export async function areacontrollerGetareabyid(
|
||||
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
|
||||
params: API.areacontrollerGetareabyidParams,
|
||||
options?: { [key: string]: any },
|
||||
) {
|
||||
const { id: param0, ...queryParams } = params;
|
||||
return request<API.Area>(`/api/area/${param0}`, {
|
||||
return request<API.Area>(`/area/${param0}`, {
|
||||
method: 'GET',
|
||||
params: { ...queryParams },
|
||||
...(options || {}),
|
||||
});
|
||||
}
|
||||
|
||||
/** 更新区域 PUT /api/area/${param0} */
|
||||
/** 更新区域 PUT /area/${param0} */
|
||||
export async function areacontrollerUpdatearea(
|
||||
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
|
||||
params: API.areacontrollerUpdateareaParams,
|
||||
|
|
@ -54,7 +58,7 @@ export async function areacontrollerUpdatearea(
|
|||
options?: { [key: string]: any },
|
||||
) {
|
||||
const { id: param0, ...queryParams } = params;
|
||||
return request<API.Area>(`/api/area/${param0}`, {
|
||||
return request<API.Area>(`/area/${param0}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
|
|
@ -65,25 +69,25 @@ export async function areacontrollerUpdatearea(
|
|||
});
|
||||
}
|
||||
|
||||
/** 删除区域 DELETE /api/area/${param0} */
|
||||
/** 删除区域 DELETE /area/${param0} */
|
||||
export async function areacontrollerDeletearea(
|
||||
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
|
||||
params: API.areacontrollerDeleteareaParams,
|
||||
options?: { [key: string]: any },
|
||||
) {
|
||||
const { id: param0, ...queryParams } = params;
|
||||
return request<any>(`/api/area/${param0}`, {
|
||||
return request<any>(`/area/${param0}`, {
|
||||
method: 'DELETE',
|
||||
params: { ...queryParams },
|
||||
...(options || {}),
|
||||
});
|
||||
}
|
||||
|
||||
/** 获取所有区域 GET /api/area/all */
|
||||
export async function areacontrollerGetallareas(options?: {
|
||||
/** 获取国家列表 GET /area/countries */
|
||||
export async function areacontrollerGetcountries(options?: {
|
||||
[key: string]: any;
|
||||
}) {
|
||||
return request<API.Area[]>('/api/area/all', {
|
||||
return request<any>('/area/countries', {
|
||||
method: 'GET',
|
||||
...(options || {}),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -8,17 +8,10 @@ declare namespace API {
|
|||
};
|
||||
|
||||
type Area = {
|
||||
id?: number;
|
||||
/** 区域名称 */
|
||||
/** 名称 */
|
||||
name?: string;
|
||||
/** 纬度 */
|
||||
latitude?: number;
|
||||
/** 经度 */
|
||||
longitude?: number;
|
||||
/** 创建时间 */
|
||||
createdAt: string;
|
||||
/** 更新时间 */
|
||||
updatedAt: string;
|
||||
/** 编码 */
|
||||
code?: string;
|
||||
};
|
||||
|
||||
type areacontrollerDeleteareaParams = {
|
||||
|
|
@ -30,12 +23,12 @@ declare namespace API {
|
|||
};
|
||||
|
||||
type areacontrollerGetarealistParams = {
|
||||
/** 当前页码 */
|
||||
/** 当前页 */
|
||||
currentPage?: number;
|
||||
/** 每页数量 */
|
||||
pageSize?: number;
|
||||
/** 区域名称 */
|
||||
name?: string;
|
||||
/** 关键词(名称或编码) */
|
||||
keyword?: string;
|
||||
};
|
||||
|
||||
type areacontrollerUpdateareaParams = {
|
||||
|
|
@ -54,12 +47,8 @@ declare namespace API {
|
|||
};
|
||||
|
||||
type CreateAreaDTO = {
|
||||
/** 区域名称 */
|
||||
name?: string;
|
||||
/** 纬度 */
|
||||
latitude?: number;
|
||||
/** 经度 */
|
||||
longitude?: number;
|
||||
/** 编码 */
|
||||
code?: string;
|
||||
};
|
||||
|
||||
type CreateDictDTO = {};
|
||||
|
|
@ -98,13 +87,18 @@ declare namespace API {
|
|||
items?: PurchaseOrderItem[];
|
||||
};
|
||||
|
||||
type CreateSiteDTO = {};
|
||||
type CreateSiteDTO = {
|
||||
/** 区域 */
|
||||
areas?: any;
|
||||
};
|
||||
|
||||
type CreateStockPointDTO = {
|
||||
name?: string;
|
||||
location?: string;
|
||||
contactPerson?: string;
|
||||
contactPhone?: string;
|
||||
/** 区域 */
|
||||
areas?: any;
|
||||
};
|
||||
|
||||
type CreateTemplateDTO = {
|
||||
|
|
@ -708,6 +702,10 @@ declare namespace API {
|
|||
nameCn?: string;
|
||||
/** 产品描述 */
|
||||
description?: string;
|
||||
/** 产品分类 */
|
||||
category?: string;
|
||||
/** 动态属性值 */
|
||||
properties?: Record<string, any>;
|
||||
/** sku */
|
||||
sku?: string;
|
||||
/** 价格 */
|
||||
|
|
@ -961,12 +959,12 @@ declare namespace API {
|
|||
};
|
||||
|
||||
type QueryAreaDTO = {
|
||||
/** 当前页码 */
|
||||
/** 当前页 */
|
||||
currentPage?: number;
|
||||
/** 每页数量 */
|
||||
pageSize?: number;
|
||||
/** 区域名称 */
|
||||
name?: string;
|
||||
/** 关键词(名称或编码) */
|
||||
keyword?: string;
|
||||
};
|
||||
|
||||
type QueryCustomerListDTO = {
|
||||
|
|
@ -1618,12 +1616,8 @@ declare namespace API {
|
|||
};
|
||||
|
||||
type UpdateAreaDTO = {
|
||||
/** 区域名称 */
|
||||
name?: string;
|
||||
/** 纬度 */
|
||||
latitude?: number;
|
||||
/** 经度 */
|
||||
longitude?: number;
|
||||
/** 编码 */
|
||||
code?: string;
|
||||
};
|
||||
|
||||
type UpdateDictDTO = {};
|
||||
|
|
@ -1655,7 +1649,10 @@ declare namespace API {
|
|||
items?: PurchaseOrderItem[];
|
||||
};
|
||||
|
||||
type UpdateSiteDTO = {};
|
||||
type UpdateSiteDTO = {
|
||||
/** 区域 */
|
||||
areas?: any;
|
||||
};
|
||||
|
||||
type UpdateStockDTO = {
|
||||
stockPointId?: number;
|
||||
|
|
@ -1671,6 +1668,8 @@ declare namespace API {
|
|||
location?: string;
|
||||
contactPerson?: string;
|
||||
contactPhone?: string;
|
||||
/** 区域 */
|
||||
areas?: any;
|
||||
};
|
||||
|
||||
type UpdateTemplateDTO = {
|
||||
|
|
|
|||
Loading…
Reference in New Issue