Compare commits
3 Commits
| Author | SHA1 | Date |
|---|---|---|
|
|
84ec4f5895 | |
|
|
82e0df6d43 | |
|
|
2245c71702 |
192
.umirc.ts
192
.umirc.ts
|
|
@ -1,10 +1,10 @@
|
||||||
import { defineConfig } from '@umijs/max';
|
import { defineConfig } from '@umijs/max';
|
||||||
import { codeInspectorPlugin } from 'code-inspector-plugin';
|
|
||||||
|
|
||||||
const isDev = process.env.NODE_ENV === 'development';
|
const isDev = process.env.NODE_ENV === 'development';
|
||||||
const UMI_APP_API_URL = isDev
|
const UMI_APP_API_URL = isDev
|
||||||
? 'http://localhost:7001'
|
? 'http://localhost:7001'
|
||||||
: 'https://api.yoone.ca';
|
: 'https://api.yoone.ca';
|
||||||
|
import { codeInspectorPlugin } from 'code-inspector-plugin';
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
hash: true,
|
hash: true,
|
||||||
|
|
@ -16,7 +16,6 @@ export default defineConfig({
|
||||||
layout: {
|
layout: {
|
||||||
title: 'YOONE',
|
title: 'YOONE',
|
||||||
},
|
},
|
||||||
esbuildMinifyIIFE: true,
|
|
||||||
define: {
|
define: {
|
||||||
UMI_APP_API_URL,
|
UMI_APP_API_URL,
|
||||||
},
|
},
|
||||||
|
|
@ -24,7 +23,7 @@ export default defineConfig({
|
||||||
config.plugin('code-inspector-plugin').use(
|
config.plugin('code-inspector-plugin').use(
|
||||||
codeInspectorPlugin({
|
codeInspectorPlugin({
|
||||||
bundler: 'webpack',
|
bundler: 'webpack',
|
||||||
}),
|
})
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
routes: [
|
routes: [
|
||||||
|
|
@ -44,24 +43,6 @@ export default defineConfig({
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
|
||||||
name: '地区管理',
|
|
||||||
path: '/area',
|
|
||||||
access: 'canSeeArea',
|
|
||||||
routes: [
|
|
||||||
{
|
|
||||||
name: '地区列表',
|
|
||||||
path: '/area/list',
|
|
||||||
component: './Area/List',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: '地区地图',
|
|
||||||
path: '/area/map',
|
|
||||||
component: './Area/Map',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: '站点管理',
|
name: '站点管理',
|
||||||
path: '/site',
|
path: '/site',
|
||||||
|
|
@ -72,118 +53,37 @@ export default defineConfig({
|
||||||
path: '/site/list',
|
path: '/site/list',
|
||||||
component: './Site/List',
|
component: './Site/List',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: '店铺管理',
|
|
||||||
path: '/site/shop',
|
|
||||||
component: './Site/Shop/Layout',
|
|
||||||
routes: [
|
|
||||||
{
|
|
||||||
path: '/site/shop/:siteId/products',
|
|
||||||
component: './Site/Shop/Products',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/site/shop/:siteId/orders',
|
|
||||||
component: './Site/Shop/Orders',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/site/shop/:siteId/subscriptions',
|
|
||||||
component: './Site/Shop/Subscriptions',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/site/shop/:siteId/logistics',
|
|
||||||
component: './Site/Shop/Logistics',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/site/shop/:siteId/media',
|
|
||||||
component: './Site/Shop/Media',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/site/shop/:siteId/customers',
|
|
||||||
component: './Site/Shop/Customers',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/site/shop/:siteId/reviews',
|
|
||||||
component: './Site/Shop/Reviews',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/site/shop/:siteId/webhooks',
|
|
||||||
component: './Site/Shop/Webhooks',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/site/shop/:siteId/links',
|
|
||||||
component: './Site/Shop/Links',
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Woo标签工具',
|
name: '商品管理',
|
||||||
path: '/site/woocommerce/product/tool/tag',
|
|
||||||
component: './Woo/Product/TagTool',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: '客户管理',
|
|
||||||
path: '/customer',
|
|
||||||
access: 'canSeeCustomer',
|
|
||||||
routes: [
|
|
||||||
{
|
|
||||||
name: '客户列表',
|
|
||||||
path: '/customer/list',
|
|
||||||
component: './Customer/List',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: '数据分析列表',
|
|
||||||
path: '/customer/statistic/list',
|
|
||||||
component: './Customer/StatisticList',
|
|
||||||
},
|
|
||||||
// {
|
|
||||||
// name: '客户统计',
|
|
||||||
// path: '/customer/statistic/home',
|
|
||||||
// component: './Customer/Statistic',
|
|
||||||
// }
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: '产品管理',
|
|
||||||
path: '/product',
|
path: '/product',
|
||||||
access: 'canSeeProduct',
|
access: 'canSeeProduct',
|
||||||
routes: [
|
routes: [
|
||||||
|
{
|
||||||
|
name: '商品分类',
|
||||||
|
path: '/product/category',
|
||||||
|
component: './Product/Category',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '强度',
|
||||||
|
path: '/product/strength',
|
||||||
|
component: './Product/Strength',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '口味',
|
||||||
|
path: '/product/flavors',
|
||||||
|
component: './Product/Flavors',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: '产品列表',
|
name: '产品列表',
|
||||||
path: '/product/list',
|
path: '/product/list',
|
||||||
component: './Product/List',
|
component: './Product/List',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: '产品分类',
|
name: 'WP商品列表',
|
||||||
path: '/product/category',
|
path: '/product/wp_list',
|
||||||
component: './Product/Category',
|
component: './Product/WpList',
|
||||||
},
|
|
||||||
{
|
|
||||||
name: '产品属性',
|
|
||||||
path: '/product/attribute',
|
|
||||||
component: './Product/Attribute',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: '产品属性排列',
|
|
||||||
path: '/product/permutation',
|
|
||||||
component: './Product/Permutation',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: '产品品牌空间',
|
|
||||||
path: '/product/groupBy',
|
|
||||||
component: './Product/GroupBy',
|
|
||||||
},
|
|
||||||
// sync
|
|
||||||
{
|
|
||||||
name: '同步产品',
|
|
||||||
path: '/product/sync',
|
|
||||||
component: './Product/Sync',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: '产品CSV 工具',
|
|
||||||
path: '/product/csvtool',
|
|
||||||
component: './Product/CsvTool',
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
@ -254,6 +154,18 @@ export default defineConfig({
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: '客户管理',
|
||||||
|
path: '/customer',
|
||||||
|
access: 'canSeeCustomer',
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
name: '客户列表',
|
||||||
|
path: '/customer/list',
|
||||||
|
component: './Customer/List',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: '物流管理',
|
name: '物流管理',
|
||||||
path: '/logistics',
|
path: '/logistics',
|
||||||
|
|
@ -313,48 +225,10 @@ export default defineConfig({
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: '系统管理',
|
|
||||||
path: '/system',
|
|
||||||
access: 'canSeeSystem',
|
|
||||||
routes: [
|
|
||||||
{
|
|
||||||
name: '字典管理',
|
|
||||||
path: '/system/dict',
|
|
||||||
access: 'canSeeDict',
|
|
||||||
routes: [
|
|
||||||
{
|
|
||||||
name: '字典列表',
|
|
||||||
path: '/system/dict/list',
|
|
||||||
component: './Dict/List',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: '模板管理',
|
|
||||||
path: '/system/template',
|
|
||||||
access: 'canSeeTemplate',
|
|
||||||
routes: [
|
|
||||||
{
|
|
||||||
name: '模板列表',
|
|
||||||
path: '/system/template/list',
|
|
||||||
component: './Template',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
// {
|
// {
|
||||||
// path: '*',
|
// path: '*',
|
||||||
// component: './404',
|
// component: './404',
|
||||||
// },
|
// },
|
||||||
],
|
],
|
||||||
proxy: {
|
|
||||||
'/api': {
|
|
||||||
target: UMI_APP_API_URL,
|
|
||||||
changeOrigin: true,
|
|
||||||
pathRewrite: { '^/api': '' },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
npmClient: 'pnpm',
|
npmClient: 'pnpm',
|
||||||
});
|
});
|
||||||
|
|
|
||||||
32
Dockerfile
32
Dockerfile
|
|
@ -1,32 +0,0 @@
|
||||||
# 构建阶段
|
|
||||||
FROM node:18-alpine as builder
|
|
||||||
|
|
||||||
# 设置工作目录
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
# 复制 package.json 和 package-lock.json
|
|
||||||
COPY package*.json ./
|
|
||||||
|
|
||||||
# 安装依赖(使用 --legacy-peer-deps 解决依赖冲突)
|
|
||||||
RUN npm install --legacy-peer-deps
|
|
||||||
|
|
||||||
# 复制源代码
|
|
||||||
COPY . .
|
|
||||||
|
|
||||||
# 构建项目
|
|
||||||
RUN npm run build
|
|
||||||
|
|
||||||
# 生产阶段
|
|
||||||
FROM nginx:alpine
|
|
||||||
|
|
||||||
# 复制构建产物到 Nginx 静态目录
|
|
||||||
COPY --from=builder /app/dist /usr/share/nginx/html
|
|
||||||
|
|
||||||
# 复制自定义 Nginx 配置
|
|
||||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
|
||||||
|
|
||||||
# 暴露端口
|
|
||||||
EXPOSE 80
|
|
||||||
|
|
||||||
# 启动 Nginx
|
|
||||||
CMD ["nginx", "-g", "daemon off;"]
|
|
||||||
33
nginx.conf
33
nginx.conf
|
|
@ -1,33 +0,0 @@
|
||||||
server {
|
|
||||||
listen 80;
|
|
||||||
server_name localhost;
|
|
||||||
|
|
||||||
root /usr/share/nginx/html;
|
|
||||||
index index.html;
|
|
||||||
|
|
||||||
location / {
|
|
||||||
try_files $uri $uri/ /index.html;
|
|
||||||
}
|
|
||||||
|
|
||||||
# API 代理配置
|
|
||||||
location /api {
|
|
||||||
proxy_pass http://api:7001;
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
|
||||||
}
|
|
||||||
|
|
||||||
# 静态文件缓存配置
|
|
||||||
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
|
|
||||||
expires 1y;
|
|
||||||
add_header Cache-Control "public, immutable";
|
|
||||||
}
|
|
||||||
|
|
||||||
# 错误页面配置
|
|
||||||
error_page 404 /index.html;
|
|
||||||
error_page 500 502 503 504 /50x.html;
|
|
||||||
location = /50x.html {
|
|
||||||
root /usr/share/nginx/html;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
10
package.json
10
package.json
|
|
@ -4,7 +4,6 @@
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "max build",
|
"build": "max build",
|
||||||
"dev": "max dev",
|
"dev": "max dev",
|
||||||
"fix:openapi2ts": "sed -i '' 's/\r$//' ./node_modules/@umijs/openapi/dist/cli.js",
|
|
||||||
"format": "prettier --cache --write .",
|
"format": "prettier --cache --write .",
|
||||||
"postinstall": "max setup",
|
"postinstall": "max setup",
|
||||||
"openapi2ts": "openapi2ts",
|
"openapi2ts": "openapi2ts",
|
||||||
|
|
@ -17,25 +16,20 @@
|
||||||
"@ant-design/icons": "^5.0.1",
|
"@ant-design/icons": "^5.0.1",
|
||||||
"@ant-design/pro-components": "^2.4.4",
|
"@ant-design/pro-components": "^2.4.4",
|
||||||
"@fingerprintjs/fingerprintjs": "^4.6.2",
|
"@fingerprintjs/fingerprintjs": "^4.6.2",
|
||||||
"@monaco-editor/react": "^4.7.0",
|
|
||||||
"@tinymce/tinymce-react": "^6.3.0",
|
|
||||||
"@umijs/max": "^4.4.4",
|
"@umijs/max": "^4.4.4",
|
||||||
"@umijs/max-plugin-openapi": "^2.0.3",
|
"@umijs/max-plugin-openapi": "^2.0.3",
|
||||||
"@umijs/plugin-openapi": "^1.3.3",
|
"@umijs/plugin-openapi": "^1.3.3",
|
||||||
"antd": "^5.4.0",
|
"antd": "^5.4.0",
|
||||||
"dayjs": "^1.11.9",
|
"dayjs": "^1.11.9",
|
||||||
"echarts": "^6.0.0",
|
"echarts": "^5.6.0",
|
||||||
"echarts-for-react": "^3.0.5",
|
"echarts-for-react": "^3.0.2",
|
||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
"i18n-iso-countries": "^7.14.0",
|
|
||||||
"print-js": "^1.6.0",
|
"print-js": "^1.6.0",
|
||||||
"react-json-view": "^1.21.3",
|
|
||||||
"react-phone-input-2": "^2.15.1",
|
"react-phone-input-2": "^2.15.1",
|
||||||
"react-toastify": "^11.0.5",
|
"react-toastify": "^11.0.5",
|
||||||
"xlsx": "^0.18.5"
|
"xlsx": "^0.18.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/file-saver": "^2.0.7",
|
|
||||||
"@types/react": "^18.0.33",
|
"@types/react": "^18.0.33",
|
||||||
"@types/react-dom": "^18.0.11",
|
"@types/react-dom": "^18.0.11",
|
||||||
"code-inspector-plugin": "^1.2.10",
|
"code-inspector-plugin": "^1.2.10",
|
||||||
|
|
|
||||||
32127
public/world.json
32127
public/world.json
File diff suppressed because it is too large
Load Diff
|
|
@ -1,52 +1,16 @@
|
||||||
export default (initialState: any) => {
|
export default (initialState: any) => {
|
||||||
const isSuper = initialState?.user?.isSuper ?? false;
|
const isSuper = initialState?.user?.isSuper ?? false;
|
||||||
const isAdmin = initialState?.user?.Admin ?? false;
|
const isAdmin = initialState?.user?.Admin ?? false;
|
||||||
const canSeeOrganiza =
|
const canSeeOrganiza = (isSuper || isAdmin) || (initialState?.user?.permissions?.includes('organiza') ?? false);
|
||||||
isSuper ||
|
const canSeeProduct = (isSuper || isAdmin) || (initialState?.user?.permissions?.includes('product') ?? false);
|
||||||
isAdmin ||
|
const canSeeStock = (isSuper || isAdmin) || (initialState?.user?.permissions?.includes('stock') ?? false);
|
||||||
(initialState?.user?.permissions?.includes('organiza') ?? false);
|
const canSeeOrder = (isSuper || isAdmin) ||
|
||||||
const canSeeProduct =
|
((initialState?.user?.permissions?.includes('order') ?? false) || (initialState?.user?.permissions?.includes('order-10-days') ?? false));
|
||||||
isSuper ||
|
const canSeeCustomer = (isSuper || isAdmin) || (initialState?.user?.permissions?.includes('customer') ?? false);
|
||||||
isAdmin ||
|
const canSeeLogistics = (isSuper || isAdmin) || (initialState?.user?.permissions?.includes('logistics') ?? false);
|
||||||
(initialState?.user?.permissions?.includes('product') ?? false);
|
const canSeeStatistics = (isSuper || isAdmin) || (initialState?.user?.permissions?.includes('statistics') ?? false);
|
||||||
const canSeeStock =
|
const canSeeSite = (isSuper || isAdmin) || (initialState?.user?.permissions?.includes('site') ?? false);
|
||||||
isSuper ||
|
|
||||||
isAdmin ||
|
|
||||||
(initialState?.user?.permissions?.includes('stock') ?? false);
|
|
||||||
const canSeeOrder =
|
|
||||||
isSuper ||
|
|
||||||
isAdmin ||
|
|
||||||
(initialState?.user?.permissions?.includes('order') ?? false) ||
|
|
||||||
(initialState?.user?.permissions?.includes('order-10-days') ?? false);
|
|
||||||
const canSeeCustomer =
|
|
||||||
isSuper ||
|
|
||||||
isAdmin ||
|
|
||||||
(initialState?.user?.permissions?.includes('customer') ?? false);
|
|
||||||
const canSeeLogistics =
|
|
||||||
isSuper ||
|
|
||||||
isAdmin ||
|
|
||||||
(initialState?.user?.permissions?.includes('logistics') ?? false);
|
|
||||||
const canSeeStatistics =
|
|
||||||
isSuper ||
|
|
||||||
isAdmin ||
|
|
||||||
(initialState?.user?.permissions?.includes('statistics') ?? false);
|
|
||||||
const canSeeSite =
|
|
||||||
isSuper ||
|
|
||||||
isAdmin ||
|
|
||||||
(initialState?.user?.permissions?.includes('site') ?? false);
|
|
||||||
const canSeeDict =
|
|
||||||
isSuper ||
|
|
||||||
isAdmin ||
|
|
||||||
(initialState?.user?.permissions?.includes('dict') ?? false);
|
|
||||||
const canSeeTemplate =
|
|
||||||
isSuper ||
|
|
||||||
isAdmin ||
|
|
||||||
(initialState?.user?.permissions?.includes('template') ?? false);
|
|
||||||
const canSeeArea =
|
|
||||||
isSuper ||
|
|
||||||
isAdmin ||
|
|
||||||
(initialState?.user?.permissions?.includes('area') ?? false);
|
|
||||||
const canSeeSystem = canSeeDict || canSeeTemplate;
|
|
||||||
return {
|
return {
|
||||||
canSeeOrganiza,
|
canSeeOrganiza,
|
||||||
canSeeProduct,
|
canSeeProduct,
|
||||||
|
|
@ -56,9 +20,5 @@ export default (initialState: any) => {
|
||||||
canSeeLogistics,
|
canSeeLogistics,
|
||||||
canSeeStatistics,
|
canSeeStatistics,
|
||||||
canSeeSite,
|
canSeeSite,
|
||||||
canSeeDict,
|
|
||||||
canSeeTemplate,
|
|
||||||
canSeeArea,
|
|
||||||
canSeeSystem,
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
||||||
13
src/app.tsx
13
src/app.tsx
|
|
@ -15,7 +15,7 @@ import { usercontrollerGetuser } from './servers/api/user';
|
||||||
// 设置 dayjs 全局语言为中文
|
// 设置 dayjs 全局语言为中文
|
||||||
dayjs.locale('zh-cn');
|
dayjs.locale('zh-cn');
|
||||||
|
|
||||||
// 全局初始化数据配置,用于 Layout 用户信息和权限初始化
|
// 全局初始化数据配置,用于 Layout 用户信息和权限初始化
|
||||||
// 更多信息见文档:https://umijs.org/docs/api/runtime-config#getinitialstate
|
// 更多信息见文档:https://umijs.org/docs/api/runtime-config#getinitialstate
|
||||||
export async function getInitialState(): Promise<{
|
export async function getInitialState(): Promise<{
|
||||||
user?: Record<string, any>;
|
user?: Record<string, any>;
|
||||||
|
|
@ -56,15 +56,12 @@ export const layout = (): ProLayoutProps => {
|
||||||
menu: {
|
menu: {
|
||||||
locale: false,
|
locale: false,
|
||||||
},
|
},
|
||||||
menuDataRender: (menuData) => {
|
|
||||||
return menuData;
|
|
||||||
},
|
|
||||||
layout: 'mix',
|
layout: 'mix',
|
||||||
actionsRender: () => (
|
actionsRender: () => (
|
||||||
<Dropdown key="avatar" menu={{ items }}>
|
<Dropdown key="avatar" menu={{ items }}>
|
||||||
<div style={{ cursor: 'pointer' }}>
|
<div style={{ cursor: 'pointer' }}>
|
||||||
<Avatar size="large" icon={<UserOutlined />} />
|
<Avatar size="large" icon={<UserOutlined />} />
|
||||||
<span style={{ marginLeft: 8 }}>{initialState?.user?.name}</span>
|
<span style={{ marginLeft: 8 }}>{initialState?.name}</span>
|
||||||
</div>
|
</div>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
),
|
),
|
||||||
|
|
@ -72,7 +69,7 @@ export const layout = (): ProLayoutProps => {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const request: RequestConfig = {
|
export const request: RequestConfig = {
|
||||||
baseURL: '/api', // baseURL: UMI_APP_API_URL,
|
baseURL: UMI_APP_API_URL,
|
||||||
requestInterceptors: [
|
requestInterceptors: [
|
||||||
(url: string, options: any) => {
|
(url: string, options: any) => {
|
||||||
const token = localStorage.getItem('token');
|
const token = localStorage.getItem('token');
|
||||||
|
|
@ -102,11 +99,11 @@ export const request: RequestConfig = {
|
||||||
export const onRouteChange = ({ location }: { location: Location }) => {
|
export const onRouteChange = ({ location }: { location: Location }) => {
|
||||||
const token = localStorage.getItem('token');
|
const token = localStorage.getItem('token');
|
||||||
|
|
||||||
// 白名单,不需要登录的页面
|
// 白名单,不需要登录的页面
|
||||||
const whiteList = ['/login', '/track'];
|
const whiteList = ['/login', '/track'];
|
||||||
|
|
||||||
if (!token && !whiteList.includes(location.pathname)) {
|
if (!token && !whiteList.includes(location.pathname)) {
|
||||||
// 没有 token 且不在白名单内,跳转到登录页
|
// 没有 token 且不在白名单内,跳转到登录页
|
||||||
history.push('/login');
|
history.push('/login');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,38 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
interface AddressProps {
|
|
||||||
address: {
|
|
||||||
address_1?: string;
|
|
||||||
address_2?: string;
|
|
||||||
city?: string;
|
|
||||||
state?: string;
|
|
||||||
postcode?: string;
|
|
||||||
country?: string;
|
|
||||||
phone?: string;
|
|
||||||
};
|
|
||||||
style?: React.CSSProperties;
|
|
||||||
}
|
|
||||||
|
|
||||||
const Address: React.FC<AddressProps> = ({ address, style }) => {
|
|
||||||
if (!address) {
|
|
||||||
return <span>-</span>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { address_1, address_2, city, state, postcode, country, phone } =
|
|
||||||
address;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div style={{ fontSize: 12, ...style }}>
|
|
||||||
<div>
|
|
||||||
{address_1} {address_2}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
{city}, {state}, {postcode}
|
|
||||||
</div>
|
|
||||||
<div>{country}</div>
|
|
||||||
<div>{phone}</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Address;
|
|
||||||
|
|
@ -1,117 +0,0 @@
|
||||||
import { sitecontrollerAll } from '@/servers/api/site';
|
|
||||||
import { SyncOutlined } from '@ant-design/icons';
|
|
||||||
import {
|
|
||||||
ActionType,
|
|
||||||
DrawerForm,
|
|
||||||
ProForm,
|
|
||||||
ProFormDateRangePicker,
|
|
||||||
ProFormSelect,
|
|
||||||
} from '@ant-design/pro-components';
|
|
||||||
import { Button } from 'antd';
|
|
||||||
import dayjs from 'dayjs';
|
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
// 定义SyncForm组件的props类型
|
|
||||||
interface SyncFormProps {
|
|
||||||
tableRef: React.MutableRefObject<ActionType | undefined>;
|
|
||||||
onFinish: (values: any) => Promise<void>;
|
|
||||||
siteId?: string;
|
|
||||||
dateRange?: [dayjs.Dayjs, dayjs.Dayjs];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 同步订单的表单组件
|
|
||||||
* @param {SyncFormProps} props 组件属性
|
|
||||||
* @returns {React.ReactElement} 抽屉表单
|
|
||||||
*/
|
|
||||||
const SyncForm: React.FC<SyncFormProps> = ({
|
|
||||||
tableRef,
|
|
||||||
onFinish,
|
|
||||||
siteId,
|
|
||||||
dateRange,
|
|
||||||
}) => {
|
|
||||||
// 使用 antd 的 App 组件提供的 message API
|
|
||||||
const [loading, setLoading] = React.useState(false);
|
|
||||||
|
|
||||||
if (siteId) {
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
key="syncSite"
|
|
||||||
type="primary"
|
|
||||||
loading={loading}
|
|
||||||
onClick={async () => {
|
|
||||||
try {
|
|
||||||
setLoading(true);
|
|
||||||
await onFinish({ siteId: Number(siteId) });
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<SyncOutlined />
|
|
||||||
同步订单
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 返回一个抽屉表单
|
|
||||||
return (
|
|
||||||
<DrawerForm<API.ordercontrollerSyncorderParams>
|
|
||||||
initialValues={{
|
|
||||||
dateRange: [dayjs().subtract(1, 'week'), dayjs()],
|
|
||||||
}}
|
|
||||||
title="同步订单"
|
|
||||||
// 表单的触发器,一个带图标的按钮
|
|
||||||
trigger={
|
|
||||||
<Button key="syncSite" type="primary">
|
|
||||||
<SyncOutlined />
|
|
||||||
同步订单
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
// 自动聚焦第一个输入框
|
|
||||||
autoFocusFirstInput
|
|
||||||
// 抽屉关闭时销毁内部组件
|
|
||||||
drawerProps={{
|
|
||||||
destroyOnHidden: true,
|
|
||||||
}}
|
|
||||||
// 表单提交成功后的回调
|
|
||||||
onFinish={onFinish}
|
|
||||||
>
|
|
||||||
<ProForm.Group>
|
|
||||||
{/* 站点选择框 */}
|
|
||||||
<ProFormSelect
|
|
||||||
name="siteId"
|
|
||||||
width="lg"
|
|
||||||
label="站点"
|
|
||||||
placeholder="请选择站点"
|
|
||||||
// 异步请求站点列表数据
|
|
||||||
request={async () => {
|
|
||||||
const { data = [] } = await sitecontrollerAll();
|
|
||||||
// 将返回的数据格式化为 ProFormSelect 需要的格式
|
|
||||||
return data.map((item: any) => ({
|
|
||||||
label: item.name || String(item.id),
|
|
||||||
value: item.id,
|
|
||||||
}));
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ProFormDateRangePicker
|
|
||||||
name="dateRange"
|
|
||||||
label="同步日期范围"
|
|
||||||
placeholder={['开始日期', '结束日期']}
|
|
||||||
transform={(value) => {
|
|
||||||
return {
|
|
||||||
dateRange: value,
|
|
||||||
};
|
|
||||||
}}
|
|
||||||
fieldProps={{
|
|
||||||
showTime: false,
|
|
||||||
style: { width: '100%' },
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</ProForm.Group>
|
|
||||||
</DrawerForm>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default SyncForm;
|
|
||||||
|
|
@ -1,91 +0,0 @@
|
||||||
import { message } from 'antd';
|
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
// 定义同步结果的数据类型
|
|
||||||
export interface SyncResultData {
|
|
||||||
total?: number;
|
|
||||||
processed?: number;
|
|
||||||
synced?: number;
|
|
||||||
created?: number;
|
|
||||||
updated?: number;
|
|
||||||
errors?: Array<{
|
|
||||||
identifier: string;
|
|
||||||
error: string;
|
|
||||||
}>;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 定义组件的 Props 类型
|
|
||||||
interface SyncResultMessageProps {
|
|
||||||
data?: SyncResultData;
|
|
||||||
entityType?: string; // 实体类型,如"订单"、"客户"等
|
|
||||||
}
|
|
||||||
|
|
||||||
// 显示同步结果的函数
|
|
||||||
export const showSyncResult = (
|
|
||||||
data: SyncResultData,
|
|
||||||
entityType: string = '订单',
|
|
||||||
) => {
|
|
||||||
const result = data || {};
|
|
||||||
const {
|
|
||||||
total = 0,
|
|
||||||
processed = 0,
|
|
||||||
synced = 0,
|
|
||||||
created = 0,
|
|
||||||
updated = 0,
|
|
||||||
errors = [],
|
|
||||||
} = result;
|
|
||||||
|
|
||||||
// 构建结果消息
|
|
||||||
let resultMessage = `同步完成!共处理 ${processed} 个${entityType}(总数 ${total} 个):`;
|
|
||||||
if (created > 0) resultMessage += ` 新建 ${created} 个`;
|
|
||||||
if (updated > 0) resultMessage += ` 更新 ${updated} 个`;
|
|
||||||
if (synced > 0) resultMessage += ` 同步成功 ${synced} 个`;
|
|
||||||
if (errors.length > 0) resultMessage += ` 失败 ${errors.length} 个`;
|
|
||||||
|
|
||||||
// 根据是否有错误显示不同的消息类型
|
|
||||||
if (errors.length > 0) {
|
|
||||||
// 如果有错误,显示警告消息
|
|
||||||
message.warning({
|
|
||||||
content: (
|
|
||||||
<div>
|
|
||||||
<div>{resultMessage}</div>
|
|
||||||
<div style={{ marginTop: 8, fontSize: 12, color: '#faad14' }}>
|
|
||||||
失败详情:
|
|
||||||
{errors
|
|
||||||
.slice(0, 3)
|
|
||||||
.map((err: any) => `${err.identifier}: ${err.error}`)
|
|
||||||
.join(', ')}
|
|
||||||
{errors.length > 3 && ` 等 ${errors.length - 3} 个错误...`}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
duration: 8,
|
|
||||||
key: 'sync-result',
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// 完全成功
|
|
||||||
message.success({
|
|
||||||
content: resultMessage,
|
|
||||||
duration: 4,
|
|
||||||
key: 'sync-result',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 同步结果显示组件
|
|
||||||
const SyncResultMessage: React.FC<SyncResultMessageProps> = ({
|
|
||||||
data,
|
|
||||||
entityType = '订单',
|
|
||||||
}) => {
|
|
||||||
// 当组件挂载时显示结果
|
|
||||||
React.useEffect(() => {
|
|
||||||
if (data) {
|
|
||||||
showSyncResult(data, entityType);
|
|
||||||
}
|
|
||||||
}, [data, entityType]);
|
|
||||||
|
|
||||||
// 这个组件不渲染任何内容,只用于显示消息
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default SyncResultMessage;
|
|
||||||
|
|
@ -116,5 +116,5 @@ export const ORDER_STATUS_ENUM: ProSchemaValueEnumObj = {
|
||||||
refund_cancelled: {
|
refund_cancelled: {
|
||||||
text: '已取消退款',
|
text: '已取消退款',
|
||||||
status: 'refund_cancelled',
|
status: 'refund_cancelled',
|
||||||
},
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import FingerprintJS from '@fingerprintjs/fingerprintjs';
|
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
import FingerprintJS from '@fingerprintjs/fingerprintjs';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hook: 获取设备指纹(visitorId)
|
* Hook: 获取设备指纹(visitorId)
|
||||||
|
|
@ -29,5 +29,5 @@ export function useDeviceFingerprint() {
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return fingerprint; // 初始为 null,加载后返回指纹 ID
|
return fingerprint; // 初始为 null,加载后返回指纹 ID
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,92 +0,0 @@
|
||||||
import { sitecontrollerAll } from '@/servers/api/site';
|
|
||||||
import { useEffect, useState } from 'react';
|
|
||||||
|
|
||||||
// 站点数据的类型定义
|
|
||||||
interface Site {
|
|
||||||
id: number;
|
|
||||||
name: string;
|
|
||||||
[key: string]: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 自定义 Hook:管理站点数据
|
|
||||||
const useSites = () => {
|
|
||||||
// 添加站点数据状态
|
|
||||||
const [sites, setSites] = useState<Site[]>([]);
|
|
||||||
|
|
||||||
// 添加加载状态
|
|
||||||
const [loading, setLoading] = useState<boolean>(false);
|
|
||||||
|
|
||||||
// 添加错误状态
|
|
||||||
const [error, setError] = useState<string | null>(null);
|
|
||||||
|
|
||||||
// 获取站点数据
|
|
||||||
const fetchSites = async () => {
|
|
||||||
// 设置加载状态为 true
|
|
||||||
setLoading(true);
|
|
||||||
// 清空之前的错误信息
|
|
||||||
setError(null);
|
|
||||||
try {
|
|
||||||
// 调用 API 获取所有站点数据
|
|
||||||
const { data, success } = await sitecontrollerAll();
|
|
||||||
// 判断请求是否成功
|
|
||||||
if (success) {
|
|
||||||
// 将站点数据保存到状态中
|
|
||||||
setSites(data || []);
|
|
||||||
} else {
|
|
||||||
// 如果请求失败,设置错误信息
|
|
||||||
setError('获取站点数据失败');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
// 捕获异常并打印错误日志
|
|
||||||
console.error('获取站点数据失败:', error);
|
|
||||||
// 设置错误信息
|
|
||||||
setError('获取站点数据时发生错误');
|
|
||||||
} finally {
|
|
||||||
// 无论成功与否,都将加载状态设置为 false
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 根据站点ID获取站点名称
|
|
||||||
const getSiteName = (siteId: number | undefined | null) => {
|
|
||||||
// 如果站点ID不存在,返回默认值
|
|
||||||
if (!siteId) return '-';
|
|
||||||
// 如果站点ID是字符串类型,直接返回
|
|
||||||
if (typeof siteId === 'string') {
|
|
||||||
return siteId;
|
|
||||||
}
|
|
||||||
// 在站点列表中查找对应的站点
|
|
||||||
const site = sites.find((s) => s.id === siteId);
|
|
||||||
// 如果找到站点,返回站点名称;否则返回站点ID的字符串形式
|
|
||||||
return site ? site.name : String(siteId);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 根据站点ID获取站点对象
|
|
||||||
const getSiteById = (siteId: number | undefined | null) => {
|
|
||||||
// 如果站点ID不存在,返回 null
|
|
||||||
if (!siteId) return null;
|
|
||||||
// 在站点列表中查找对应的站点
|
|
||||||
const site = sites.find((s) => s.id === siteId);
|
|
||||||
// 返回找到的站点对象,如果找不到则返回 null
|
|
||||||
return site || null;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 组件加载时获取站点数据
|
|
||||||
useEffect(() => {
|
|
||||||
// 调用获取站点数据的函数
|
|
||||||
fetchSites();
|
|
||||||
}, []); // 空依赖数组表示只在组件挂载时执行一次
|
|
||||||
|
|
||||||
// 返回站点数据和相关方法
|
|
||||||
return {
|
|
||||||
sites, // 站点数据列表
|
|
||||||
loading, // 加载状态
|
|
||||||
error, // 错误信息
|
|
||||||
fetchSites, // 重新获取站点数据的方法
|
|
||||||
getSiteName, // 根据ID获取站点名称的方法
|
|
||||||
getSiteById, // 根据ID获取站点对象的方法
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
// 导出 useSites Hook
|
|
||||||
export default useSites;
|
|
||||||
|
|
@ -1,196 +0,0 @@
|
||||||
import {
|
|
||||||
ActionType,
|
|
||||||
DrawerForm,
|
|
||||||
ProColumns,
|
|
||||||
ProFormInstance,
|
|
||||||
ProFormSelect,
|
|
||||||
ProTable,
|
|
||||||
} from '@ant-design/pro-components';
|
|
||||||
import { request } from '@umijs/max';
|
|
||||||
import { Button, message, Popconfirm, Space } from 'antd';
|
|
||||||
import React, { useEffect, useRef, useState } from 'react';
|
|
||||||
|
|
||||||
interface AreaItem {
|
|
||||||
id: number;
|
|
||||||
name: string;
|
|
||||||
code: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Country {
|
|
||||||
code: string;
|
|
||||||
name: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const AreaList: React.FC = () => {
|
|
||||||
const actionRef = useRef<ActionType>();
|
|
||||||
const formRef = useRef<ProFormInstance>();
|
|
||||||
const [open, setOpen] = useState(false);
|
|
||||||
const [editing, setEditing] = useState<AreaItem | null>(null);
|
|
||||||
const [countries, setCountries] = useState<Country[]>([]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!open) return;
|
|
||||||
if (editing) {
|
|
||||||
formRef.current?.setFieldsValue(editing);
|
|
||||||
} else {
|
|
||||||
formRef.current?.resetFields();
|
|
||||||
}
|
|
||||||
}, [open, editing]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const fetchCountries = async () => {
|
|
||||||
try {
|
|
||||||
const resp = await request('/area/countries', { method: 'GET' });
|
|
||||||
const { success, data, message: errMsg } = resp as any;
|
|
||||||
if (!success) throw new Error(errMsg || '获取国家列表失败');
|
|
||||||
setCountries(data || []);
|
|
||||||
} catch (e: any) {
|
|
||||||
message.error(e.message || '获取国家列表失败');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
fetchCountries();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const columns: ProColumns<AreaItem>[] = [
|
|
||||||
{
|
|
||||||
title: 'ID',
|
|
||||||
dataIndex: 'id',
|
|
||||||
width: 80,
|
|
||||||
sorter: true,
|
|
||||||
hideInSearch: true,
|
|
||||||
},
|
|
||||||
{ title: '名称', dataIndex: 'name', width: 220 },
|
|
||||||
{ title: '编码', dataIndex: 'code', width: 160 },
|
|
||||||
{
|
|
||||||
title: '操作',
|
|
||||||
dataIndex: 'actions',
|
|
||||||
width: 240,
|
|
||||||
hideInSearch: true,
|
|
||||||
render: (_, row) => (
|
|
||||||
<Space>
|
|
||||||
<Button
|
|
||||||
size="small"
|
|
||||||
onClick={() => {
|
|
||||||
setEditing(row);
|
|
||||||
setOpen(true);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
编辑
|
|
||||||
</Button>
|
|
||||||
<Popconfirm
|
|
||||||
title="删除区域"
|
|
||||||
description="确认删除该区域?"
|
|
||||||
onConfirm={async () => {
|
|
||||||
try {
|
|
||||||
await request(`/area/${row.id}`, {
|
|
||||||
method: 'DELETE',
|
|
||||||
});
|
|
||||||
message.success('删除成功');
|
|
||||||
actionRef.current?.reload();
|
|
||||||
} catch (e: any) {
|
|
||||||
message.error(e?.message || '删除失败');
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button size="small" type="primary" danger>
|
|
||||||
删除
|
|
||||||
</Button>
|
|
||||||
</Popconfirm>
|
|
||||||
</Space>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const tableRequest = async (params: Record<string, any>) => {
|
|
||||||
try {
|
|
||||||
const { current = 1, pageSize = 10, keyword } = params;
|
|
||||||
const resp = await request('/area', {
|
|
||||||
method: 'GET',
|
|
||||||
params: {
|
|
||||||
currentPage: current,
|
|
||||||
pageSize,
|
|
||||||
keyword: keyword || undefined,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const { success, data, message: errMsg } = resp as any;
|
|
||||||
if (!success) throw new Error(errMsg || '获取失败');
|
|
||||||
return {
|
|
||||||
data: (data?.list ?? []) as AreaItem[],
|
|
||||||
total: data?.total ?? 0,
|
|
||||||
success: true,
|
|
||||||
};
|
|
||||||
} catch (e: any) {
|
|
||||||
message.error(e?.message || '获取失败');
|
|
||||||
return { data: [], total: 0, success: false };
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSubmit = async (values: AreaItem) => {
|
|
||||||
try {
|
|
||||||
if (editing) {
|
|
||||||
await request(`/area/${editing.id}`, {
|
|
||||||
method: 'PUT',
|
|
||||||
data: values,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
await request('/area', {
|
|
||||||
method: 'POST',
|
|
||||||
data: values,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
message.success('提交成功');
|
|
||||||
setOpen(false);
|
|
||||||
setEditing(null);
|
|
||||||
actionRef.current?.reload();
|
|
||||||
return true;
|
|
||||||
} catch (e: any) {
|
|
||||||
message.error(e?.message || '提交失败');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<ProTable<AreaItem>
|
|
||||||
actionRef={actionRef}
|
|
||||||
rowKey="id"
|
|
||||||
columns={columns}
|
|
||||||
request={tableRequest}
|
|
||||||
toolBarRender={() => [
|
|
||||||
<Button
|
|
||||||
key="new"
|
|
||||||
type="primary"
|
|
||||||
onClick={() => {
|
|
||||||
setEditing(null);
|
|
||||||
setOpen(true);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
新增区域
|
|
||||||
</Button>,
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<DrawerForm<AreaItem>
|
|
||||||
title={editing ? '编辑区域' : '新增区域'}
|
|
||||||
open={open}
|
|
||||||
onOpenChange={setOpen}
|
|
||||||
formRef={formRef}
|
|
||||||
onFinish={handleSubmit}
|
|
||||||
>
|
|
||||||
<ProFormSelect
|
|
||||||
name="code"
|
|
||||||
label="国家/地区"
|
|
||||||
options={countries.map((c) => ({
|
|
||||||
label: `${c.name}(${c.code})`,
|
|
||||||
value: c.code,
|
|
||||||
}))}
|
|
||||||
placeholder="请选择国家/地区"
|
|
||||||
rules={[{ required: true, message: '国家/地区为必填项' }]}
|
|
||||||
showSearch
|
|
||||||
/>
|
|
||||||
</DrawerForm>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AreaList;
|
|
||||||
|
|
@ -1,134 +0,0 @@
|
||||||
import { request } from '@umijs/max';
|
|
||||||
import { Spin, message } from 'antd';
|
|
||||||
import ReactECharts from 'echarts-for-react';
|
|
||||||
import { MapChart } from 'echarts/charts';
|
|
||||||
import { TooltipComponent, VisualMapComponent } from 'echarts/components';
|
|
||||||
import * as echarts from 'echarts/core';
|
|
||||||
import { CanvasRenderer } from 'echarts/renderers';
|
|
||||||
import * as countries from 'i18n-iso-countries';
|
|
||||||
import React, { useEffect, useState } from 'react';
|
|
||||||
|
|
||||||
// 注册 ECharts 组件
|
|
||||||
echarts.use([TooltipComponent, VisualMapComponent, MapChart, CanvasRenderer]);
|
|
||||||
|
|
||||||
// 注册 i18n-iso-countries 语言包
|
|
||||||
countries.registerLocale(require('i18n-iso-countries/langs/en.json'));
|
|
||||||
|
|
||||||
interface AreaItem {
|
|
||||||
id: number;
|
|
||||||
name: string; // 中文名
|
|
||||||
code: string; // 国家代码
|
|
||||||
}
|
|
||||||
|
|
||||||
const AreaMap: React.FC = () => {
|
|
||||||
const [option, setOption] = useState({});
|
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const fetchAndSetMapData = async () => {
|
|
||||||
try {
|
|
||||||
// 1. 动态加载 world.json 地图数据
|
|
||||||
const worldMapResponse = await fetch('/world.json');
|
|
||||||
const worldMap = await worldMapResponse.json();
|
|
||||||
echarts.registerMap('world', worldMap);
|
|
||||||
|
|
||||||
// 2. 从后端获取已存储的区域列表
|
|
||||||
const areaResponse = await request('/area', {
|
|
||||||
method: 'GET',
|
|
||||||
params: {
|
|
||||||
currentPage: 1,
|
|
||||||
pageSize: 9999,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
if (!areaResponse.success) {
|
|
||||||
throw new Error(areaResponse.message || '获取区域列表失败');
|
|
||||||
}
|
|
||||||
const savedAreas: AreaItem[] = areaResponse.data?.list || [];
|
|
||||||
|
|
||||||
// 3. 将后端数据转换为 ECharts 需要的格式
|
|
||||||
const mapData = savedAreas.map((area) => {
|
|
||||||
let nameEn = countries.getName(area.code, 'en');
|
|
||||||
return {
|
|
||||||
name: nameEn || area.code,
|
|
||||||
value: 1,
|
|
||||||
chineseName: area.name,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
// 4. 配置 ECharts 地图选项
|
|
||||||
const mapOption = {
|
|
||||||
tooltip: {
|
|
||||||
trigger: 'item',
|
|
||||||
formatter: (params: any) => {
|
|
||||||
if (params.data && params.data.chineseName) {
|
|
||||||
return `${params.data.chineseName}`;
|
|
||||||
}
|
|
||||||
return `${params.name}`;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
visualMap: {
|
|
||||||
left: 'left',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
inRange: {
|
|
||||||
color: ['#f0f0f0', '#1890ff'],
|
|
||||||
},
|
|
||||||
calculable: false,
|
|
||||||
show: false,
|
|
||||||
},
|
|
||||||
series: [
|
|
||||||
{
|
|
||||||
name: 'World Map',
|
|
||||||
type: 'map',
|
|
||||||
map: 'world',
|
|
||||||
roam: true,
|
|
||||||
emphasis: {
|
|
||||||
label: {
|
|
||||||
show: false,
|
|
||||||
},
|
|
||||||
itemStyle: {
|
|
||||||
areaColor: '#ffc107',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
data: mapData,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
setOption(mapOption);
|
|
||||||
} catch (error: any) {
|
|
||||||
message.error(`加载地图数据失败: ${error.message}`);
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
fetchAndSetMapData();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
if (loading) {
|
|
||||||
return (
|
|
||||||
<Spin
|
|
||||||
size="large"
|
|
||||||
style={{
|
|
||||||
display: 'flex',
|
|
||||||
justifyContent: 'center',
|
|
||||||
alignItems: 'center',
|
|
||||||
height: '100%',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ReactECharts
|
|
||||||
echarts={echarts}
|
|
||||||
option={option}
|
|
||||||
style={{ height: '80vh', width: '100%' }}
|
|
||||||
notMerge={true}
|
|
||||||
lazyUpdate={true}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AreaMap;
|
|
||||||
|
|
@ -1,347 +0,0 @@
|
||||||
import {
|
|
||||||
productcontrollerCreatecategory,
|
|
||||||
productcontrollerCreatecategoryattribute,
|
|
||||||
productcontrollerDeletecategory,
|
|
||||||
productcontrollerDeletecategoryattribute,
|
|
||||||
productcontrollerGetcategoriesall,
|
|
||||||
productcontrollerGetcategoryattributes,
|
|
||||||
productcontrollerUpdatecategory,
|
|
||||||
} from '@/servers/api/product';
|
|
||||||
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 { notAttributes } 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) => !notAttributes.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;
|
|
||||||
|
|
@ -1,254 +0,0 @@
|
||||||
import { ordercontrollerGetorders } from '@/servers/api/order';
|
|
||||||
import {
|
|
||||||
App,
|
|
||||||
Col,
|
|
||||||
Modal,
|
|
||||||
Row,
|
|
||||||
Spin,
|
|
||||||
Statistic,
|
|
||||||
Table,
|
|
||||||
Tag,
|
|
||||||
Typography,
|
|
||||||
} from 'antd';
|
|
||||||
import dayjs from 'dayjs';
|
|
||||||
import { useState } from 'react';
|
|
||||||
|
|
||||||
const { Text, Title } = Typography;
|
|
||||||
|
|
||||||
interface HistoryOrdersProps {
|
|
||||||
customer: API.UnifiedCustomerDTO;
|
|
||||||
siteId?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface OrderStats {
|
|
||||||
totalOrders: number;
|
|
||||||
totalAmount: number;
|
|
||||||
yooneOrders: number;
|
|
||||||
yooneAmount: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
const HistoryOrders: React.FC<HistoryOrdersProps> = ({ customer, siteId }) => {
|
|
||||||
const { message } = App.useApp();
|
|
||||||
const [modalVisible, setModalVisible] = useState(false);
|
|
||||||
const [orders, setOrders] = useState<any[]>([]);
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const [stats, setStats] = useState<OrderStats>({
|
|
||||||
totalOrders: 0,
|
|
||||||
totalAmount: 0,
|
|
||||||
yooneOrders: 0,
|
|
||||||
yooneAmount: 0,
|
|
||||||
});
|
|
||||||
|
|
||||||
// 计算订单统计信息
|
|
||||||
const calculateStats = (orders: any[]) => {
|
|
||||||
let totalOrders = 0;
|
|
||||||
let totalAmount = 0;
|
|
||||||
let yooneOrders = 0;
|
|
||||||
let yooneAmount = 0;
|
|
||||||
|
|
||||||
orders.forEach((order) => {
|
|
||||||
totalOrders++;
|
|
||||||
// total是字符串,需要转换为数字
|
|
||||||
const orderTotal = parseFloat(order.total || '0');
|
|
||||||
totalAmount += orderTotal;
|
|
||||||
|
|
||||||
// 检查订单中是否包含yoone商品
|
|
||||||
let hasYoone = false;
|
|
||||||
let orderYooneAmount = 0;
|
|
||||||
|
|
||||||
// 优先使用line_items,如果没有则使用items
|
|
||||||
const items = order.line_items || order.items || [];
|
|
||||||
if (Array.isArray(items)) {
|
|
||||||
items.forEach((item: any) => {
|
|
||||||
// 检查商品名称或SKU是否包含yoone(不区分大小写)
|
|
||||||
const itemName = (item.name || '').toLowerCase();
|
|
||||||
const sku = (item.sku || '').toLowerCase();
|
|
||||||
|
|
||||||
if (itemName.includes('yoone') || sku.includes('yoone')) {
|
|
||||||
hasYoone = true;
|
|
||||||
const itemTotal = parseFloat(item.total || item.price || '0');
|
|
||||||
orderYooneAmount += itemTotal;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hasYoone) {
|
|
||||||
yooneOrders++;
|
|
||||||
yooneAmount += orderYooneAmount;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
totalOrders,
|
|
||||||
totalAmount,
|
|
||||||
yooneOrders,
|
|
||||||
yooneAmount,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
// 获取客户订单数据
|
|
||||||
const fetchOrders = async () => {
|
|
||||||
setLoading(true);
|
|
||||||
try {
|
|
||||||
const response = await ordercontrollerGetorders({
|
|
||||||
where: {
|
|
||||||
customer_email: customer.email,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response) {
|
|
||||||
const orderList = response.items || [];
|
|
||||||
setOrders(orderList);
|
|
||||||
const calculatedStats = calculateStats(orderList);
|
|
||||||
setStats(calculatedStats);
|
|
||||||
} else {
|
|
||||||
message.error('获取订单数据失败');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('获取订单失败:', error);
|
|
||||||
message.error('获取订单失败');
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 打开弹框时获取数据
|
|
||||||
const handleOpenModal = () => {
|
|
||||||
setModalVisible(true);
|
|
||||||
fetchOrders();
|
|
||||||
};
|
|
||||||
|
|
||||||
// 订单表格列配置
|
|
||||||
const orderColumns = [
|
|
||||||
{
|
|
||||||
title: '订单号',
|
|
||||||
dataIndex: 'externalOrderId',
|
|
||||||
key: 'externalOrderId',
|
|
||||||
width: 120,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '订单状态',
|
|
||||||
dataIndex: 'status',
|
|
||||||
key: 'status',
|
|
||||||
width: 100,
|
|
||||||
render: (status: string) => {
|
|
||||||
const statusMap: Record<string, string> = {
|
|
||||||
pending: '待处理',
|
|
||||||
processing: '处理中',
|
|
||||||
'on-hold': '等待中',
|
|
||||||
completed: '已完成',
|
|
||||||
cancelled: '已取消',
|
|
||||||
refunded: '已退款',
|
|
||||||
failed: '失败',
|
|
||||||
};
|
|
||||||
return <Tag color="blue">{statusMap[status] || status}</Tag>;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '订单金额',
|
|
||||||
dataIndex: 'total',
|
|
||||||
key: 'total',
|
|
||||||
width: 100,
|
|
||||||
render: (total: string, record: any) => (
|
|
||||||
<Text>
|
|
||||||
{record.currency_symbol || '$'}
|
|
||||||
{parseFloat(total || '0').toFixed(2)}
|
|
||||||
</Text>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '创建时间',
|
|
||||||
dataIndex: 'date_created',
|
|
||||||
key: 'date_created',
|
|
||||||
width: 140,
|
|
||||||
render: (date: string) => (
|
|
||||||
<Text>{date ? dayjs(date).format('YYYY-MM-DD HH:mm') : '-'}</Text>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '包含Yoone',
|
|
||||||
key: 'hasYoone',
|
|
||||||
width: 80,
|
|
||||||
render: (_: any, record: any) => {
|
|
||||||
let hasYoone = false;
|
|
||||||
const items = record.line_items || record.items || [];
|
|
||||||
if (Array.isArray(items)) {
|
|
||||||
hasYoone = items.some((item: any) => {
|
|
||||||
const itemName = (item.name || '').toLowerCase();
|
|
||||||
const sku = (item.sku || '').toLowerCase();
|
|
||||||
return itemName.includes('yoone') || sku.includes('yoone');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return hasYoone ? <Tag color="green">是</Tag> : <Tag>否</Tag>;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<a onClick={handleOpenModal}>历史订单</a>
|
|
||||||
|
|
||||||
<Modal
|
|
||||||
title={`${customer.fullname || customer.email} 的历史订单`}
|
|
||||||
open={modalVisible}
|
|
||||||
onCancel={() => setModalVisible(false)}
|
|
||||||
footer={null}
|
|
||||||
width={1000}
|
|
||||||
>
|
|
||||||
<Spin spinning={loading}>
|
|
||||||
{/* 统计信息 */}
|
|
||||||
<Row gutter={16} style={{ marginBottom: 24 }}>
|
|
||||||
<Col span={6}>
|
|
||||||
<Statistic
|
|
||||||
title="总订单数"
|
|
||||||
value={stats.totalOrders}
|
|
||||||
prefix="#"
|
|
||||||
/>
|
|
||||||
</Col>
|
|
||||||
<Col span={6}>
|
|
||||||
<Statistic
|
|
||||||
title="总金额"
|
|
||||||
value={stats.totalAmount}
|
|
||||||
precision={2}
|
|
||||||
prefix="$"
|
|
||||||
/>
|
|
||||||
</Col>
|
|
||||||
<Col span={6}>
|
|
||||||
<Statistic
|
|
||||||
title="Yoone订单数"
|
|
||||||
value={stats.yooneOrders}
|
|
||||||
prefix="#"
|
|
||||||
/>
|
|
||||||
</Col>
|
|
||||||
<Col span={6}>
|
|
||||||
<Statistic
|
|
||||||
title="Yoone金额"
|
|
||||||
value={stats.yooneAmount}
|
|
||||||
precision={2}
|
|
||||||
prefix="$"
|
|
||||||
/>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
|
|
||||||
{/* 订单列表 */}
|
|
||||||
<Title level={4} style={{ marginTop: 24 }}>
|
|
||||||
订单详情
|
|
||||||
</Title>
|
|
||||||
<Table
|
|
||||||
columns={orderColumns}
|
|
||||||
dataSource={orders}
|
|
||||||
rowKey="id"
|
|
||||||
pagination={{
|
|
||||||
pageSize: 10,
|
|
||||||
showSizeChanger: true,
|
|
||||||
showQuickJumper: true,
|
|
||||||
}}
|
|
||||||
scroll={{ x: 800 }}
|
|
||||||
/>
|
|
||||||
</Spin>
|
|
||||||
</Modal>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default HistoryOrders;
|
|
||||||
|
|
@ -1,249 +1,151 @@
|
||||||
|
import { HistoryOrder } from '@/pages/Statistics/Order';
|
||||||
import {
|
import {
|
||||||
customercontrollerAddtag,
|
customercontrollerAddtag,
|
||||||
customercontrollerDeltag,
|
customercontrollerDeltag,
|
||||||
customercontrollerGetcustomerlist,
|
customercontrollerGetcustomerlist,
|
||||||
customercontrollerGettags,
|
customercontrollerGettags,
|
||||||
customercontrollerSetrate,
|
customercontrollerSetrate,
|
||||||
customercontrollerSynccustomers,
|
|
||||||
} from '@/servers/api/customer';
|
} from '@/servers/api/customer';
|
||||||
import { sitecontrollerAll } from '@/servers/api/site';
|
|
||||||
import {
|
import {
|
||||||
ActionType,
|
ActionType,
|
||||||
ModalForm,
|
ModalForm,
|
||||||
PageContainer,
|
PageContainer,
|
||||||
ProColumns,
|
ProColumns,
|
||||||
ProFormDateTimeRangePicker,
|
|
||||||
ProFormSelect,
|
ProFormSelect,
|
||||||
ProFormText,
|
|
||||||
ProTable,
|
ProTable,
|
||||||
} from '@ant-design/pro-components';
|
} from '@ant-design/pro-components';
|
||||||
import { App, Avatar, Button, Form, Rate, Space, Tag, Tooltip } from 'antd';
|
import { App, Button, Rate, Space, Tag } from 'antd';
|
||||||
import { useEffect, useRef, useState } from 'react';
|
import dayjs from 'dayjs';
|
||||||
import HistoryOrders from './HistoryOrders';
|
import { useRef, useState } from 'react';
|
||||||
|
|
||||||
// 地址格式化函数
|
const ListPage: React.FC = () => {
|
||||||
const formatAddress = (address: any) => {
|
|
||||||
if (!address) return '-';
|
|
||||||
|
|
||||||
if (typeof address === 'string') {
|
|
||||||
try {
|
|
||||||
address = JSON.parse(address);
|
|
||||||
} catch (e) {
|
|
||||||
return address;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const {
|
|
||||||
first_name,
|
|
||||||
last_name,
|
|
||||||
company,
|
|
||||||
address_1,
|
|
||||||
address_2,
|
|
||||||
city,
|
|
||||||
state,
|
|
||||||
postcode,
|
|
||||||
country,
|
|
||||||
phone: addressPhone,
|
|
||||||
email: addressEmail,
|
|
||||||
} = address;
|
|
||||||
|
|
||||||
const parts = [];
|
|
||||||
|
|
||||||
// 姓名
|
|
||||||
const fullName = [first_name, last_name].filter(Boolean).join(' ');
|
|
||||||
if (fullName) parts.push(fullName);
|
|
||||||
|
|
||||||
// 公司
|
|
||||||
if (company) parts.push(company);
|
|
||||||
|
|
||||||
// 地址行
|
|
||||||
if (address_1) parts.push(address_1);
|
|
||||||
if (address_2) parts.push(address_2);
|
|
||||||
|
|
||||||
// 城市、州、邮编
|
|
||||||
const locationParts = [city, state, postcode].filter(Boolean).join(', ');
|
|
||||||
if (locationParts) parts.push(locationParts);
|
|
||||||
|
|
||||||
// 国家
|
|
||||||
if (country) parts.push(country);
|
|
||||||
|
|
||||||
// 联系方式
|
|
||||||
if (addressPhone) parts.push(`电话: ${addressPhone}`);
|
|
||||||
if (addressEmail) parts.push(`邮箱: ${addressEmail}`);
|
|
||||||
|
|
||||||
return parts.join(', ');
|
|
||||||
};
|
|
||||||
|
|
||||||
// 地址卡片组件
|
|
||||||
const AddressCell: React.FC<{ address: any; title: string }> = ({
|
|
||||||
address,
|
|
||||||
title,
|
|
||||||
}) => {
|
|
||||||
const formattedAddress = formatAddress(address);
|
|
||||||
|
|
||||||
if (formattedAddress === '-') {
|
|
||||||
return <span>-</span>;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Tooltip
|
|
||||||
title={
|
|
||||||
<div style={{ maxWidth: 300, whiteSpace: 'pre-line' }}>
|
|
||||||
<strong>{title}:</strong>
|
|
||||||
<br />
|
|
||||||
{formattedAddress}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
placement="topLeft"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
maxWidth: 200,
|
|
||||||
overflow: 'hidden',
|
|
||||||
textOverflow: 'ellipsis',
|
|
||||||
whiteSpace: 'nowrap',
|
|
||||||
cursor: 'pointer',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{formattedAddress}
|
|
||||||
</div>
|
|
||||||
</Tooltip>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const CustomerList: React.FC = () => {
|
|
||||||
const actionRef = useRef<ActionType>();
|
const actionRef = useRef<ActionType>();
|
||||||
const { message } = App.useApp();
|
const { message } = App.useApp();
|
||||||
const [syncModalVisible, setSyncModalVisible] = useState(false);
|
const columns: ProColumns[] = [
|
||||||
|
|
||||||
const columns: ProColumns<API.GetCustomerDTO>[] = [
|
|
||||||
{
|
|
||||||
title: 'ID',
|
|
||||||
dataIndex: 'id',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '原始 ID',
|
|
||||||
dataIndex: 'origin_id',
|
|
||||||
sorter: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '站点',
|
|
||||||
dataIndex: 'site_id',
|
|
||||||
valueType: 'select',
|
|
||||||
request: async () => {
|
|
||||||
try {
|
|
||||||
const { data, success } = await sitecontrollerAll();
|
|
||||||
if (success && data) {
|
|
||||||
return data.map((site: any) => ({
|
|
||||||
label: site.name,
|
|
||||||
value: site.id,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
return [];
|
|
||||||
} catch (error) {
|
|
||||||
console.error('获取站点列表失败:', error);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '头像',
|
|
||||||
dataIndex: 'avatar',
|
|
||||||
hideInSearch: true,
|
|
||||||
width: 60,
|
|
||||||
render: (_, record) => (
|
|
||||||
<Avatar
|
|
||||||
src={record.avatar}
|
|
||||||
size="small"
|
|
||||||
style={{ backgroundColor: '#1890ff' }}
|
|
||||||
>
|
|
||||||
{!record.avatar && record.fullname?.charAt(0)?.toUpperCase()}
|
|
||||||
</Avatar>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '姓名',
|
|
||||||
dataIndex: 'fullname',
|
|
||||||
sorter: true,
|
|
||||||
render: (_, record) => {
|
|
||||||
return (
|
|
||||||
record.fullname ||
|
|
||||||
`${record.first_name || ''} ${record.last_name || ''}`.trim() ||
|
|
||||||
record.username ||
|
|
||||||
'-'
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
title: '用户名',
|
title: '用户名',
|
||||||
dataIndex: 'username',
|
dataIndex: 'username',
|
||||||
copyable: true,
|
hideInSearch: true,
|
||||||
sorter: true,
|
render: (_, record) => {
|
||||||
|
if (record.billing.first_name || record.billing.last_name)
|
||||||
|
return record.billing.first_name + ' ' + record.billing.last_name;
|
||||||
|
return record.shipping.first_name + ' ' + record.shipping.last_name;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '邮箱',
|
title: '邮箱',
|
||||||
dataIndex: 'email',
|
dataIndex: 'email',
|
||||||
copyable: true,
|
|
||||||
sorter: true,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '电话',
|
title: '客户编号',
|
||||||
dataIndex: 'phone',
|
dataIndex: 'customerId',
|
||||||
copyable: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '账单地址',
|
|
||||||
dataIndex: 'billing',
|
|
||||||
hideInSearch: true,
|
|
||||||
|
|
||||||
width: 200,
|
|
||||||
render: (billing) => <AddressCell address={billing} title="账单地址" />,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '物流地址',
|
|
||||||
dataIndex: 'shipping',
|
|
||||||
hideInSearch: true,
|
|
||||||
width: 200,
|
|
||||||
render: (shipping) => <AddressCell address={shipping} title="物流地址" />,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '评分',
|
|
||||||
dataIndex: 'rate',
|
|
||||||
hideInSearch: true,
|
|
||||||
sorter: true,
|
|
||||||
render: (_, record) => {
|
render: (_, record) => {
|
||||||
return (
|
if(!record.customerId) return '-';
|
||||||
<Rate
|
return String(record.customerId).padStart(6,0)
|
||||||
onChange={async (val) => {
|
},
|
||||||
try {
|
sorter: true,
|
||||||
const { success, message: msg } =
|
},
|
||||||
await customercontrollerSetrate({
|
{
|
||||||
id: record.id,
|
title: '首单时间',
|
||||||
rate: val,
|
dataIndex: 'first_purchase_date',
|
||||||
|
valueType: 'dateMonth',
|
||||||
|
sorter: true,
|
||||||
|
render: (_, record) =>
|
||||||
|
record.first_purchase_date
|
||||||
|
? dayjs(record.first_purchase_date).format('YYYY-MM-DD HH:mm:ss')
|
||||||
|
: '-',
|
||||||
|
// search: {
|
||||||
|
// transform: (value: string) => {
|
||||||
|
// return { month: value };
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '尾单时间',
|
||||||
|
hideInSearch: true,
|
||||||
|
dataIndex: 'last_purchase_date',
|
||||||
|
valueType: 'dateTime',
|
||||||
|
sorter: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '订单数',
|
||||||
|
dataIndex: 'orders',
|
||||||
|
hideInSearch: true,
|
||||||
|
sorter: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '金额',
|
||||||
|
dataIndex: 'total',
|
||||||
|
hideInSearch: true,
|
||||||
|
sorter: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'YOONE订单数',
|
||||||
|
dataIndex: 'yoone_orders',
|
||||||
|
hideInSearch: true,
|
||||||
|
sorter: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'YOONE金额',
|
||||||
|
dataIndex: 'yoone_total',
|
||||||
|
hideInSearch: true,
|
||||||
|
sorter: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '等级',
|
||||||
|
hideInSearch: true,
|
||||||
|
render: (_, record) => {
|
||||||
|
if(!record.yoone_orders || !record.yoone_total) return '-'
|
||||||
|
if(Number(record.yoone_orders) === 1 && Number(record.yoone_total) > 0 ) return 'B'
|
||||||
|
return '-'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '评星',
|
||||||
|
dataIndex: 'rate',
|
||||||
|
width: 200,
|
||||||
|
render: (_, record) => {
|
||||||
|
return <Rate onChange={async(val)=>{
|
||||||
|
try{
|
||||||
|
const { success, message: msg } = await customercontrollerSetrate({
|
||||||
|
id: record.customerId,
|
||||||
|
rate: val
|
||||||
});
|
});
|
||||||
if (success) {
|
if (success) {
|
||||||
message.success(msg);
|
message.success(msg);
|
||||||
actionRef.current?.reload();
|
actionRef.current?.reload();
|
||||||
}
|
}
|
||||||
} catch (e: any) {
|
}catch(e){
|
||||||
message.error(e?.message || '设置评分失败');
|
message.error(e.message);
|
||||||
|
|
||||||
}
|
}
|
||||||
}}
|
}} value={record.rate} />
|
||||||
value={record.rate || 0}
|
|
||||||
allowHalf
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: 'phone',
|
||||||
|
dataIndex: 'phone',
|
||||||
|
hideInSearch: true,
|
||||||
|
render: (_, record) => record?.billing.phone || record?.shipping.phone,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'state',
|
||||||
|
dataIndex: 'state',
|
||||||
|
render: (_, record) => record?.billing.state || record?.shipping.state,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'city',
|
||||||
|
dataIndex: 'city',
|
||||||
|
hideInSearch: true,
|
||||||
|
render: (_, record) => record?.billing.city || record?.shipping.city,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: '标签',
|
title: '标签',
|
||||||
dataIndex: 'tags',
|
dataIndex: 'tags',
|
||||||
hideInSearch: true,
|
|
||||||
render: (_, record) => {
|
render: (_, record) => {
|
||||||
const tags = record?.tags || [];
|
|
||||||
return (
|
return (
|
||||||
<Space size={[0, 8]} wrap>
|
<Space>
|
||||||
{tags.map((tag: string) => {
|
{(record.tags || []).map((tag) => {
|
||||||
return (
|
return (
|
||||||
<Tag
|
<Tag
|
||||||
key={tag}
|
key={tag}
|
||||||
|
|
@ -254,14 +156,8 @@ const CustomerList: React.FC = () => {
|
||||||
email: record.email,
|
email: record.email,
|
||||||
tag,
|
tag,
|
||||||
});
|
});
|
||||||
if (!success) {
|
|
||||||
message.error(msg);
|
|
||||||
return false;
|
return false;
|
||||||
}
|
|
||||||
actionRef.current?.reload();
|
|
||||||
return true;
|
|
||||||
}}
|
}}
|
||||||
style={{ marginBottom: 4 }}
|
|
||||||
>
|
>
|
||||||
{tag}
|
{tag}
|
||||||
</Tag>
|
</Tag>
|
||||||
|
|
@ -271,110 +167,54 @@ const CustomerList: React.FC = () => {
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
title: '创建时间',
|
|
||||||
dataIndex: 'site_created_at',
|
|
||||||
valueType: 'dateTime',
|
|
||||||
hideInSearch: true,
|
|
||||||
sorter: true,
|
|
||||||
width: 140,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '更新时间',
|
|
||||||
dataIndex: 'site_created_at',
|
|
||||||
valueType: 'dateTime',
|
|
||||||
hideInSearch: true,
|
|
||||||
sorter: true,
|
|
||||||
width: 140,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
title: '操作',
|
title: '操作',
|
||||||
dataIndex: 'option',
|
dataIndex: 'option',
|
||||||
valueType: 'option',
|
valueType: 'option',
|
||||||
fixed: 'right',
|
|
||||||
width: 120,
|
|
||||||
render: (_, record) => {
|
render: (_, record) => {
|
||||||
return (
|
return (
|
||||||
<Space direction="vertical" size="small">
|
<Space>
|
||||||
<AddTag
|
<AddTag
|
||||||
email={record.email || ''}
|
email={record.email}
|
||||||
tags={record.raw?.tags || []}
|
tags={record.tags}
|
||||||
|
tableRef={actionRef}
|
||||||
|
/>
|
||||||
|
<HistoryOrder
|
||||||
|
email={record.email}
|
||||||
|
tags={record.tags}
|
||||||
tableRef={actionRef}
|
tableRef={actionRef}
|
||||||
/>
|
/>
|
||||||
{/* 订单 */}
|
|
||||||
<HistoryOrders customer={record} siteId={record.site_id} />
|
|
||||||
</Space>
|
</Space>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageContainer header={{ title: '客户列表' }}>
|
<PageContainer ghost>
|
||||||
<ProTable
|
<ProTable
|
||||||
scroll={{ x: 'max-content' }}
|
|
||||||
headerTitle="查询表格"
|
headerTitle="查询表格"
|
||||||
actionRef={actionRef}
|
actionRef={actionRef}
|
||||||
columns={columns}
|
|
||||||
rowKey="id"
|
rowKey="id"
|
||||||
request={async (params, sorter, filter) => {
|
request={async (params, sorter) => {
|
||||||
console.log('custoemr request', params, sorter, filter);
|
const key = Object.keys(sorter)[0];
|
||||||
const { current, pageSize, ...restParams } = params;
|
const { data, success } = await customercontrollerGetcustomerlist({
|
||||||
const orderBy: any = {};
|
...params,
|
||||||
Object.entries(sorter).forEach(([key, value]) => {
|
...(key ? { sorterKey: key, sorterValue: sorter[key] } : {}),
|
||||||
orderBy[key] = value === 'ascend' ? 'asc' : 'desc';
|
|
||||||
});
|
});
|
||||||
// 构建查询参数
|
|
||||||
const queryParams: any = {
|
|
||||||
page: current || 1,
|
|
||||||
per_page: pageSize || 20,
|
|
||||||
where: {
|
|
||||||
...filter,
|
|
||||||
...restParams,
|
|
||||||
},
|
|
||||||
orderBy,
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = await customercontrollerGetcustomerlist({
|
|
||||||
params: queryParams,
|
|
||||||
});
|
|
||||||
console.log(queryParams, result);
|
|
||||||
return {
|
return {
|
||||||
total: result?.data?.total || 0,
|
total: data?.total || 0,
|
||||||
data: result?.data?.items || [],
|
data: data?.items || [],
|
||||||
success: true,
|
success,
|
||||||
};
|
};
|
||||||
}}
|
}}
|
||||||
search={{
|
columns={columns}
|
||||||
labelWidth: 'auto',
|
|
||||||
span: 6,
|
|
||||||
}}
|
|
||||||
pagination={{
|
|
||||||
pageSize: 20,
|
|
||||||
showSizeChanger: true,
|
|
||||||
showQuickJumper: true,
|
|
||||||
}}
|
|
||||||
toolBarRender={() => [
|
|
||||||
<Button
|
|
||||||
key="sync"
|
|
||||||
type="primary"
|
|
||||||
onClick={() => setSyncModalVisible(true)}
|
|
||||||
>
|
|
||||||
同步客户数据
|
|
||||||
</Button>,
|
|
||||||
// 这里可以添加导出、导入等功能按钮
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
<SyncCustomersModal
|
|
||||||
visible={syncModalVisible}
|
|
||||||
onClose={() => setSyncModalVisible(false)}
|
|
||||||
tableRef={actionRef}
|
|
||||||
/>
|
/>
|
||||||
</PageContainer>
|
</PageContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const AddTag: React.FC<{
|
export const AddTag: React.FC<{
|
||||||
email: string;
|
email: string;
|
||||||
tags?: string[];
|
tags?: string[];
|
||||||
tableRef: React.MutableRefObject<ActionType | undefined>;
|
tableRef: React.MutableRefObject<ActionType | undefined>;
|
||||||
|
|
@ -385,11 +225,7 @@ const AddTag: React.FC<{
|
||||||
return (
|
return (
|
||||||
<ModalForm
|
<ModalForm
|
||||||
title={`修改标签 - ${email}`}
|
title={`修改标签 - ${email}`}
|
||||||
trigger={
|
trigger={<Button>修改标签</Button>}
|
||||||
<Button type="link" size="small">
|
|
||||||
修改标签
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
width={800}
|
width={800}
|
||||||
modalProps={{
|
modalProps={{
|
||||||
destroyOnHidden: true,
|
destroyOnHidden: true,
|
||||||
|
|
@ -406,16 +242,16 @@ const AddTag: React.FC<{
|
||||||
if (!success) return [];
|
if (!success) return [];
|
||||||
setTagList(tags || []);
|
setTagList(tags || []);
|
||||||
return data
|
return data
|
||||||
.filter((tag: string) => {
|
.filter((tag) => {
|
||||||
return !(tags || []).includes(tag);
|
return !(tags || []).includes(tag);
|
||||||
})
|
})
|
||||||
.map((tag: string) => ({ label: tag, value: tag }));
|
.map((tag) => ({ label: tag, value: tag }));
|
||||||
}}
|
}}
|
||||||
fieldProps={{
|
fieldProps={{
|
||||||
value: tagList, // 当前值
|
value: tagList, // 当前值
|
||||||
onChange: async (newValue) => {
|
onChange: async (newValue) => {
|
||||||
const added = newValue.filter((x) => !(tags || []).includes(x));
|
const added = newValue.filter((x) => !tagList.includes(x));
|
||||||
const removed = (tags || []).filter((x) => !newValue.includes(x));
|
const removed = tagList.filter((x) => !newValue.includes(x));
|
||||||
|
|
||||||
if (added.length) {
|
if (added.length) {
|
||||||
const { success, message: msg } = await customercontrollerAddtag({
|
const { success, message: msg } = await customercontrollerAddtag({
|
||||||
|
|
@ -438,6 +274,7 @@ const AddTag: React.FC<{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
tableRef?.current?.reload();
|
tableRef?.current?.reload();
|
||||||
|
|
||||||
setTagList(newValue);
|
setTagList(newValue);
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
|
|
@ -446,228 +283,4 @@ const AddTag: React.FC<{
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const SyncCustomersModal: React.FC<{
|
export default ListPage;
|
||||||
visible: boolean;
|
|
||||||
onClose: () => void;
|
|
||||||
tableRef: React.MutableRefObject<ActionType | undefined>;
|
|
||||||
}> = ({ visible, onClose, tableRef }) => {
|
|
||||||
const { message } = App.useApp();
|
|
||||||
const [sites, setSites] = useState<any[]>([]);
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const [form] = Form.useForm(); // 添加表单实例
|
|
||||||
|
|
||||||
// 获取站点列表
|
|
||||||
useEffect(() => {
|
|
||||||
if (visible) {
|
|
||||||
setLoading(true);
|
|
||||||
sitecontrollerAll()
|
|
||||||
.then((res: any) => {
|
|
||||||
setSites(res?.data || []);
|
|
||||||
})
|
|
||||||
.catch((error: any) => {
|
|
||||||
message.error('获取站点列表失败: ' + (error.message || '未知错误'));
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
setLoading(false);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [visible]);
|
|
||||||
|
|
||||||
// 定义同步参数类型
|
|
||||||
type SyncParams = {
|
|
||||||
siteId: number;
|
|
||||||
search?: string;
|
|
||||||
role?: string;
|
|
||||||
dateRange?: [string, string];
|
|
||||||
orderBy?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSync = async (values: SyncParams) => {
|
|
||||||
try {
|
|
||||||
setLoading(true);
|
|
||||||
|
|
||||||
// 构建过滤参数
|
|
||||||
const params: any = {};
|
|
||||||
|
|
||||||
// 添加搜索关键词
|
|
||||||
if (values.search) {
|
|
||||||
params.search = values.search;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 添加角色过滤
|
|
||||||
if (values.role) {
|
|
||||||
params.where = {
|
|
||||||
...params.where,
|
|
||||||
role: values.role,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// 添加日期范围过滤(使用 after 和 before 参数)
|
|
||||||
if (values.dateRange && values.dateRange[0] && values.dateRange[1]) {
|
|
||||||
params.where = {
|
|
||||||
...params.where,
|
|
||||||
after: values.dateRange[0],
|
|
||||||
before: values.dateRange[1],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// 添加排序
|
|
||||||
if (values.orderBy) {
|
|
||||||
params.orderBy = values.orderBy;
|
|
||||||
}
|
|
||||||
|
|
||||||
const {
|
|
||||||
success,
|
|
||||||
message: msg,
|
|
||||||
data,
|
|
||||||
} = await customercontrollerSynccustomers({
|
|
||||||
siteId: values.siteId,
|
|
||||||
params: Object.keys(params).length > 0 ? params : undefined,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (success) {
|
|
||||||
// 显示详细的同步结果
|
|
||||||
const result = data || {};
|
|
||||||
const {
|
|
||||||
total = 0,
|
|
||||||
synced = 0,
|
|
||||||
created = 0,
|
|
||||||
updated = 0,
|
|
||||||
errors = [],
|
|
||||||
} = result;
|
|
||||||
|
|
||||||
let resultMessage = `同步完成!共处理 ${total} 个客户:`;
|
|
||||||
if (created > 0) resultMessage += ` 新建 ${created} 个`;
|
|
||||||
if (updated > 0) resultMessage += ` 更新 ${updated} 个`;
|
|
||||||
if (synced > 0) resultMessage += ` 同步成功 ${synced} 个`;
|
|
||||||
if (errors.length > 0) resultMessage += ` 失败 ${errors.length} 个`;
|
|
||||||
|
|
||||||
if (errors.length > 0) {
|
|
||||||
// 如果有错误,显示警告消息
|
|
||||||
message.warning({
|
|
||||||
content: (
|
|
||||||
<div>
|
|
||||||
<div>{resultMessage}</div>
|
|
||||||
<div style={{ marginTop: 8, fontSize: 12, color: '#faad14' }}>
|
|
||||||
失败详情:
|
|
||||||
{errors
|
|
||||||
.slice(0, 3)
|
|
||||||
.map((err: any) => err.email || err.error)
|
|
||||||
.join(', ')}
|
|
||||||
{errors.length > 3 && ` 等 ${errors.length - 3} 个错误...`}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
duration: 8,
|
|
||||||
key: 'sync-result',
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// 完全成功
|
|
||||||
message.success({
|
|
||||||
content: resultMessage,
|
|
||||||
duration: 4,
|
|
||||||
key: 'sync-result',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
onClose();
|
|
||||||
// 刷新表格数据
|
|
||||||
tableRef.current?.reload();
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
message.error(msg || '同步失败');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
} catch (error: any) {
|
|
||||||
message.error('同步失败: ' + (error.message || '未知错误'));
|
|
||||||
return false;
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ModalForm
|
|
||||||
title="同步客户数据"
|
|
||||||
open={visible}
|
|
||||||
onOpenChange={(open) => !open && onClose()}
|
|
||||||
modalProps={{
|
|
||||||
destroyOnClose: true,
|
|
||||||
confirmLoading: loading,
|
|
||||||
}}
|
|
||||||
onFinish={handleSync}
|
|
||||||
form={form}
|
|
||||||
>
|
|
||||||
<ProFormSelect
|
|
||||||
name="siteId"
|
|
||||||
label="选择站点"
|
|
||||||
placeholder="请选择要同步的站点"
|
|
||||||
options={sites.map((site) => ({
|
|
||||||
label: site.name,
|
|
||||||
value: site.id,
|
|
||||||
}))}
|
|
||||||
rules={[{ required: true, message: '请选择站点' }]}
|
|
||||||
fieldProps={{
|
|
||||||
loading: loading,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<ProFormText
|
|
||||||
name="search"
|
|
||||||
label="搜索关键词"
|
|
||||||
placeholder="输入邮箱、姓名或用户名进行搜索"
|
|
||||||
tooltip="支持搜索邮箱、姓名、用户名等字段"
|
|
||||||
/>
|
|
||||||
<ProFormSelect
|
|
||||||
name="role"
|
|
||||||
label="客户角色"
|
|
||||||
placeholder="选择客户角色进行过滤"
|
|
||||||
options={[
|
|
||||||
{ label: '所有角色', value: '' },
|
|
||||||
{ label: '管理员', value: 'administrator' },
|
|
||||||
{ label: '编辑', value: 'editor' },
|
|
||||||
{ label: '作者', value: 'author' },
|
|
||||||
{ label: '订阅者', value: 'subscriber' },
|
|
||||||
{ label: '客户', value: 'customer' },
|
|
||||||
]}
|
|
||||||
fieldProps={{
|
|
||||||
allowClear: true,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<ProFormDateTimeRangePicker
|
|
||||||
name="dateRange"
|
|
||||||
label="注册日期范围"
|
|
||||||
placeholder={['开始日期', '结束日期']}
|
|
||||||
transform={(value) => {
|
|
||||||
return {
|
|
||||||
dateRange: value,
|
|
||||||
};
|
|
||||||
}}
|
|
||||||
fieldProps={{
|
|
||||||
showTime: false,
|
|
||||||
style: { width: '100%' },
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<ProFormSelect
|
|
||||||
name="orderBy"
|
|
||||||
label="排序方式"
|
|
||||||
placeholder="选择排序方式"
|
|
||||||
options={[
|
|
||||||
{ label: '默认排序', value: '' },
|
|
||||||
{ label: '注册时间(升序)', value: 'date_created:asc' },
|
|
||||||
{ label: '注册时间(降序)', value: 'date_created:desc' },
|
|
||||||
{ label: '邮箱(升序)', value: 'email:asc' },
|
|
||||||
{ label: '邮箱(降序)', value: 'email:desc' },
|
|
||||||
{ label: '姓名(升序)', value: 'first_name:asc' },
|
|
||||||
{ label: '姓名(降序)', value: 'first_name:desc' },
|
|
||||||
]}
|
|
||||||
fieldProps={{
|
|
||||||
allowClear: true,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</ModalForm>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export { AddTag };
|
|
||||||
|
|
||||||
export default CustomerList;
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
export default function Statistic() {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<h1>客户统计</h1>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,289 +0,0 @@
|
||||||
import { HistoryOrder } from '@/pages/Statistics/Order';
|
|
||||||
import {
|
|
||||||
customercontrollerAddtag,
|
|
||||||
customercontrollerDeltag,
|
|
||||||
customercontrollerGetcustomerlist,
|
|
||||||
customercontrollerGettags,
|
|
||||||
customercontrollerSetrate,
|
|
||||||
} from '@/servers/api/customer';
|
|
||||||
import {
|
|
||||||
ActionType,
|
|
||||||
ModalForm,
|
|
||||||
PageContainer,
|
|
||||||
ProColumns,
|
|
||||||
ProFormSelect,
|
|
||||||
ProTable,
|
|
||||||
} from '@ant-design/pro-components';
|
|
||||||
import { App, Button, Rate, Space, Tag } from 'antd';
|
|
||||||
import dayjs from 'dayjs';
|
|
||||||
import { useRef, useState } from 'react';
|
|
||||||
|
|
||||||
const ListPage: React.FC = () => {
|
|
||||||
const actionRef = useRef<ActionType>();
|
|
||||||
const { message } = App.useApp();
|
|
||||||
const columns: ProColumns[] = [
|
|
||||||
{
|
|
||||||
title: '用户名',
|
|
||||||
dataIndex: 'username',
|
|
||||||
hideInSearch: true,
|
|
||||||
render: (_, record) => {
|
|
||||||
if (record.billing.first_name || record.billing.last_name)
|
|
||||||
return record.billing.first_name + ' ' + record.billing.last_name;
|
|
||||||
return record.shipping.first_name + ' ' + record.shipping.last_name;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '邮箱',
|
|
||||||
dataIndex: 'email',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '客户编号',
|
|
||||||
dataIndex: 'customerId',
|
|
||||||
render: (_, record) => {
|
|
||||||
if (!record.customerId) return '-';
|
|
||||||
return String(record.customerId).padStart(6, 0);
|
|
||||||
},
|
|
||||||
sorter: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '首单时间',
|
|
||||||
dataIndex: 'first_purchase_date',
|
|
||||||
valueType: 'dateMonth',
|
|
||||||
sorter: true,
|
|
||||||
render: (_, record) =>
|
|
||||||
record.first_purchase_date
|
|
||||||
? dayjs(record.first_purchase_date).format('YYYY-MM-DD HH:mm:ss')
|
|
||||||
: '-',
|
|
||||||
// search: {
|
|
||||||
// transform: (value: string) => {
|
|
||||||
// return { month: value };
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '尾单时间',
|
|
||||||
hideInSearch: true,
|
|
||||||
dataIndex: 'last_purchase_date',
|
|
||||||
valueType: 'dateTime',
|
|
||||||
sorter: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '订单数',
|
|
||||||
dataIndex: 'orders',
|
|
||||||
hideInSearch: true,
|
|
||||||
sorter: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '金额',
|
|
||||||
dataIndex: 'total',
|
|
||||||
hideInSearch: true,
|
|
||||||
sorter: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'YOONE订单数',
|
|
||||||
dataIndex: 'yoone_orders',
|
|
||||||
hideInSearch: true,
|
|
||||||
sorter: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'YOONE金额',
|
|
||||||
dataIndex: 'yoone_total',
|
|
||||||
hideInSearch: true,
|
|
||||||
sorter: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '等级',
|
|
||||||
hideInSearch: true,
|
|
||||||
render: (_, record) => {
|
|
||||||
if (!record.yoone_orders || !record.yoone_total) return '-';
|
|
||||||
if (Number(record.yoone_orders) === 1 && Number(record.yoone_total) > 0)
|
|
||||||
return 'B';
|
|
||||||
return '-';
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '评星',
|
|
||||||
dataIndex: 'rate',
|
|
||||||
width: 200,
|
|
||||||
render: (_, record) => {
|
|
||||||
return (
|
|
||||||
<Rate
|
|
||||||
onChange={async (val) => {
|
|
||||||
try {
|
|
||||||
const { success, message: msg } =
|
|
||||||
await customercontrollerSetrate({
|
|
||||||
id: record.customerId,
|
|
||||||
rate: val,
|
|
||||||
});
|
|
||||||
if (success) {
|
|
||||||
message.success(msg);
|
|
||||||
actionRef.current?.reload();
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
message.error(e.message);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
value={record.rate}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '联系电话',
|
|
||||||
dataIndex: 'phone',
|
|
||||||
hideInSearch: true,
|
|
||||||
render: (_, record) => record?.billing.phone || record?.shipping.phone,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '账单地址',
|
|
||||||
dataIndex: 'billing',
|
|
||||||
render: (_, record) =>
|
|
||||||
JSON.stringify(record?.billing || record?.shipping),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '标签',
|
|
||||||
dataIndex: 'tags',
|
|
||||||
render: (_, record) => {
|
|
||||||
return (
|
|
||||||
<Space>
|
|
||||||
{(record.tags || []).map((tag) => {
|
|
||||||
return (
|
|
||||||
<Tag
|
|
||||||
key={tag}
|
|
||||||
closable
|
|
||||||
onClose={async () => {
|
|
||||||
const { success, message: msg } =
|
|
||||||
await customercontrollerDeltag({
|
|
||||||
email: record.email,
|
|
||||||
tag,
|
|
||||||
});
|
|
||||||
return false;
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{tag}
|
|
||||||
</Tag>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</Space>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '操作',
|
|
||||||
dataIndex: 'option',
|
|
||||||
valueType: 'option',
|
|
||||||
fixed: 'right',
|
|
||||||
render: (_, record) => {
|
|
||||||
return (
|
|
||||||
<Space>
|
|
||||||
<AddTag
|
|
||||||
email={record.email}
|
|
||||||
tags={record.tags}
|
|
||||||
tableRef={actionRef}
|
|
||||||
/>
|
|
||||||
<HistoryOrder
|
|
||||||
email={record.email}
|
|
||||||
tags={record.tags}
|
|
||||||
tableRef={actionRef}
|
|
||||||
/>
|
|
||||||
</Space>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
return (
|
|
||||||
<PageContainer ghost>
|
|
||||||
<ProTable
|
|
||||||
scroll={{ x: 'max-content' }}
|
|
||||||
headerTitle="查询表格"
|
|
||||||
actionRef={actionRef}
|
|
||||||
rowKey="id"
|
|
||||||
request={async (params, sorter) => {
|
|
||||||
const key = Object.keys(sorter)[0];
|
|
||||||
const { data, success } = await customercontrollerGetcustomerlist({
|
|
||||||
...params,
|
|
||||||
...(key ? { sorterKey: key, sorterValue: sorter[key] } : {}),
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
total: data?.total || 0,
|
|
||||||
data: data?.items || [],
|
|
||||||
success,
|
|
||||||
};
|
|
||||||
}}
|
|
||||||
columns={columns}
|
|
||||||
/>
|
|
||||||
</PageContainer>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const AddTag: React.FC<{
|
|
||||||
email: string;
|
|
||||||
tags?: string[];
|
|
||||||
tableRef: React.MutableRefObject<ActionType | undefined>;
|
|
||||||
}> = ({ email, tags, tableRef }) => {
|
|
||||||
const { message } = App.useApp();
|
|
||||||
const [tagList, setTagList] = useState<string[]>([]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ModalForm
|
|
||||||
title={`修改标签 - ${email}`}
|
|
||||||
trigger={<Button>修改标签</Button>}
|
|
||||||
width={800}
|
|
||||||
modalProps={{
|
|
||||||
destroyOnHidden: true,
|
|
||||||
}}
|
|
||||||
submitter={false}
|
|
||||||
>
|
|
||||||
<ProFormSelect
|
|
||||||
mode="tags"
|
|
||||||
allowClear
|
|
||||||
name="tag"
|
|
||||||
label="标签"
|
|
||||||
request={async () => {
|
|
||||||
const { data, success } = await customercontrollerGettags();
|
|
||||||
if (!success) return [];
|
|
||||||
setTagList(tags || []);
|
|
||||||
return data
|
|
||||||
.filter((tag) => {
|
|
||||||
return !(tags || []).includes(tag);
|
|
||||||
})
|
|
||||||
.map((tag) => ({ label: tag, value: tag }));
|
|
||||||
}}
|
|
||||||
fieldProps={{
|
|
||||||
value: tagList, // 当前值
|
|
||||||
onChange: async (newValue) => {
|
|
||||||
const added = newValue.filter((x) => !tagList.includes(x));
|
|
||||||
const removed = tagList.filter((x) => !newValue.includes(x));
|
|
||||||
|
|
||||||
if (added.length) {
|
|
||||||
const { success, message: msg } = await customercontrollerAddtag({
|
|
||||||
email,
|
|
||||||
tag: added[0],
|
|
||||||
});
|
|
||||||
if (!success) {
|
|
||||||
message.error(msg);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (removed.length) {
|
|
||||||
const { success, message: msg } = await customercontrollerDeltag({
|
|
||||||
email,
|
|
||||||
tag: removed[0],
|
|
||||||
});
|
|
||||||
if (!success) {
|
|
||||||
message.error(msg);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
tableRef?.current?.reload();
|
|
||||||
|
|
||||||
setTagList(newValue);
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
></ProFormSelect>
|
|
||||||
</ModalForm>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ListPage;
|
|
||||||
|
|
@ -1,604 +0,0 @@
|
||||||
import * as dictApi from '@/servers/api/dict';
|
|
||||||
import { UploadOutlined } from '@ant-design/icons';
|
|
||||||
import {
|
|
||||||
ActionType,
|
|
||||||
PageContainer,
|
|
||||||
ProTable,
|
|
||||||
} from '@ant-design/pro-components';
|
|
||||||
import {
|
|
||||||
Button,
|
|
||||||
Form,
|
|
||||||
Input,
|
|
||||||
Layout,
|
|
||||||
Modal,
|
|
||||||
Space,
|
|
||||||
Table,
|
|
||||||
Upload,
|
|
||||||
message,
|
|
||||||
} from 'antd';
|
|
||||||
import React, { useEffect, useRef, useState } from 'react';
|
|
||||||
import DictItemActions from '../components/DictItemActions';
|
|
||||||
import DictItemModal from '../components/DictItemModal';
|
|
||||||
|
|
||||||
const { Sider, Content } = Layout;
|
|
||||||
|
|
||||||
const DictPage: React.FC = () => {
|
|
||||||
// 左侧字典列表的状态
|
|
||||||
const [dicts, setDicts] = useState<any[]>([]);
|
|
||||||
const [loadingDicts, setLoadingDicts] = useState(false);
|
|
||||||
const [searchText, setSearchText] = useState('');
|
|
||||||
const [selectedDict, setSelectedDict] = useState<any>(null);
|
|
||||||
|
|
||||||
// 添加字典 modal 状态
|
|
||||||
const [isAddDictModalVisible, setIsAddDictModalVisible] = useState(false);
|
|
||||||
const [addDictName, setAddDictName] = useState('');
|
|
||||||
const [addDictTitle, setAddDictTitle] = useState('');
|
|
||||||
|
|
||||||
// 编辑字典 modal 状态
|
|
||||||
const [isEditDictModalVisible, setIsEditDictModalVisible] = useState(false);
|
|
||||||
const [editDictData, setEditDictData] = useState<any>(null);
|
|
||||||
|
|
||||||
// 字典项模态框状态(由 DictItemModal 组件管理)
|
|
||||||
const [isDictItemModalVisible, setIsDictItemModalVisible] = useState(false);
|
|
||||||
const [isEditDictItem, setIsEditDictItem] = useState(false);
|
|
||||||
const [editingDictItemData, setEditingDictItemData] = useState<any>(null);
|
|
||||||
const actionRef = useRef<ActionType>();
|
|
||||||
|
|
||||||
// 获取字典列表
|
|
||||||
const fetchDicts = async (name = '') => {
|
|
||||||
setLoadingDicts(true);
|
|
||||||
try {
|
|
||||||
const res = await dictApi.dictcontrollerGetdicts({ name });
|
|
||||||
setDicts(res);
|
|
||||||
} catch (error) {
|
|
||||||
message.error('获取字典列表失败');
|
|
||||||
} finally {
|
|
||||||
setLoadingDicts(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 搜索字典
|
|
||||||
const handleSearch = (value: string) => {
|
|
||||||
fetchDicts(value);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 添加字典
|
|
||||||
const handleAddDict = async () => {
|
|
||||||
const values = { name: addDictName, title: addDictTitle };
|
|
||||||
try {
|
|
||||||
await dictApi.dictcontrollerCreatedict(values);
|
|
||||||
message.success('添加成功');
|
|
||||||
setIsAddDictModalVisible(false);
|
|
||||||
setAddDictName('');
|
|
||||||
setAddDictTitle('');
|
|
||||||
fetchDicts(); // 重新获取列表
|
|
||||||
} catch (error) {
|
|
||||||
message.error('添加失败');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 编辑字典
|
|
||||||
const handleEditDict = async () => {
|
|
||||||
if (!editDictData) return;
|
|
||||||
const values = { name: editDictData.name, title: editDictData.title };
|
|
||||||
try {
|
|
||||||
await dictApi.dictcontrollerUpdatedict({ id: editDictData.id }, values);
|
|
||||||
message.success('更新成功');
|
|
||||||
setIsEditDictModalVisible(false);
|
|
||||||
setEditDictData(null);
|
|
||||||
fetchDicts(); // 重新获取列表
|
|
||||||
} catch (error) {
|
|
||||||
message.error('更新失败');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 删除字典
|
|
||||||
const handleDeleteDict = async (id: number) => {
|
|
||||||
try {
|
|
||||||
const result = await dictApi.dictcontrollerDeletedict({ id });
|
|
||||||
if (!result.success) {
|
|
||||||
throw new Error(result.message || '删除失败');
|
|
||||||
}
|
|
||||||
message.success('删除成功');
|
|
||||||
fetchDicts();
|
|
||||||
if (selectedDict?.id === id) {
|
|
||||||
setSelectedDict(null);
|
|
||||||
}
|
|
||||||
} catch (error: any) {
|
|
||||||
message.error(`删除失败,原因为:${error.message}`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 打开编辑字典 modal
|
|
||||||
const openEditDictModal = (record: any) => {
|
|
||||||
setEditDictData(record);
|
|
||||||
setIsEditDictModalVisible(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 下载字典导入模板
|
|
||||||
const handleDownloadDictTemplate = async () => {
|
|
||||||
try {
|
|
||||||
// 使用 dictApi.dictcontrollerDownloaddicttemplate 获取字典模板
|
|
||||||
const response = await dictApi.dictcontrollerDownloaddicttemplate({
|
|
||||||
responseType: 'blob',
|
|
||||||
skipErrorHandler: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
// 创建 blob 对象和下载链接
|
|
||||||
const blob = new Blob([response], {
|
|
||||||
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
||||||
});
|
|
||||||
const url = window.URL.createObjectURL(blob);
|
|
||||||
const link = document.createElement('a');
|
|
||||||
link.href = url;
|
|
||||||
link.setAttribute('download', 'dict_template.xlsx');
|
|
||||||
document.body.appendChild(link);
|
|
||||||
link.click();
|
|
||||||
link.remove();
|
|
||||||
window.URL.revokeObjectURL(url);
|
|
||||||
} catch (error: any) {
|
|
||||||
message.error('下载字典模板失败:' + (error.message || '未知错误'));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 添加字典项
|
|
||||||
const handleAddDictItem = () => {
|
|
||||||
setIsEditDictItem(false);
|
|
||||||
setEditingDictItemData(null);
|
|
||||||
setIsDictItemModalVisible(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 编辑字典项
|
|
||||||
const handleEditDictItem = (record: any) => {
|
|
||||||
setIsEditDictItem(true);
|
|
||||||
setEditingDictItemData(record);
|
|
||||||
setIsDictItemModalVisible(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 删除字典项
|
|
||||||
const handleDeleteDictItem = async (id: number) => {
|
|
||||||
try {
|
|
||||||
const result = await dictApi.dictcontrollerDeletedictitem({ id });
|
|
||||||
if (!result.success) {
|
|
||||||
throw new Error(result.message || '删除失败');
|
|
||||||
}
|
|
||||||
message.success('删除成功');
|
|
||||||
|
|
||||||
// 强制刷新字典项列表
|
|
||||||
setTimeout(() => {
|
|
||||||
actionRef.current?.reload();
|
|
||||||
}, 100);
|
|
||||||
} catch (error: any) {
|
|
||||||
message.error(`删除失败,原因为:${error.message}`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 处理字典项模态框提交(添加或编辑)
|
|
||||||
const handleDictItemModalOk = async (values: any) => {
|
|
||||||
try {
|
|
||||||
if (isEditDictItem && editingDictItemData) {
|
|
||||||
// 编辑字典项
|
|
||||||
const result = await dictApi.dictcontrollerUpdatedictitem(
|
|
||||||
{ id: editingDictItemData.id },
|
|
||||||
values,
|
|
||||||
);
|
|
||||||
if (!result.success) {
|
|
||||||
throw new Error(result.message || '更新失败');
|
|
||||||
}
|
|
||||||
message.success('更新成功');
|
|
||||||
} else {
|
|
||||||
// 添加字典项
|
|
||||||
const result = await dictApi.dictcontrollerCreatedictitem({
|
|
||||||
...values,
|
|
||||||
dictId: selectedDict.id,
|
|
||||||
});
|
|
||||||
if (!result.success) {
|
|
||||||
throw new Error(result.message || '添加失败');
|
|
||||||
}
|
|
||||||
message.success('添加成功');
|
|
||||||
}
|
|
||||||
setIsDictItemModalVisible(false);
|
|
||||||
|
|
||||||
// 强制刷新字典项列表
|
|
||||||
setTimeout(() => {
|
|
||||||
actionRef.current?.reload();
|
|
||||||
}, 100);
|
|
||||||
} catch (error: any) {
|
|
||||||
message.error(
|
|
||||||
`${isEditDictItem ? '更新' : '添加'}失败:${
|
|
||||||
error.message || '未知错误'
|
|
||||||
}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 导出字典项数据
|
|
||||||
const handleExportDictItems = async () => {
|
|
||||||
if (!selectedDict) {
|
|
||||||
message.warning('请先选择字典');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 获取当前字典的所有数据
|
|
||||||
const response = await dictApi.dictcontrollerGetdictitems({
|
|
||||||
dictId: selectedDict.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response || response.length === 0) {
|
|
||||||
message.warning('当前字典没有数据可导出');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 将数据转换为CSV格式
|
|
||||||
const headers = [
|
|
||||||
'name',
|
|
||||||
'title',
|
|
||||||
'titleCN',
|
|
||||||
'value',
|
|
||||||
'sort',
|
|
||||||
'image',
|
|
||||||
'shortName',
|
|
||||||
];
|
|
||||||
const csvContent = [
|
|
||||||
headers.join(','),
|
|
||||||
...response.map((item: any) =>
|
|
||||||
headers
|
|
||||||
.map((header) => {
|
|
||||||
const value = item[header] || '';
|
|
||||||
// 如果值包含逗号或引号,需要转义
|
|
||||||
if (
|
|
||||||
typeof value === 'string' &&
|
|
||||||
(value.includes(',') || value.includes('"'))
|
|
||||||
) {
|
|
||||||
return `"${value.replace(/"/g, '""')}"`;
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
})
|
|
||||||
.join(','),
|
|
||||||
),
|
|
||||||
].join('\n');
|
|
||||||
|
|
||||||
// 创建blob并下载
|
|
||||||
const blob = new Blob(['\ufeff' + csvContent], {
|
|
||||||
// 添加BOM以支持中文
|
|
||||||
type: 'text/csv;charset=utf-8',
|
|
||||||
});
|
|
||||||
const url = window.URL.createObjectURL(blob);
|
|
||||||
const link = document.createElement('a');
|
|
||||||
link.href = url;
|
|
||||||
link.setAttribute('download', `${selectedDict.name}_dict_items.csv`);
|
|
||||||
document.body.appendChild(link);
|
|
||||||
link.click();
|
|
||||||
link.remove();
|
|
||||||
window.URL.revokeObjectURL(url);
|
|
||||||
|
|
||||||
message.success(`成功导出 ${response.length} 条数据`);
|
|
||||||
} catch (error: any) {
|
|
||||||
message.error('导出字典项失败:' + (error.message || '未知错误'));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Effects
|
|
||||||
useEffect(() => {
|
|
||||||
fetchDicts();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// 左侧字典表格的列定义
|
|
||||||
const dictColumns = [
|
|
||||||
{ title: '字典名称', dataIndex: 'name', key: 'name' },
|
|
||||||
{
|
|
||||||
title: '操作',
|
|
||||||
key: 'action',
|
|
||||||
render: (_: any, record: any) => (
|
|
||||||
<Space size="small">
|
|
||||||
<Button
|
|
||||||
type="link"
|
|
||||||
size="small"
|
|
||||||
onClick={() => openEditDictModal(record)}
|
|
||||||
>
|
|
||||||
编辑
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
type="link"
|
|
||||||
size="small"
|
|
||||||
danger
|
|
||||||
onClick={() => handleDeleteDict(record.id)}
|
|
||||||
>
|
|
||||||
删除
|
|
||||||
</Button>
|
|
||||||
</Space>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
// 右侧字典项列表的列定义
|
|
||||||
const dictItemColumns = [
|
|
||||||
{
|
|
||||||
title: '名称',
|
|
||||||
dataIndex: 'name',
|
|
||||||
key: 'name',
|
|
||||||
copyable: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '简称',
|
|
||||||
dataIndex: 'shortName',
|
|
||||||
key: 'shortName',
|
|
||||||
copyable: true,
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
title: '标题',
|
|
||||||
dataIndex: 'title',
|
|
||||||
key: 'title',
|
|
||||||
copyable: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '中文标题',
|
|
||||||
dataIndex: 'titleCN',
|
|
||||||
key: 'titleCN',
|
|
||||||
copyable: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '图片',
|
|
||||||
dataIndex: 'image',
|
|
||||||
key: 'image',
|
|
||||||
valueType: 'image',
|
|
||||||
width: 80,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '操作',
|
|
||||||
key: 'action',
|
|
||||||
render: (_: any, record: any) => (
|
|
||||||
<Space size="middle">
|
|
||||||
<Button type="link" onClick={() => handleEditDictItem(record)}>
|
|
||||||
编辑
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
type="link"
|
|
||||||
danger
|
|
||||||
onClick={() => handleDeleteDictItem(record.id)}
|
|
||||||
>
|
|
||||||
删除
|
|
||||||
</Button>
|
|
||||||
</Space>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<PageContainer>
|
|
||||||
<Layout style={{ background: '#fff' }}>
|
|
||||||
<Sider
|
|
||||||
width={300}
|
|
||||||
style={{
|
|
||||||
background: '#fff',
|
|
||||||
padding: '8px',
|
|
||||||
borderRight: '1px solid #f0f0f0',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Space direction="vertical" style={{ width: '100%' }}>
|
|
||||||
<Input.Search
|
|
||||||
size="small"
|
|
||||||
placeholder="搜索字典"
|
|
||||||
onSearch={handleSearch}
|
|
||||||
onChange={(e) => setSearchText(e.target.value)}
|
|
||||||
enterButton
|
|
||||||
allowClear
|
|
||||||
/>
|
|
||||||
<Space size="small">
|
|
||||||
<Button
|
|
||||||
type="primary"
|
|
||||||
onClick={() => setIsAddDictModalVisible(true)}
|
|
||||||
size="small"
|
|
||||||
>
|
|
||||||
添加字典
|
|
||||||
</Button>
|
|
||||||
<Upload
|
|
||||||
name="file"
|
|
||||||
action={undefined}
|
|
||||||
customRequest={async (options) => {
|
|
||||||
const { file, onSuccess, onError } = options;
|
|
||||||
try {
|
|
||||||
const result = await dictApi.dictcontrollerImportdicts({}, [
|
|
||||||
file as File,
|
|
||||||
]);
|
|
||||||
onSuccess?.(result);
|
|
||||||
} catch (error) {
|
|
||||||
onError?.(error as Error);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
showUploadList={false}
|
|
||||||
onChange={(info) => {
|
|
||||||
if (info.file.status === 'done') {
|
|
||||||
message.success(`${info.file.name} 文件上传成功`);
|
|
||||||
fetchDicts();
|
|
||||||
} else if (info.file.status === 'error') {
|
|
||||||
message.error(`${info.file.name} 文件上传失败`);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button size="small" icon={<UploadOutlined />}>
|
|
||||||
导入字典
|
|
||||||
</Button>
|
|
||||||
</Upload>
|
|
||||||
<Button size="small" onClick={handleDownloadDictTemplate}>
|
|
||||||
导出模板
|
|
||||||
</Button>
|
|
||||||
</Space>
|
|
||||||
<Table
|
|
||||||
dataSource={dicts}
|
|
||||||
columns={dictColumns}
|
|
||||||
rowKey="id"
|
|
||||||
loading={loadingDicts}
|
|
||||||
size="small"
|
|
||||||
onRow={(record) => ({
|
|
||||||
onClick: () => {
|
|
||||||
// 如果点击的是当前已选中的行,则取消选择
|
|
||||||
if (selectedDict?.id === record.id) {
|
|
||||||
setSelectedDict(null);
|
|
||||||
} else {
|
|
||||||
setSelectedDict(record);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})}
|
|
||||||
rowClassName={(record) =>
|
|
||||||
selectedDict?.id === record.id ? 'ant-table-row-selected' : ''
|
|
||||||
}
|
|
||||||
pagination={false}
|
|
||||||
/>
|
|
||||||
</Space>
|
|
||||||
</Sider>
|
|
||||||
<Content style={{ padding: '8px' }}>
|
|
||||||
<Space direction="vertical" style={{ width: '100%' }}>
|
|
||||||
<ProTable
|
|
||||||
columns={dictItemColumns}
|
|
||||||
request={async (params) => {
|
|
||||||
// 当没有选择字典时,不发起请求
|
|
||||||
if (!selectedDict?.id) {
|
|
||||||
return {
|
|
||||||
data: [],
|
|
||||||
success: true,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
const { name, title } = params;
|
|
||||||
const res = await dictApi.dictcontrollerGetdictitems({
|
|
||||||
dictId: selectedDict?.id,
|
|
||||||
name,
|
|
||||||
title,
|
|
||||||
});
|
|
||||||
|
|
||||||
// 适配新的响应格式,检查是否有 successResponse 包裹
|
|
||||||
if (res && res.success !== undefined) {
|
|
||||||
return {
|
|
||||||
data: res.data || [],
|
|
||||||
success: res.success,
|
|
||||||
total: res.data?.length || 0,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// 兼容旧的响应格式(直接返回数组)
|
|
||||||
return {
|
|
||||||
data: res || [],
|
|
||||||
success: true,
|
|
||||||
};
|
|
||||||
}}
|
|
||||||
rowKey="id"
|
|
||||||
search={{
|
|
||||||
layout: 'vertical',
|
|
||||||
}}
|
|
||||||
pagination={false}
|
|
||||||
options={{
|
|
||||||
reload: true,
|
|
||||||
density: true,
|
|
||||||
setting: true,
|
|
||||||
}}
|
|
||||||
size="small"
|
|
||||||
key={selectedDict?.id}
|
|
||||||
toolBarRender={() => [
|
|
||||||
<DictItemActions
|
|
||||||
key="dictItemActions"
|
|
||||||
selectedDict={selectedDict}
|
|
||||||
actionRef={actionRef}
|
|
||||||
showExport={true}
|
|
||||||
onImport={async (file: File, dictId: number) => {
|
|
||||||
// 创建 FormData 对象
|
|
||||||
const formData = new FormData();
|
|
||||||
// 添加文件到 FormData
|
|
||||||
formData.append('file', file);
|
|
||||||
// 添加字典 ID 到 FormData
|
|
||||||
formData.append('dictId', String(dictId));
|
|
||||||
// 调用导入字典项的 API,直接返回解析后的 JSON 对象
|
|
||||||
const result = await dictApi.dictcontrollerImportdictitems(
|
|
||||||
formData,
|
|
||||||
);
|
|
||||||
return result;
|
|
||||||
}}
|
|
||||||
onExport={handleExportDictItems}
|
|
||||||
onAdd={handleAddDictItem}
|
|
||||||
onRefreshDicts={fetchDicts}
|
|
||||||
/>,
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</Space>
|
|
||||||
</Content>
|
|
||||||
</Layout>
|
|
||||||
|
|
||||||
{/* 字典项 Modal(添加或编辑) */}
|
|
||||||
<DictItemModal
|
|
||||||
visible={isDictItemModalVisible}
|
|
||||||
isEdit={isEditDictItem}
|
|
||||||
editingData={editingDictItemData}
|
|
||||||
selectedDict={selectedDict}
|
|
||||||
onCancel={() => {
|
|
||||||
setIsDictItemModalVisible(false);
|
|
||||||
setEditingDictItemData(null);
|
|
||||||
}}
|
|
||||||
onOk={handleDictItemModalOk}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* 添加字典 Modal */}
|
|
||||||
<Modal
|
|
||||||
title="添加新字典"
|
|
||||||
open={isAddDictModalVisible}
|
|
||||||
onOk={handleAddDict}
|
|
||||||
onCancel={() => {
|
|
||||||
setIsAddDictModalVisible(false);
|
|
||||||
setAddDictName('');
|
|
||||||
setAddDictTitle('');
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Form layout="vertical">
|
|
||||||
<Form.Item label="字典名称">
|
|
||||||
<Input
|
|
||||||
placeholder="字典名称 (e.g., brand)"
|
|
||||||
value={addDictName}
|
|
||||||
onChange={(e) => setAddDictName(e.target.value)}
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item label="字典标题">
|
|
||||||
<Input
|
|
||||||
placeholder="字典标题 (e.g., 品牌)"
|
|
||||||
value={addDictTitle}
|
|
||||||
onChange={(e) => setAddDictTitle(e.target.value)}
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
|
||||||
</Form>
|
|
||||||
</Modal>
|
|
||||||
|
|
||||||
{/* 编辑字典 Modal */}
|
|
||||||
<Modal
|
|
||||||
title="编辑字典"
|
|
||||||
open={isEditDictModalVisible}
|
|
||||||
onOk={handleEditDict}
|
|
||||||
onCancel={() => {
|
|
||||||
setIsEditDictModalVisible(false);
|
|
||||||
setEditDictData(null);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Form layout="vertical">
|
|
||||||
<Form.Item label="字典名称">
|
|
||||||
<Input
|
|
||||||
placeholder="字典名称 (e.g., brand)"
|
|
||||||
value={editDictData?.name || ''}
|
|
||||||
onChange={(e) =>
|
|
||||||
setEditDictData({ ...editDictData, name: e.target.value })
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item label="字典标题">
|
|
||||||
<Input
|
|
||||||
placeholder="字典标题 (e.g., 品牌)"
|
|
||||||
value={editDictData?.title || ''}
|
|
||||||
onChange={(e) =>
|
|
||||||
setEditDictData({ ...editDictData, title: e.target.value })
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
|
||||||
</Form>
|
|
||||||
</Modal>
|
|
||||||
</PageContainer>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default DictPage;
|
|
||||||
|
|
@ -1,57 +0,0 @@
|
||||||
import { ActionType } from '@ant-design/pro-components';
|
|
||||||
import { Space } from 'antd';
|
|
||||||
import React from 'react';
|
|
||||||
import DictItemAddButton from './DictItemAddButton';
|
|
||||||
import DictItemExportButton from './DictItemExportButton';
|
|
||||||
import DictItemImportButton from './DictItemImportButton';
|
|
||||||
|
|
||||||
// 字典项操作组合组件的属性接口
|
|
||||||
interface DictItemActionsProps {
|
|
||||||
// 当前选中的字典
|
|
||||||
selectedDict?: any;
|
|
||||||
// ProTable 的 actionRef,用于刷新列表
|
|
||||||
actionRef?: React.MutableRefObject<ActionType | undefined>;
|
|
||||||
// 是否显示导出按钮(某些页面可能不需要导出功能)
|
|
||||||
showExport?: boolean;
|
|
||||||
// 导入字典项的回调函数(如果不提供,则使用默认的导入逻辑)
|
|
||||||
onImport?: (file: File, dictId: number) => Promise<any>;
|
|
||||||
// 导出字典项的回调函数
|
|
||||||
onExport?: () => Promise<void>;
|
|
||||||
// 添加字典项的回调函数
|
|
||||||
onAdd?: () => void;
|
|
||||||
// 刷新字典列表的回调函数(导入成功后可能需要刷新左侧字典列表)
|
|
||||||
onRefreshDicts?: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 字典项操作组合组件(包含添加、导入、导出按钮)
|
|
||||||
const DictItemActions: React.FC<DictItemActionsProps> = ({
|
|
||||||
selectedDict,
|
|
||||||
actionRef,
|
|
||||||
showExport = true,
|
|
||||||
onImport,
|
|
||||||
onExport,
|
|
||||||
onAdd,
|
|
||||||
onRefreshDicts,
|
|
||||||
}) => {
|
|
||||||
return (
|
|
||||||
<Space>
|
|
||||||
{/* 添加字典项按钮 */}
|
|
||||||
{onAdd && <DictItemAddButton disabled={!selectedDict} onClick={onAdd} />}
|
|
||||||
|
|
||||||
{/* 导入字典项按钮 */}
|
|
||||||
<DictItemImportButton
|
|
||||||
selectedDict={selectedDict}
|
|
||||||
actionRef={actionRef}
|
|
||||||
onImport={onImport}
|
|
||||||
onRefreshDicts={onRefreshDicts}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* 导出字典项按钮 */}
|
|
||||||
{showExport && (
|
|
||||||
<DictItemExportButton selectedDict={selectedDict} onExport={onExport} />
|
|
||||||
)}
|
|
||||||
</Space>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default DictItemActions;
|
|
||||||
|
|
@ -1,24 +0,0 @@
|
||||||
import { Button } from 'antd';
|
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
// 字典项添加按钮组件的属性接口
|
|
||||||
interface DictItemAddButtonProps {
|
|
||||||
// 是否禁用按钮
|
|
||||||
disabled?: boolean;
|
|
||||||
// 点击按钮时的回调函数
|
|
||||||
onClick: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 字典项添加按钮组件
|
|
||||||
const DictItemAddButton: React.FC<DictItemAddButtonProps> = ({
|
|
||||||
disabled = false,
|
|
||||||
onClick,
|
|
||||||
}) => {
|
|
||||||
return (
|
|
||||||
<Button type="primary" size="small" onClick={onClick} disabled={disabled}>
|
|
||||||
添加字典项
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default DictItemAddButton;
|
|
||||||
|
|
@ -1,53 +0,0 @@
|
||||||
import { DownloadOutlined } from '@ant-design/icons';
|
|
||||||
import { Button, message } from 'antd';
|
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
// 字典项导出按钮组件的属性接口
|
|
||||||
interface DictItemExportButtonProps {
|
|
||||||
// 当前选中的字典
|
|
||||||
selectedDict?: any;
|
|
||||||
// 是否禁用按钮
|
|
||||||
disabled?: boolean;
|
|
||||||
// 自定义导出函数
|
|
||||||
onExport?: () => Promise<void>;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 字典项导出按钮组件
|
|
||||||
const DictItemExportButton: React.FC<DictItemExportButtonProps> = ({
|
|
||||||
selectedDict,
|
|
||||||
disabled = false,
|
|
||||||
onExport,
|
|
||||||
}) => {
|
|
||||||
// 处理导出操作
|
|
||||||
const handleExport = async () => {
|
|
||||||
if (!selectedDict) {
|
|
||||||
message.warning('请先选择字典');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 如果提供了自定义导出函数,则使用自定义函数
|
|
||||||
if (onExport) {
|
|
||||||
await onExport();
|
|
||||||
} else {
|
|
||||||
// 如果没有提供自定义导出函数,这里可以添加默认逻辑
|
|
||||||
message.warning('未提供导出函数');
|
|
||||||
}
|
|
||||||
} catch (error: any) {
|
|
||||||
message.error('导出字典项失败:' + (error.message || '未知错误'));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
size="small"
|
|
||||||
icon={<DownloadOutlined />}
|
|
||||||
onClick={handleExport}
|
|
||||||
disabled={disabled || !selectedDict}
|
|
||||||
>
|
|
||||||
导出
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default DictItemExportButton;
|
|
||||||
|
|
@ -1,118 +0,0 @@
|
||||||
import { UploadOutlined } from '@ant-design/icons';
|
|
||||||
import { ActionType } from '@ant-design/pro-components';
|
|
||||||
import { request } from '@umijs/max';
|
|
||||||
import { Button, Upload, message } from 'antd';
|
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
// 字典项导入按钮组件的属性接口
|
|
||||||
interface DictItemImportButtonProps {
|
|
||||||
// 当前选中的字典
|
|
||||||
selectedDict?: any;
|
|
||||||
// ProTable 的 actionRef,用于刷新列表
|
|
||||||
actionRef?: React.MutableRefObject<ActionType | undefined>;
|
|
||||||
// 是否禁用按钮
|
|
||||||
disabled?: boolean;
|
|
||||||
// 自定义导入函数,返回 Promise(如果不提供,则使用默认的导入逻辑)
|
|
||||||
onImport?: (file: File, dictId: number) => Promise<any>;
|
|
||||||
// 导入成功后刷新字典列表的回调函数
|
|
||||||
onRefreshDicts?: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 字典项导入按钮组件
|
|
||||||
const DictItemImportButton: React.FC<DictItemImportButtonProps> = ({
|
|
||||||
selectedDict,
|
|
||||||
actionRef,
|
|
||||||
disabled = false,
|
|
||||||
onImport,
|
|
||||||
onRefreshDicts,
|
|
||||||
}) => {
|
|
||||||
// 处理导入文件上传
|
|
||||||
const handleImportUpload = async (options: any) => {
|
|
||||||
console.log(options);
|
|
||||||
const { file, onSuccess, onError } = options;
|
|
||||||
try {
|
|
||||||
// 条件判断,确保已选择字典
|
|
||||||
if (!selectedDict?.id) {
|
|
||||||
throw new Error('请先选择字典');
|
|
||||||
}
|
|
||||||
|
|
||||||
let result: any;
|
|
||||||
// 如果提供了自定义导入函数,则使用自定义函数
|
|
||||||
if (onImport) {
|
|
||||||
result = await onImport(file as File, selectedDict.id);
|
|
||||||
} else {
|
|
||||||
// 使用默认的导入逻辑,将 dictId 传入到 body 中
|
|
||||||
const formData = new FormData();
|
|
||||||
formData.append('file', file as File);
|
|
||||||
formData.append('dictId', String(selectedDict.id));
|
|
||||||
|
|
||||||
result = await request('/api/dict/item/import', {
|
|
||||||
method: 'POST',
|
|
||||||
body: formData,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 显示导入结果详情
|
|
||||||
showImportResult(result);
|
|
||||||
|
|
||||||
// 导入成功后刷新列表
|
|
||||||
setTimeout(() => {
|
|
||||||
actionRef?.current?.reload();
|
|
||||||
onRefreshDicts?.();
|
|
||||||
}, 100);
|
|
||||||
} catch (error: any) {
|
|
||||||
onError?.(error as Error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 显示导入结果详情
|
|
||||||
const showImportResult = (result: any) => {
|
|
||||||
// 从 result.data 中获取实际数据(因为后端返回格式为 { success: true, data: {...} })
|
|
||||||
const data = result.data || result;
|
|
||||||
const { total, processed, updated, created, errors } = data;
|
|
||||||
|
|
||||||
// 构建结果消息
|
|
||||||
let messageContent = `总共处理 ${total} 条,成功处理 ${processed} 条,新增 ${created} 条,更新 ${updated} 条`;
|
|
||||||
|
|
||||||
if (errors && errors.length > 0) {
|
|
||||||
messageContent += `,失败 ${errors.length} 条`;
|
|
||||||
// 显示错误详情
|
|
||||||
const errorDetails = errors
|
|
||||||
.map((err: any) => `${err.identifier}: ${err.error}`)
|
|
||||||
.join('\n');
|
|
||||||
message.warning(messageContent + '\n\n错误详情: \n' + errorDetails);
|
|
||||||
} else {
|
|
||||||
message.success(messageContent);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 处理上传状态变化
|
|
||||||
const handleUploadChange = (info: any) => {
|
|
||||||
if (info.file.status === 'done') {
|
|
||||||
message.success(`${info.file.name} 文件上传成功`);
|
|
||||||
} else if (info.file.status === 'error') {
|
|
||||||
message.error(`${info.file.name} 文件上传失败`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Upload
|
|
||||||
name="file"
|
|
||||||
action={undefined}
|
|
||||||
customRequest={handleImportUpload}
|
|
||||||
showUploadList={false}
|
|
||||||
disabled={disabled || !selectedDict}
|
|
||||||
onChange={handleUploadChange}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
size="small"
|
|
||||||
icon={<UploadOutlined />}
|
|
||||||
disabled={disabled || !selectedDict}
|
|
||||||
>
|
|
||||||
导入字典项
|
|
||||||
</Button>
|
|
||||||
</Upload>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default DictItemImportButton;
|
|
||||||
|
|
@ -1,96 +0,0 @@
|
||||||
import { Form, Input, Modal } from 'antd';
|
|
||||||
import React, { useEffect } from 'react';
|
|
||||||
|
|
||||||
interface DictItemModalProps {
|
|
||||||
// 模态框是否可见
|
|
||||||
visible: boolean;
|
|
||||||
// 是否为编辑模式
|
|
||||||
isEdit: boolean;
|
|
||||||
// 编辑时的字典项数据
|
|
||||||
editingData?: any;
|
|
||||||
// 当前选中的字典
|
|
||||||
selectedDict?: any;
|
|
||||||
// 取消回调
|
|
||||||
onCancel: () => void;
|
|
||||||
// 确认回调
|
|
||||||
onOk: (values: any) => Promise<void>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const DictItemModal: React.FC<DictItemModalProps> = ({
|
|
||||||
visible,
|
|
||||||
isEdit,
|
|
||||||
editingData,
|
|
||||||
selectedDict,
|
|
||||||
onCancel,
|
|
||||||
onOk,
|
|
||||||
}) => {
|
|
||||||
const [form] = Form.useForm();
|
|
||||||
|
|
||||||
// 当模态框打开或编辑数据变化时,重置或设置表单值
|
|
||||||
useEffect(() => {
|
|
||||||
if (visible) {
|
|
||||||
if (isEdit && editingData) {
|
|
||||||
// 编辑模式,设置表单值为编辑数据
|
|
||||||
form.setFieldsValue(editingData);
|
|
||||||
} else {
|
|
||||||
// 新增模式,重置表单
|
|
||||||
form.resetFields();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [visible, isEdit, editingData, form]);
|
|
||||||
|
|
||||||
// 表单提交处理
|
|
||||||
const handleOk = async () => {
|
|
||||||
try {
|
|
||||||
const values = await form.validateFields();
|
|
||||||
await onOk(values);
|
|
||||||
} catch (error) {
|
|
||||||
// 表单验证失败,不关闭模态框
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Modal
|
|
||||||
title={
|
|
||||||
isEdit
|
|
||||||
? '编辑字典项'
|
|
||||||
: `添加字典项 - ${selectedDict?.title || '未选择字典'}`
|
|
||||||
}
|
|
||||||
open={visible}
|
|
||||||
onOk={handleOk}
|
|
||||||
onCancel={onCancel}
|
|
||||||
destroyOnClose
|
|
||||||
>
|
|
||||||
<Form form={form} layout="vertical">
|
|
||||||
<Form.Item
|
|
||||||
label="名称"
|
|
||||||
name="name"
|
|
||||||
rules={[{ required: true, message: '请输入名称' }]}
|
|
||||||
>
|
|
||||||
<Input placeholder="名称 (e.g., zyn)" />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
label="标题"
|
|
||||||
name="title"
|
|
||||||
rules={[{ required: true, message: '请输入标题' }]}
|
|
||||||
>
|
|
||||||
<Input placeholder="标题 (e.g., ZYN)" />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item label="中文标题" name="titleCN">
|
|
||||||
<Input placeholder="中文标题 (e.g., 品牌)" />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item label="简称 (可选)" name="shortName">
|
|
||||||
<Input placeholder="简称 (可选)" />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item label="图片 (可选)" name="image">
|
|
||||||
<Input placeholder="图片链接 (可选)" />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item label="值 (可选)" name="value">
|
|
||||||
<Input placeholder="值 (可选)" />
|
|
||||||
</Form.Item>
|
|
||||||
</Form>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default DictItemModal;
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
import { useDeviceFingerprint } from '@/hooks/useDeviceFingerprint';
|
|
||||||
import { usercontrollerGetuser, usercontrollerLogin } from '@/servers/api/user';
|
import { usercontrollerGetuser, usercontrollerLogin } from '@/servers/api/user';
|
||||||
import { LockOutlined, UserOutlined } from '@ant-design/icons';
|
import { LockOutlined, UserOutlined } from '@ant-design/icons';
|
||||||
import {
|
import {
|
||||||
|
|
@ -8,6 +7,7 @@ import {
|
||||||
} from '@ant-design/pro-components';
|
} from '@ant-design/pro-components';
|
||||||
import { history, useModel } from '@umijs/max';
|
import { history, useModel } from '@umijs/max';
|
||||||
import { App, theme } from 'antd';
|
import { App, theme } from 'antd';
|
||||||
|
import {useDeviceFingerprint} from '@/hooks/useDeviceFingerprint';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
const Page = () => {
|
const Page = () => {
|
||||||
|
|
@ -15,32 +15,28 @@ const Page = () => {
|
||||||
const { token } = theme.useToken();
|
const { token } = theme.useToken();
|
||||||
const { message } = App.useApp();
|
const { message } = App.useApp();
|
||||||
const deviceId = useDeviceFingerprint();
|
const deviceId = useDeviceFingerprint();
|
||||||
const [isAuth, setIsAuth] = useState(false);
|
const [ isAuth, setIsAuth ] = useState(false)
|
||||||
|
|
||||||
console.log(deviceId);
|
console.log(deviceId) ;
|
||||||
|
|
||||||
const onFinish = async (values: { username: string; password: string }) => {
|
const onFinish = async (values: { username: string; password: string }) => {
|
||||||
try {
|
try {
|
||||||
const {
|
const { data, success, code, message: msg } = await usercontrollerLogin({...values, deviceId});
|
||||||
data,
|
|
||||||
success,
|
|
||||||
code,
|
|
||||||
message: msg,
|
|
||||||
} = await usercontrollerLogin({ ...values, deviceId });
|
|
||||||
if (success) {
|
if (success) {
|
||||||
message.success('登录成功');
|
message.success('登录成功');
|
||||||
localStorage.setItem('token', data?.token as string);
|
localStorage.setItem('token', data?.token as string);
|
||||||
const { data: user } = await usercontrollerGetuser();
|
const { data: user } = await usercontrollerGetuser();
|
||||||
setInitialState({ user });
|
setInitialState({ user });
|
||||||
history.push('/');
|
history.push('/');
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
if (code === 10001) {
|
if(code === 10001){
|
||||||
message.info('验证码已发送至管理邮箱');
|
message.info("验证码已发送至管理邮箱")
|
||||||
setIsAuth(true);
|
setIsAuth(true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
message.error(msg);
|
message.error(msg);
|
||||||
|
|
||||||
} catch {
|
} catch {
|
||||||
message.error('登录失败');
|
message.error('登录失败');
|
||||||
}
|
}
|
||||||
|
|
@ -96,25 +92,24 @@ const Page = () => {
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
}}
|
}}
|
||||||
placeholder={'请输入密码!'}
|
placeholder={'请输入密码!'}
|
||||||
rules={[
|
rules={[
|
||||||
{
|
{
|
||||||
required: true,
|
required: true,
|
||||||
message: '请输入密码!',
|
message: '请输入密码!',
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
{isAuth ? (
|
{
|
||||||
|
isAuth?
|
||||||
<ProFormText
|
<ProFormText
|
||||||
name="authCode"
|
name="authCode"
|
||||||
label="验证码"
|
label="验证码"
|
||||||
width="lg"
|
width="lg"
|
||||||
placeholder="请输入验证码"
|
placeholder="请输入验证码"
|
||||||
rules={[{ required: true, message: '请输入验证码' }]}
|
rules={[{ required: true, message: '请输入验证码' }]}
|
||||||
/>
|
/>:<></>
|
||||||
) : (
|
}
|
||||||
<></>
|
|
||||||
)}
|
|
||||||
{/* <div
|
{/* <div
|
||||||
style={{
|
style={{
|
||||||
marginBlockEnd: 24,
|
marginBlockEnd: 24,
|
||||||
|
|
|
||||||
|
|
@ -75,7 +75,7 @@ const ListPage: React.FC = () => {
|
||||||
<Divider type="vertical" />
|
<Divider type="vertical" />
|
||||||
<Popconfirm
|
<Popconfirm
|
||||||
title="删除"
|
title="删除"
|
||||||
description="确认删除?"
|
description="确认删除?"
|
||||||
onConfirm={async () => {
|
onConfirm={async () => {
|
||||||
try {
|
try {
|
||||||
const { success, message: errMsg } =
|
const { success, message: errMsg } =
|
||||||
|
|
@ -393,14 +393,6 @@ const UpdateForm: React.FC<{
|
||||||
}));
|
}));
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<ProFormText
|
|
||||||
name={['email']}
|
|
||||||
label="邮箱"
|
|
||||||
width="lg"
|
|
||||||
placeholder="请输入邮箱"
|
|
||||||
required
|
|
||||||
rules={[{ required: true, message: '请输入邮箱' }]}
|
|
||||||
/>
|
|
||||||
<ProForm.Group title="地址">
|
<ProForm.Group title="地址">
|
||||||
<ProFormText
|
<ProFormText
|
||||||
name={['address', 'country']}
|
name={['address', 'country']}
|
||||||
|
|
@ -439,8 +431,6 @@ const UpdateForm: React.FC<{
|
||||||
required
|
required
|
||||||
rules={[{ required: true, message: '请输入详细地址' }]}
|
rules={[{ required: true, message: '请输入详细地址' }]}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
||||||
</ProForm.Group>
|
</ProForm.Group>
|
||||||
<ProFormItem
|
<ProFormItem
|
||||||
name="contact"
|
name="contact"
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,12 @@
|
||||||
import {
|
import { logisticscontrollerGetlist, logisticscontrollerGetshipmentlabel,
|
||||||
logisticscontrollerDeleteshipment,
|
logisticscontrollerDeleteshipment,
|
||||||
logisticscontrollerGetlist,
|
logisticscontrollerUpdateshipmentstate
|
||||||
logisticscontrollerGetshipmentlabel,
|
} from '@/servers/api/logistics';
|
||||||
logisticscontrollerUpdateshipmentstate,
|
|
||||||
} from '@/servers/api/logistics';
|
|
||||||
import { sitecontrollerAll } from '@/servers/api/site';
|
|
||||||
import { stockcontrollerGetallstockpoints } from '@/servers/api/stock';
|
import { stockcontrollerGetallstockpoints } from '@/servers/api/stock';
|
||||||
import { formatUniuniShipmentState } from '@/utils/format';
|
import { formatUniuniShipmentState } from '@/utils/format';
|
||||||
import { printPDF } from '@/utils/util';
|
import { printPDF } from '@/utils/util';
|
||||||
import { CopyOutlined } from '@ant-design/icons';
|
import { CopyOutlined } from '@ant-design/icons';
|
||||||
|
import { ToastContainer, toast } from 'react-toastify';
|
||||||
import {
|
import {
|
||||||
ActionType,
|
ActionType,
|
||||||
PageContainer,
|
PageContainer,
|
||||||
|
|
@ -17,7 +15,7 @@ import {
|
||||||
} from '@ant-design/pro-components';
|
} from '@ant-design/pro-components';
|
||||||
import { App, Button, Divider, Popconfirm } from 'antd';
|
import { App, Button, Divider, Popconfirm } from 'antd';
|
||||||
import { useRef, useState } from 'react';
|
import { useRef, useState } from 'react';
|
||||||
import { ToastContainer } from 'react-toastify';
|
import { sitecontrollerAll } from '@/servers/api/site';
|
||||||
|
|
||||||
const ListPage: React.FC = () => {
|
const ListPage: React.FC = () => {
|
||||||
const actionRef = useRef<ActionType>();
|
const actionRef = useRef<ActionType>();
|
||||||
|
|
@ -52,7 +50,7 @@ const ListPage: React.FC = () => {
|
||||||
request: async () => {
|
request: async () => {
|
||||||
const { data = [] } = await sitecontrollerAll();
|
const { data = [] } = await sitecontrollerAll();
|
||||||
return data.map((item) => ({
|
return data.map((item) => ({
|
||||||
label: item.name,
|
label: item.siteName,
|
||||||
value: item.id,
|
value: item.id,
|
||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
|
|
@ -71,12 +69,10 @@ const ListPage: React.FC = () => {
|
||||||
<CopyOutlined
|
<CopyOutlined
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
try {
|
try {
|
||||||
await navigator.clipboard.writeText(
|
await navigator.clipboard.writeText(record.return_tracking_number);
|
||||||
record.return_tracking_number,
|
message.success('复制成功!');
|
||||||
);
|
|
||||||
message.success('复制成功!');
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
message.error('复制失败!');
|
message.error('复制失败!');
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
@ -110,9 +106,7 @@ const ListPage: React.FC = () => {
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
const { data } = await logisticscontrollerGetshipmentlabel({
|
const { data } = await logisticscontrollerGetshipmentlabel({shipmentId:record.id});
|
||||||
shipmentId: record.id,
|
|
||||||
});
|
|
||||||
const content = data.content;
|
const content = data.content;
|
||||||
printPDF([content]);
|
printPDF([content]);
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
|
|
@ -126,9 +120,7 @@ const ListPage: React.FC = () => {
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
const res = await logisticscontrollerUpdateshipmentstate({
|
const res = await logisticscontrollerUpdateshipmentstate({shipmentId:record.id});
|
||||||
shipmentId: record.id,
|
|
||||||
});
|
|
||||||
console.log('res', res);
|
console.log('res', res);
|
||||||
|
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
|
|
@ -140,12 +132,12 @@ const ListPage: React.FC = () => {
|
||||||
<Popconfirm
|
<Popconfirm
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
title="删除"
|
title="删除"
|
||||||
description="确认删除?"
|
description="确认删除?"
|
||||||
onConfirm={async () => {
|
onConfirm={async () => {
|
||||||
try {
|
try {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
const { success, message: errMsg } =
|
const { success, message: errMsg } =
|
||||||
await logisticscontrollerDeleteshipment({ id: record.id });
|
await logisticscontrollerDeleteshipment({id:record.id});
|
||||||
if (!success) {
|
if (!success) {
|
||||||
throw new Error(errMsg);
|
throw new Error(errMsg);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import {
|
import {
|
||||||
logisticscontrollerGetservicelist,
|
logisticscontrollerGetservicelist,
|
||||||
|
logisticscontrollerSyncservices,
|
||||||
logisticscontrollerToggleactive,
|
logisticscontrollerToggleactive,
|
||||||
} from '@/servers/api/logistics';
|
} from '@/servers/api/logistics';
|
||||||
import {
|
import {
|
||||||
|
|
@ -9,7 +10,7 @@ import {
|
||||||
ProFormSwitch,
|
ProFormSwitch,
|
||||||
ProTable,
|
ProTable,
|
||||||
} from '@ant-design/pro-components';
|
} from '@ant-design/pro-components';
|
||||||
import { App } from 'antd';
|
import { App, Button } from 'antd';
|
||||||
import { useRef } from 'react';
|
import { useRef } from 'react';
|
||||||
|
|
||||||
const ListPage: React.FC = () => {
|
const ListPage: React.FC = () => {
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,11 @@
|
||||||
import { ordercontrollerGetordersales } from '@/servers/api/order';
|
import React, { useRef } from 'react';
|
||||||
import { sitecontrollerAll } from '@/servers/api/site';
|
|
||||||
import type {
|
|
||||||
ActionType,
|
|
||||||
ProColumns,
|
|
||||||
ProTableProps,
|
|
||||||
} from '@ant-design/pro-components';
|
|
||||||
import { ProTable } from '@ant-design/pro-components';
|
|
||||||
import { PageContainer } from '@ant-design/pro-layout';
|
import { PageContainer } from '@ant-design/pro-layout';
|
||||||
|
import type { ProColumns, ActionType, ProTableProps } from '@ant-design/pro-components';
|
||||||
|
import { ProTable } from '@ant-design/pro-components';
|
||||||
import { App } from 'antd';
|
import { App } from 'antd';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import React, { useRef } from 'react';
|
import { ordercontrollerGetordersales } from '@/servers/api/order';
|
||||||
|
import { sitecontrollerAll } from '@/servers/api/site';
|
||||||
|
|
||||||
// 列表行数据结构(订单商品聚合)
|
// 列表行数据结构(订单商品聚合)
|
||||||
interface OrderItemAggRow {
|
interface OrderItemAggRow {
|
||||||
|
|
@ -28,7 +24,7 @@ const OrderItemsPage: React.FC = () => {
|
||||||
const actionRef = useRef<ActionType>();
|
const actionRef = useRef<ActionType>();
|
||||||
const { message } = App.useApp();
|
const { message } = App.useApp();
|
||||||
|
|
||||||
// 列配置(中文标题,符合当前项目风格;显示英文默认语言可后续走国际化)
|
// 列配置(中文标题,符合当前项目风格;显示英文默认语言可后续走国际化)
|
||||||
const columns: ProColumns<OrderItemAggRow>[] = [
|
const columns: ProColumns<OrderItemAggRow>[] = [
|
||||||
{
|
{
|
||||||
title: '商品名称',
|
title: '商品名称',
|
||||||
|
|
@ -91,10 +87,7 @@ const OrderItemsPage: React.FC = () => {
|
||||||
request: async () => {
|
request: async () => {
|
||||||
// 拉取站点列表(后台 /site/all)
|
// 拉取站点列表(后台 /site/all)
|
||||||
const { data = [] } = await sitecontrollerAll();
|
const { data = [] } = await sitecontrollerAll();
|
||||||
return (data || []).map((item: any) => ({
|
return (data || []).map((item: any) => ({ label: item.siteName, value: item.id }));
|
||||||
label: item.name,
|
|
||||||
value: item.id,
|
|
||||||
}));
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -111,9 +104,7 @@ const OrderItemsPage: React.FC = () => {
|
||||||
];
|
];
|
||||||
|
|
||||||
// 表格请求方法:调用 /order/getOrderSales 接口并设置 isSource=true 获取订单项聚合
|
// 表格请求方法:调用 /order/getOrderSales 接口并设置 isSource=true 获取订单项聚合
|
||||||
const request: ProTableProps<OrderItemAggRow>['request'] = async (
|
const request: ProTableProps<OrderItemAggRow>['request'] = async (params:any) => {
|
||||||
params: any,
|
|
||||||
) => {
|
|
||||||
try {
|
try {
|
||||||
const { current = 1, pageSize = 10, siteId, name } = params as any;
|
const { current = 1, pageSize = 10, siteId, name } = params as any;
|
||||||
const [startDate, endDate] = (params as any).dateRange || [];
|
const [startDate, endDate] = (params as any).dateRange || [];
|
||||||
|
|
@ -124,9 +115,7 @@ const OrderItemsPage: React.FC = () => {
|
||||||
siteId,
|
siteId,
|
||||||
name,
|
name,
|
||||||
isSource: true as any,
|
isSource: true as any,
|
||||||
startDate: startDate
|
startDate: startDate ? (dayjs(startDate).toISOString() as any) : undefined,
|
||||||
? (dayjs(startDate).toISOString() as any)
|
|
||||||
: undefined,
|
|
||||||
endDate: endDate ? (dayjs(endDate).toISOString() as any) : undefined,
|
endDate: endDate ? (dayjs(endDate).toISOString() as any) : undefined,
|
||||||
} as any);
|
} as any);
|
||||||
const { success, data, message: errMsg } = resp as any;
|
const { success, data, message: errMsg } = resp as any;
|
||||||
|
|
@ -143,18 +132,13 @@ const OrderItemsPage: React.FC = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageContainer title="订单商品概览">
|
<PageContainer title='订单商品概览'>
|
||||||
<ProTable<OrderItemAggRow>
|
<ProTable<OrderItemAggRow>
|
||||||
actionRef={actionRef}
|
actionRef={actionRef}
|
||||||
rowKey={(r) =>
|
rowKey={(r) => `${r.externalProductId}-${r.externalVariationId}-${r.name}`}
|
||||||
`${r.externalProductId}-${r.externalVariationId}-${r.name}`
|
|
||||||
}
|
|
||||||
columns={columns}
|
columns={columns}
|
||||||
request={request}
|
request={request}
|
||||||
pagination={{
|
pagination={{ showSizeChanger: true }}
|
||||||
showSizeChanger: true,
|
|
||||||
showQuickJumper: true,
|
|
||||||
}}
|
|
||||||
search={{ labelWidth: 90, span: 6 }}
|
search={{ labelWidth: 90, span: 6 }}
|
||||||
toolBarRender={false}
|
toolBarRender={false}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -37,7 +37,7 @@ const ListPage: React.FC = () => {
|
||||||
hideInSearch: true,
|
hideInSearch: true,
|
||||||
width: 800,
|
width: 800,
|
||||||
render: (_, record) => {
|
render: (_, record) => {
|
||||||
return record?.numbers?.join?.(',');
|
return record?.numbers?.join?.('、');
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
@ -72,7 +72,7 @@ const ListPage: React.FC = () => {
|
||||||
|
|
||||||
// 数据行
|
// 数据行
|
||||||
const rows = (data?.items || []).map((item) => {
|
const rows = (data?.items || []).map((item) => {
|
||||||
return [item.name, item.quantity, item.numbers?.join(',')];
|
return [item.name, item.quantity, item.numbers?.join('、')];
|
||||||
});
|
});
|
||||||
|
|
||||||
// 导出
|
// 导出
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,6 @@
|
||||||
import {
|
import {
|
||||||
usercontrollerAdduser,
|
usercontrollerAdduser,
|
||||||
usercontrollerListusers,
|
usercontrollerListusers,
|
||||||
usercontrollerToggleactive,
|
|
||||||
usercontrollerUpdateuser,
|
|
||||||
} from '@/servers/api/user';
|
} from '@/servers/api/user';
|
||||||
import { PlusOutlined } from '@ant-design/icons';
|
import { PlusOutlined } from '@ant-design/icons';
|
||||||
import {
|
import {
|
||||||
|
|
@ -11,41 +9,18 @@ import {
|
||||||
PageContainer,
|
PageContainer,
|
||||||
ProColumns,
|
ProColumns,
|
||||||
ProForm,
|
ProForm,
|
||||||
ProFormSwitch,
|
|
||||||
ProFormText,
|
ProFormText,
|
||||||
ProFormTextArea,
|
|
||||||
ProTable,
|
ProTable,
|
||||||
} from '@ant-design/pro-components';
|
} from '@ant-design/pro-components';
|
||||||
import { App, Button, Tag } from 'antd';
|
import { App, Button } from 'antd';
|
||||||
import { useRef } from 'react';
|
import { useRef } from 'react';
|
||||||
|
|
||||||
const ListPage: React.FC = () => {
|
const ListPage: React.FC = () => {
|
||||||
const actionRef = useRef<ActionType>();
|
const actionRef = useRef<ActionType>();
|
||||||
const { message } = App.useApp();
|
|
||||||
const columns: ProColumns[] = [
|
const columns: ProColumns[] = [
|
||||||
{
|
{
|
||||||
title: '用户名',
|
title: '用户名',
|
||||||
dataIndex: 'username',
|
dataIndex: 'username',
|
||||||
sorter: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '邮箱',
|
|
||||||
dataIndex: 'email',
|
|
||||||
sorter: true,
|
|
||||||
ellipsis: true,
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
title: '超管',
|
|
||||||
dataIndex: 'isSuper',
|
|
||||||
valueType: 'select',
|
|
||||||
valueEnum: {
|
|
||||||
true: { text: '是' },
|
|
||||||
false: { text: '否' },
|
|
||||||
},
|
|
||||||
sorter: true,
|
|
||||||
filters: true,
|
|
||||||
filterMultiple: false,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '激活',
|
title: '激活',
|
||||||
|
|
@ -58,46 +33,18 @@ const ListPage: React.FC = () => {
|
||||||
text: '否',
|
text: '否',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
sorter: true,
|
|
||||||
filters: true,
|
|
||||||
filterMultiple: false,
|
|
||||||
render: (_, record: any) => (
|
|
||||||
<Tag color={record?.isActive ? 'green' : 'red'}>
|
|
||||||
{record?.isActive ? '启用中' : '已禁用'}
|
|
||||||
</Tag>
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '备注',
|
title: '超管',
|
||||||
dataIndex: 'remark',
|
dataIndex: 'isSuper',
|
||||||
ellipsis: true,
|
valueEnum: {
|
||||||
|
true: {
|
||||||
|
text: '是',
|
||||||
|
},
|
||||||
|
false: {
|
||||||
|
text: '否',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
title: '操作',
|
|
||||||
dataIndex: 'option',
|
|
||||||
valueType: 'option',
|
|
||||||
render: (_, record: any) => (
|
|
||||||
<>
|
|
||||||
<EditForm record={record} tableRef={actionRef} />
|
|
||||||
<Button
|
|
||||||
danger={record.isActive}
|
|
||||||
type="link"
|
|
||||||
onClick={async () => {
|
|
||||||
// 软删除为禁用(isActive=false),再次点击可启用
|
|
||||||
const next = !record.isActive;
|
|
||||||
const { success, message: errMsg } =
|
|
||||||
await usercontrollerToggleactive({
|
|
||||||
userId: record.id,
|
|
||||||
isActive: next,
|
|
||||||
});
|
|
||||||
if (!success) return message.error(errMsg);
|
|
||||||
actionRef.current?.reload();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{record.isActive ? '禁用' : '启用'}
|
|
||||||
</Button>
|
|
||||||
</>
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
return (
|
return (
|
||||||
|
|
@ -106,45 +53,9 @@ const ListPage: React.FC = () => {
|
||||||
headerTitle="查询表格"
|
headerTitle="查询表格"
|
||||||
actionRef={actionRef}
|
actionRef={actionRef}
|
||||||
rowKey="id"
|
rowKey="id"
|
||||||
request={async (params, sort, filter) => {
|
request={async (params) => {
|
||||||
const {
|
const { data, success } = await usercontrollerListusers(params);
|
||||||
current = 1,
|
|
||||||
pageSize = 10,
|
|
||||||
username,
|
|
||||||
email,
|
|
||||||
isActive,
|
|
||||||
isSuper,
|
|
||||||
remark,
|
|
||||||
} = params as any;
|
|
||||||
console.log(`params`, params, sort);
|
|
||||||
const qp: any = { current, pageSize };
|
|
||||||
if (username) qp.username = username;
|
|
||||||
// 条件判断 透传邮箱查询参数
|
|
||||||
if (email) qp.email = email;
|
|
||||||
if (typeof isActive !== 'undefined' && isActive !== '')
|
|
||||||
qp.isActive = String(isActive);
|
|
||||||
if (typeof isSuper !== 'undefined' && isSuper !== '')
|
|
||||||
qp.isSuper = String(isSuper);
|
|
||||||
|
|
||||||
// 处理表头筛选
|
|
||||||
if (filter.isActive && filter.isActive.length > 0) {
|
|
||||||
qp.isActive = filter.isActive[0];
|
|
||||||
}
|
|
||||||
if (filter.isSuper && filter.isSuper.length > 0) {
|
|
||||||
qp.isSuper = filter.isSuper[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (remark) qp.remark = remark;
|
|
||||||
|
|
||||||
const sortField = Object.keys(sort)[0];
|
|
||||||
if (sortField) {
|
|
||||||
qp.sortField = sortField;
|
|
||||||
qp.sortOrder = sort[sortField];
|
|
||||||
}
|
|
||||||
|
|
||||||
const { data, success } = await usercontrollerListusers({
|
|
||||||
params: qp,
|
|
||||||
});
|
|
||||||
return {
|
return {
|
||||||
total: data?.total || 0,
|
total: data?.total || 0,
|
||||||
data: data?.items || [],
|
data: data?.items || [],
|
||||||
|
|
@ -199,13 +110,6 @@ const CreateForm: React.FC<{
|
||||||
placeholder="请输入用户名"
|
placeholder="请输入用户名"
|
||||||
rules={[{ required: true, message: '请输入用户名' }]}
|
rules={[{ required: true, message: '请输入用户名' }]}
|
||||||
/>
|
/>
|
||||||
<ProFormText
|
|
||||||
name="email"
|
|
||||||
label="邮箱"
|
|
||||||
width="lg"
|
|
||||||
placeholder="请输入邮箱"
|
|
||||||
rules={[{ type: 'email', message: '请输入正确的邮箱' }]}
|
|
||||||
/>
|
|
||||||
<ProFormText
|
<ProFormText
|
||||||
name="password"
|
name="password"
|
||||||
label="密码"
|
label="密码"
|
||||||
|
|
@ -213,81 +117,6 @@ const CreateForm: React.FC<{
|
||||||
placeholder="请输入密码"
|
placeholder="请输入密码"
|
||||||
rules={[{ required: true, message: '请输入密码' }]}
|
rules={[{ required: true, message: '请输入密码' }]}
|
||||||
/>
|
/>
|
||||||
<ProFormSwitch name="isSuper" label="超管" />
|
|
||||||
<ProFormSwitch name="isAdmin" label="管理员" />
|
|
||||||
<ProFormTextArea
|
|
||||||
name="remark"
|
|
||||||
label="备注"
|
|
||||||
placeholder="请输入备注"
|
|
||||||
fieldProps={{ autoSize: { minRows: 2, maxRows: 4 } }}
|
|
||||||
/>
|
|
||||||
</ProForm.Group>
|
|
||||||
</DrawerForm>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const EditForm: React.FC<{
|
|
||||||
tableRef: React.MutableRefObject<ActionType | undefined>;
|
|
||||||
record: any;
|
|
||||||
}> = ({ tableRef, record }) => {
|
|
||||||
const { message } = App.useApp();
|
|
||||||
return (
|
|
||||||
<DrawerForm
|
|
||||||
title="编辑"
|
|
||||||
trigger={<Button type="link">编辑</Button>}
|
|
||||||
initialValues={{
|
|
||||||
username: record.username,
|
|
||||||
email: record.email,
|
|
||||||
isSuper: record.isSuper,
|
|
||||||
isAdmin: record.isAdmin,
|
|
||||||
remark: record.remark,
|
|
||||||
}}
|
|
||||||
onFinish={async (values: any) => {
|
|
||||||
try {
|
|
||||||
// 更新用户,密码可选填
|
|
||||||
const { success, message: err } = await usercontrollerUpdateuser(
|
|
||||||
{ id: record.id },
|
|
||||||
values,
|
|
||||||
);
|
|
||||||
if (!success) throw new Error(err);
|
|
||||||
tableRef.current?.reload();
|
|
||||||
message.success('更新成功');
|
|
||||||
return true;
|
|
||||||
} catch (error: any) {
|
|
||||||
message.error(error.message);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ProForm.Group>
|
|
||||||
<ProFormText
|
|
||||||
name="username"
|
|
||||||
label="用户名"
|
|
||||||
width="lg"
|
|
||||||
placeholder="请输入用户名"
|
|
||||||
rules={[{ required: true, message: '请输入用户名' }]}
|
|
||||||
/>
|
|
||||||
<ProFormText
|
|
||||||
name="email"
|
|
||||||
label="邮箱"
|
|
||||||
width="lg"
|
|
||||||
placeholder="请输入邮箱"
|
|
||||||
rules={[{ type: 'email', message: '请输入正确的邮箱' }]}
|
|
||||||
/>
|
|
||||||
<ProFormText
|
|
||||||
name="password"
|
|
||||||
label="密码(不填不改)"
|
|
||||||
width="lg"
|
|
||||||
placeholder="如需修改请输入新密码"
|
|
||||||
/>
|
|
||||||
<ProFormSwitch name="isSuper" label="超管" />
|
|
||||||
<ProFormSwitch name="isAdmin" label="管理员" />
|
|
||||||
<ProFormTextArea
|
|
||||||
name="remark"
|
|
||||||
label="备注"
|
|
||||||
placeholder="请输入备注"
|
|
||||||
fieldProps={{ autoSize: { minRows: 2, maxRows: 4 } }}
|
|
||||||
/>
|
|
||||||
</ProForm.Group>
|
</ProForm.Group>
|
||||||
</DrawerForm>
|
</DrawerForm>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,72 +0,0 @@
|
||||||
import { productcontrollerGetattributelist } from '@/servers/api/product';
|
|
||||||
import { ProFormSelect } from '@ant-design/pro-components';
|
|
||||||
import { useState } from 'react';
|
|
||||||
|
|
||||||
interface AttributeFormItemProps {
|
|
||||||
dictName: string;
|
|
||||||
name: string;
|
|
||||||
label: string;
|
|
||||||
isTag?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const fetchDictOptions = async (dictName: string, keyword?: string) => {
|
|
||||||
const { data } = await productcontrollerGetattributelist({
|
|
||||||
dictName,
|
|
||||||
name: keyword,
|
|
||||||
});
|
|
||||||
return (data?.items || []).map((item: any) => ({
|
|
||||||
label: item.name,
|
|
||||||
value: item.name,
|
|
||||||
id: item.id,
|
|
||||||
item,
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
const AttributeFormItem: React.FC<AttributeFormItemProps> = ({
|
|
||||||
dictName,
|
|
||||||
name,
|
|
||||||
label,
|
|
||||||
isTag = false,
|
|
||||||
}) => {
|
|
||||||
const [options, setOptions] = useState<{ label: string; value: string }[]>(
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
|
|
||||||
if (isTag) {
|
|
||||||
return (
|
|
||||||
<ProFormSelect
|
|
||||||
name={name}
|
|
||||||
width="lg"
|
|
||||||
label={label}
|
|
||||||
placeholder={`请输入或选择${label}`}
|
|
||||||
fieldProps={{
|
|
||||||
mode: 'tags',
|
|
||||||
showSearch: true,
|
|
||||||
filterOption: false,
|
|
||||||
onSearch: async (val) => {
|
|
||||||
const opts = await fetchDictOptions(dictName, val);
|
|
||||||
setOptions(opts);
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
request={async () => {
|
|
||||||
const opts = await fetchDictOptions(dictName);
|
|
||||||
setOptions(opts);
|
|
||||||
return opts;
|
|
||||||
}}
|
|
||||||
options={options}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ProFormSelect
|
|
||||||
name={name}
|
|
||||||
width="lg"
|
|
||||||
label={label}
|
|
||||||
placeholder={`请选择${label}`}
|
|
||||||
request={() => fetchDictOptions(dictName)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AttributeFormItem;
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
export const notAttributes = new Set(['zh-cn', 'en-us', 'category']);
|
|
||||||
|
|
@ -1,433 +0,0 @@
|
||||||
import * as dictApi from '@/servers/api/dict';
|
|
||||||
import {
|
|
||||||
ActionType,
|
|
||||||
PageContainer,
|
|
||||||
ProColumns,
|
|
||||||
ProTable,
|
|
||||||
} from '@ant-design/pro-components';
|
|
||||||
import { request } from '@umijs/max';
|
|
||||||
import { Button, Input, Layout, Space, Table, message } from 'antd';
|
|
||||||
import React, { useEffect, useRef, useState } from 'react';
|
|
||||||
import DictItemActions from '../../Dict/components/DictItemActions';
|
|
||||||
import DictItemModal from '../../Dict/components/DictItemModal';
|
|
||||||
|
|
||||||
const { Sider, Content } = Layout;
|
|
||||||
|
|
||||||
import { notAttributes } from './consts';
|
|
||||||
|
|
||||||
const AttributePage: React.FC = () => {
|
|
||||||
// 左侧字典列表状态
|
|
||||||
const [dicts, setDicts] = useState<any[]>([]);
|
|
||||||
const [loadingDicts, setLoadingDicts] = useState(false);
|
|
||||||
const [searchText, setSearchText] = useState('');
|
|
||||||
const [selectedDict, setSelectedDict] = useState<any>(null);
|
|
||||||
|
|
||||||
// 右侧字典项 ProTable 的引用
|
|
||||||
const actionRef = useRef<ActionType>();
|
|
||||||
|
|
||||||
// 字典项模态框状态(由 DictItemModal 组件管理)
|
|
||||||
const [isDictItemModalVisible, setIsDictItemModalVisible] = useState(false);
|
|
||||||
const [isEditDictItem, setIsEditDictItem] = useState(false);
|
|
||||||
const [editingDictItemData, setEditingDictItemData] = useState<any>(null);
|
|
||||||
|
|
||||||
// 导出字典项数据
|
|
||||||
const handleExportDictItems = async () => {
|
|
||||||
// 条件判断,确保已选择字典
|
|
||||||
if (!selectedDict) {
|
|
||||||
message.warning('请先选择字典');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 获取当前字典的所有数据
|
|
||||||
const response = await request('/dict/items', {
|
|
||||||
params: {
|
|
||||||
dictId: selectedDict.id,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// 确保返回的是数组
|
|
||||||
const data = Array.isArray(response) ? response : response?.data || [];
|
|
||||||
|
|
||||||
// 条件判断,检查是否有数据可导出
|
|
||||||
if (data.length === 0) {
|
|
||||||
message.warning('当前字典没有数据可导出');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 将数据转换为CSV格式
|
|
||||||
const headers = [
|
|
||||||
'name',
|
|
||||||
'title',
|
|
||||||
'titleCN',
|
|
||||||
'value',
|
|
||||||
'sort',
|
|
||||||
'image',
|
|
||||||
'shortName',
|
|
||||||
];
|
|
||||||
const csvContent = [
|
|
||||||
headers.join(','),
|
|
||||||
...data.map((item: any) =>
|
|
||||||
headers
|
|
||||||
.map((header) => {
|
|
||||||
const value = item[header] || '';
|
|
||||||
// 条件判断,如果值包含逗号或引号,需要转义
|
|
||||||
if (
|
|
||||||
typeof value === 'string' &&
|
|
||||||
(value.includes(',') || value.includes('"'))
|
|
||||||
) {
|
|
||||||
return `"${value.replace(/"/g, '""')}"`;
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
})
|
|
||||||
.join(','),
|
|
||||||
),
|
|
||||||
].join('\n');
|
|
||||||
|
|
||||||
// 创建blob并下载
|
|
||||||
const blob = new Blob(['\ufeff' + csvContent], {
|
|
||||||
// 添加BOM以支持中文
|
|
||||||
type: 'text/csv;charset=utf-8',
|
|
||||||
});
|
|
||||||
const url = window.URL.createObjectURL(blob);
|
|
||||||
const link = document.createElement('a');
|
|
||||||
link.href = url;
|
|
||||||
link.setAttribute('download', `${selectedDict.name}_dict_items.csv`);
|
|
||||||
document.body.appendChild(link);
|
|
||||||
link.click();
|
|
||||||
link.remove();
|
|
||||||
window.URL.revokeObjectURL(url);
|
|
||||||
|
|
||||||
message.success(`成功导出 ${data.length} 条数据`);
|
|
||||||
} catch (error: any) {
|
|
||||||
message.error('导出字典项失败:' + (error.message || '未知错误'));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const fetchDicts = async (title?: string) => {
|
|
||||||
setLoadingDicts(true);
|
|
||||||
try {
|
|
||||||
const res = await request('/dict/list', { params: { title } });
|
|
||||||
// 条件判断,确保res是数组再进行过滤
|
|
||||||
const dataList = Array.isArray(res) ? res : res?.data || [];
|
|
||||||
const filtered = dataList.filter((d: any) => !notAttributes.has(d?.name));
|
|
||||||
setDicts(filtered);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('获取字典列表失败:', error);
|
|
||||||
message.error('获取字典列表失败');
|
|
||||||
setDicts([]);
|
|
||||||
}
|
|
||||||
setLoadingDicts(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 组件挂载时初始化数据
|
|
||||||
useEffect(() => {
|
|
||||||
fetchDicts();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// 搜索触发过滤
|
|
||||||
const handleSearch = (value: string) => {
|
|
||||||
fetchDicts(value);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 打开添加字典项模态框
|
|
||||||
const handleAddDictItem = () => {
|
|
||||||
setIsEditDictItem(false);
|
|
||||||
setEditingDictItemData(null);
|
|
||||||
setIsDictItemModalVisible(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 打开编辑字典项模态框
|
|
||||||
const handleEditDictItem = (item: any) => {
|
|
||||||
setIsEditDictItem(true);
|
|
||||||
setEditingDictItemData(item);
|
|
||||||
setIsDictItemModalVisible(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 字典项表单提交(新增或编辑)
|
|
||||||
const handleDictItemFormSubmit = async (values: any) => {
|
|
||||||
try {
|
|
||||||
if (isEditDictItem && editingDictItemData) {
|
|
||||||
// 条件判断,存在编辑项则执行更新
|
|
||||||
await request(`/dict/item/${editingDictItemData.id}`, {
|
|
||||||
method: 'PUT',
|
|
||||||
data: values,
|
|
||||||
});
|
|
||||||
message.success('更新成功');
|
|
||||||
} else {
|
|
||||||
// 否则执行新增,绑定到当前选择的字典
|
|
||||||
await request('/dict/item', {
|
|
||||||
method: 'POST',
|
|
||||||
data: { ...values, dictId: selectedDict.id },
|
|
||||||
});
|
|
||||||
message.success('添加成功');
|
|
||||||
}
|
|
||||||
setIsDictItemModalVisible(false);
|
|
||||||
actionRef.current?.reload(); // 刷新 ProTable
|
|
||||||
} catch (error) {
|
|
||||||
message.error(isEditDictItem ? '更新失败' : '添加失败');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 删除字典项
|
|
||||||
const handleDeleteDictItem = async (itemId: number) => {
|
|
||||||
try {
|
|
||||||
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) {
|
|
||||||
try {
|
|
||||||
const list = await request('/dict/items', {
|
|
||||||
params: {
|
|
||||||
dictId: selectedDict.id,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
// 确保list是数组再进行some操作
|
|
||||||
const dataList = Array.isArray(list) ? list : list?.data || [];
|
|
||||||
const exists = dataList.some((it: any) => it.id === itemId);
|
|
||||||
if (exists) {
|
|
||||||
message.error('删除失败');
|
|
||||||
} else {
|
|
||||||
message.success('删除成功');
|
|
||||||
actionRef.current?.reload();
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('验证删除结果失败:', error);
|
|
||||||
message.success('删除成功');
|
|
||||||
actionRef.current?.reload();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
message.success('删除成功');
|
|
||||||
actionRef.current?.reload();
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
message.error('删除失败');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 左侧字典列表列定义(紧凑样式)
|
|
||||||
const dictColumns = [
|
|
||||||
{ title: '名称', dataIndex: 'name', key: 'name' },
|
|
||||||
{ title: '标题', dataIndex: 'title', key: 'title' },
|
|
||||||
];
|
|
||||||
|
|
||||||
// 右侧字典项列表列定义(紧凑样式)
|
|
||||||
const dictItemColumns: ProColumns<any>[] = [
|
|
||||||
{
|
|
||||||
title: '名称',
|
|
||||||
dataIndex: 'name',
|
|
||||||
key: 'name',
|
|
||||||
copyable: true,
|
|
||||||
sorter: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '标题',
|
|
||||||
dataIndex: 'title',
|
|
||||||
key: 'title',
|
|
||||||
copyable: true,
|
|
||||||
sorter: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '中文标题',
|
|
||||||
dataIndex: 'titleCN',
|
|
||||||
key: 'titleCN',
|
|
||||||
copyable: true,
|
|
||||||
sorter: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '简称',
|
|
||||||
dataIndex: 'shortName',
|
|
||||||
key: 'shortName',
|
|
||||||
copyable: true,
|
|
||||||
sorter: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '图片',
|
|
||||||
dataIndex: 'image',
|
|
||||||
key: 'image',
|
|
||||||
valueType: 'image',
|
|
||||||
width: 80,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '操作',
|
|
||||||
key: 'action',
|
|
||||||
valueType: 'option',
|
|
||||||
render: (_: any, record: any) => (
|
|
||||||
<Space size="small">
|
|
||||||
<Button
|
|
||||||
size="small"
|
|
||||||
type="link"
|
|
||||||
onClick={() => handleEditDictItem(record)}
|
|
||||||
>
|
|
||||||
编辑
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
size="small"
|
|
||||||
type="link"
|
|
||||||
danger
|
|
||||||
onClick={() => handleDeleteDictItem(record.id)}
|
|
||||||
>
|
|
||||||
删除
|
|
||||||
</Button>
|
|
||||||
</Space>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<PageContainer>
|
|
||||||
<Layout style={{ background: '#fff' }}>
|
|
||||||
<Sider
|
|
||||||
width={240}
|
|
||||||
style={{
|
|
||||||
background: '#fff',
|
|
||||||
padding: '8px',
|
|
||||||
borderRight: '1px solid #f0f0f0',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Space direction="vertical" style={{ width: '100%' }} size="small">
|
|
||||||
<Input.Search
|
|
||||||
placeholder="搜索字典"
|
|
||||||
onSearch={handleSearch}
|
|
||||||
onChange={(e) => setSearchText(e.target.value)}
|
|
||||||
enterButton
|
|
||||||
allowClear
|
|
||||||
size="small"
|
|
||||||
/>
|
|
||||||
</Space>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
marginTop: '8px',
|
|
||||||
overflowY: 'auto',
|
|
||||||
height: 'calc(100vh - 150px)',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Table
|
|
||||||
dataSource={dicts}
|
|
||||||
columns={dictColumns}
|
|
||||||
rowKey="id"
|
|
||||||
loading={loadingDicts}
|
|
||||||
size="small"
|
|
||||||
onRow={(record) => ({
|
|
||||||
onClick: () => {
|
|
||||||
// 条件判断,重复点击同一行则取消选择
|
|
||||||
if (selectedDict?.id === record.id) {
|
|
||||||
setSelectedDict(null);
|
|
||||||
} else {
|
|
||||||
setSelectedDict(record);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})}
|
|
||||||
rowClassName={(record) =>
|
|
||||||
selectedDict?.id === record.id ? 'ant-table-row-selected' : ''
|
|
||||||
}
|
|
||||||
pagination={false}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Sider>
|
|
||||||
<Content style={{ padding: '8px' }}>
|
|
||||||
<ProTable
|
|
||||||
columns={dictItemColumns}
|
|
||||||
actionRef={actionRef}
|
|
||||||
request={async (params) => {
|
|
||||||
// 当没有选择字典时,不发起请求
|
|
||||||
if (!selectedDict?.id) {
|
|
||||||
return {
|
|
||||||
data: [],
|
|
||||||
success: true,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
const { name, title } = params;
|
|
||||||
try {
|
|
||||||
const res = await request('/dict/items', {
|
|
||||||
params: {
|
|
||||||
dictId: selectedDict.id,
|
|
||||||
name,
|
|
||||||
title,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
// 确保返回的是数组
|
|
||||||
const data = Array.isArray(res) ? res : res?.data || [];
|
|
||||||
return {
|
|
||||||
data: data,
|
|
||||||
success: true,
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
console.error('获取字典项失败:', error);
|
|
||||||
return {
|
|
||||||
data: [],
|
|
||||||
success: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
rowKey="id"
|
|
||||||
search={{
|
|
||||||
layout: 'vertical',
|
|
||||||
}}
|
|
||||||
pagination={false}
|
|
||||||
options={{
|
|
||||||
reload: true,
|
|
||||||
density: false,
|
|
||||||
setting: {
|
|
||||||
draggable: true,
|
|
||||||
checkable: true,
|
|
||||||
checkedReset: false,
|
|
||||||
},
|
|
||||||
search: false,
|
|
||||||
fullScreen: false,
|
|
||||||
}}
|
|
||||||
size="small"
|
|
||||||
key={selectedDict?.id}
|
|
||||||
headerTitle={
|
|
||||||
<DictItemActions
|
|
||||||
selectedDict={selectedDict}
|
|
||||||
actionRef={actionRef}
|
|
||||||
showExport={true}
|
|
||||||
onImport={async (file: File, dictId: number) => {
|
|
||||||
// 创建 FormData 对象
|
|
||||||
const formData = new FormData();
|
|
||||||
// 添加文件到 FormData
|
|
||||||
formData.append('file', file);
|
|
||||||
// 添加字典 ID 到 FormData
|
|
||||||
formData.append('dictId', String(dictId));
|
|
||||||
// 调用导入字典项的 API
|
|
||||||
const response = await dictApi.dictcontrollerImportdictitems(
|
|
||||||
formData,
|
|
||||||
);
|
|
||||||
// 返回 JSON 响应
|
|
||||||
return await response.json();
|
|
||||||
}}
|
|
||||||
onExport={handleExportDictItems}
|
|
||||||
onAdd={handleAddDictItem}
|
|
||||||
onRefreshDicts={fetchDicts}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</Content>
|
|
||||||
</Layout>
|
|
||||||
|
|
||||||
{/* 字典项 Modal(添加或编辑) */}
|
|
||||||
<DictItemModal
|
|
||||||
visible={isDictItemModalVisible}
|
|
||||||
isEdit={isEditDictItem}
|
|
||||||
editingData={editingDictItemData}
|
|
||||||
selectedDict={selectedDict}
|
|
||||||
onCancel={() => {
|
|
||||||
setIsDictItemModalVisible(false);
|
|
||||||
setEditingDictItemData(null);
|
|
||||||
}}
|
|
||||||
onOk={handleDictItemFormSubmit}
|
|
||||||
/>
|
|
||||||
</PageContainer>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AttributePage;
|
|
||||||
|
|
@ -1,362 +1,210 @@
|
||||||
import {
|
import {
|
||||||
productcontrollerCreatecategory,
|
productcontrollerCreatecategory,
|
||||||
productcontrollerCreatecategoryattribute,
|
|
||||||
productcontrollerDeletecategory,
|
productcontrollerDeletecategory,
|
||||||
productcontrollerDeletecategoryattribute,
|
productcontrollerGetcategories,
|
||||||
productcontrollerGetcategoriesall,
|
|
||||||
productcontrollerGetcategoryattributes,
|
|
||||||
productcontrollerUpdatecategory,
|
productcontrollerUpdatecategory,
|
||||||
} from '@/servers/api/product';
|
} from '@/servers/api/product';
|
||||||
import { PlusOutlined } from '@ant-design/icons';
|
import { EditOutlined, PlusOutlined } from '@ant-design/icons';
|
||||||
import { PageContainer } from '@ant-design/pro-components';
|
|
||||||
import { request } from '@umijs/max';
|
|
||||||
import {
|
import {
|
||||||
Button,
|
ActionType,
|
||||||
Card,
|
DrawerForm,
|
||||||
Form,
|
PageContainer,
|
||||||
Input,
|
ProColumns,
|
||||||
Layout,
|
ProForm,
|
||||||
List,
|
ProFormText,
|
||||||
Modal,
|
ProTable,
|
||||||
Popconfirm,
|
} from '@ant-design/pro-components';
|
||||||
Select,
|
import { App, Button, Popconfirm } from 'antd';
|
||||||
message,
|
import { useRef } from 'react';
|
||||||
} from 'antd';
|
|
||||||
import React, { useEffect, useState } from 'react';
|
|
||||||
import { notAttributes } from '../Attribute/consts';
|
|
||||||
|
|
||||||
const { Sider, Content } = Layout;
|
const List: React.FC = () => {
|
||||||
|
const actionRef = useRef<ActionType>();
|
||||||
const CategoryPage: React.FC = () => {
|
const { message } = App.useApp();
|
||||||
const [categories, setCategories] = useState<any[]>([]);
|
const columns: ProColumns<API.Category>[] = [
|
||||||
const [loadingCategories, setLoadingCategories] = useState(false);
|
{
|
||||||
const [selectedCategory, setSelectedCategory] = useState<any>(null);
|
title: '名称',
|
||||||
const [categoryAttributes, setCategoryAttributes] = useState<any[]>([]);
|
dataIndex: 'name',
|
||||||
const [loadingAttributes, setLoadingAttributes] = useState(false);
|
tip: '名称是唯一的 key',
|
||||||
|
formItemProps: {
|
||||||
const [isCategoryModalVisible, setIsCategoryModalVisible] = useState(false);
|
rules: [
|
||||||
const [categoryForm] = Form.useForm();
|
{
|
||||||
const [editingCategory, setEditingCategory] = useState<any>(null);
|
required: true,
|
||||||
|
message: '名称为必填项',
|
||||||
const [isAttributeModalVisible, setIsAttributeModalVisible] = useState(false);
|
},
|
||||||
const [availableDicts, setAvailableDicts] = useState<any[]>([]);
|
],
|
||||||
const [selectedDictIds, setSelectedDictIds] = useState<number[]>([]);
|
},
|
||||||
|
},
|
||||||
const fetchCategories = async () => {
|
{
|
||||||
setLoadingCategories(true);
|
title: '标识',
|
||||||
|
dataIndex: 'unique_key',
|
||||||
|
hideInSearch: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '更新时间',
|
||||||
|
dataIndex: 'updatedAt',
|
||||||
|
valueType: 'dateTime',
|
||||||
|
hideInSearch: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '创建时间',
|
||||||
|
dataIndex: 'createdAt',
|
||||||
|
valueType: 'dateTime',
|
||||||
|
hideInSearch: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
dataIndex: 'option',
|
||||||
|
valueType: 'option',
|
||||||
|
render: (_, record) => (
|
||||||
|
<>
|
||||||
|
{/* <UpdateForm tableRef={actionRef} values={record} />
|
||||||
|
<Divider type="vertical" /> */}
|
||||||
|
<Popconfirm
|
||||||
|
title="删除"
|
||||||
|
description="确认删除?"
|
||||||
|
onConfirm={async () => {
|
||||||
try {
|
try {
|
||||||
const res = await productcontrollerGetcategoriesall();
|
const { success, message: errMsg } =
|
||||||
setCategories(res?.data || []);
|
await productcontrollerDeletecategory({ id: record.id });
|
||||||
} catch (error) {
|
if (!success) {
|
||||||
message.error('获取分类列表失败');
|
throw new Error(errMsg);
|
||||||
}
|
}
|
||||||
setLoadingCategories(false);
|
actionRef.current?.reload();
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetchCategories();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const fetchCategoryAttributes = async (categoryId: number) => {
|
|
||||||
setLoadingAttributes(true);
|
|
||||||
try {
|
|
||||||
const res = await productcontrollerGetcategoryattributes({
|
|
||||||
id: categoryId,
|
|
||||||
});
|
|
||||||
setCategoryAttributes(res?.data || []);
|
|
||||||
} 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) {
|
} catch (error: any) {
|
||||||
message.error(error.message || '操作失败');
|
message.error(error.message);
|
||||||
}
|
}
|
||||||
};
|
}}
|
||||||
|
>
|
||||||
const handleDeleteCategory = async (id: number) => {
|
<Button type="primary" danger>
|
||||||
try {
|
删除
|
||||||
await productcontrollerDeletecategory({ id });
|
</Button>
|
||||||
message.success('删除成功');
|
</Popconfirm>
|
||||||
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');
|
|
||||||
// Defensive check for response structure: handle both raw array and wrapped response
|
|
||||||
const list = Array.isArray(res) ? res : res?.data || [];
|
|
||||||
const filtered = list.filter((d: any) => !notAttributes.has(d.name));
|
|
||||||
// Filter out already added attributes
|
|
||||||
const existingDictIds = new Set(
|
|
||||||
categoryAttributes.map((ca: any) => ca.dictId),
|
|
||||||
);
|
|
||||||
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 {
|
|
||||||
// Loop through selected IDs and create attribute for each
|
|
||||||
await Promise.all(
|
|
||||||
selectedDictIds.map((dictId) =>
|
|
||||||
productcontrollerCreatecategoryattribute({
|
|
||||||
categoryId: selectedCategory.id,
|
|
||||||
dictId: dictId,
|
|
||||||
}),
|
|
||||||
),
|
),
|
||||||
);
|
},
|
||||||
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 (
|
return (
|
||||||
<PageContainer>
|
<PageContainer header={{ title: '分类列表' }}>
|
||||||
<Layout style={{ background: '#fff', height: 'calc(100vh - 200px)' }}>
|
<ProTable<API.Category>
|
||||||
<Sider
|
headerTitle="查询表格"
|
||||||
width={300}
|
actionRef={actionRef}
|
||||||
style={{
|
rowKey="id"
|
||||||
background: '#fff',
|
toolBarRender={() => [<CreateForm tableRef={actionRef} />]}
|
||||||
borderRight: '1px solid #f0f0f0',
|
request={async (params) => {
|
||||||
padding: '16px',
|
const { data, success } = await productcontrollerGetcategories(
|
||||||
|
params,
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
total: data?.total || 0,
|
||||||
|
data: data?.items || [],
|
||||||
|
success,
|
||||||
|
};
|
||||||
}}
|
}}
|
||||||
>
|
columns={columns}
|
||||||
<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}(${item.titleCN ?? '-'})`}
|
|
||||||
description={`${item.name} | ${item.shortName ?? '-'}`}
|
|
||||||
/>
|
/>
|
||||||
</List.Item>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</Sider>
|
|
||||||
<Content style={{ padding: '24px' }}>
|
|
||||||
{selectedCategory ? (
|
|
||||||
<Card
|
|
||||||
title={`分类:${selectedCategory.title} (${
|
|
||||||
selectedCategory.shortName ?? 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.title}
|
|
||||||
description={`Code: ${item.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="标题">
|
|
||||||
<Input />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item name="titleCN" label="中文名称">
|
|
||||||
<Input />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item name="shortName" label="短名称">
|
|
||||||
<Input />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item name="name" label="标识 (Code)">
|
|
||||||
<Input />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item name="sort" label="排序">
|
|
||||||
<Input type="number" />
|
|
||||||
</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>
|
</PageContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default CategoryPage;
|
const CreateForm: React.FC<{
|
||||||
|
tableRef: React.MutableRefObject<ActionType | undefined>;
|
||||||
|
}> = ({ tableRef }) => {
|
||||||
|
const { message } = App.useApp();
|
||||||
|
return (
|
||||||
|
<DrawerForm<API.CreateCategoryDTO>
|
||||||
|
title="新建"
|
||||||
|
trigger={
|
||||||
|
<Button type="primary">
|
||||||
|
<PlusOutlined />
|
||||||
|
新建
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
autoFocusFirstInput
|
||||||
|
drawerProps={{
|
||||||
|
destroyOnHidden: true,
|
||||||
|
}}
|
||||||
|
onFinish={async (values) => {
|
||||||
|
try {
|
||||||
|
const { success, message: errMsg } =
|
||||||
|
await productcontrollerCreatecategory(values);
|
||||||
|
if (!success) {
|
||||||
|
throw new Error(errMsg);
|
||||||
|
}
|
||||||
|
tableRef.current?.reload();
|
||||||
|
message.success('提交成功');
|
||||||
|
return true;
|
||||||
|
} catch (error: any) {
|
||||||
|
message.error(error.message);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ProFormText
|
||||||
|
name="name"
|
||||||
|
width="md"
|
||||||
|
label="分类名称"
|
||||||
|
placeholder="请输入名称"
|
||||||
|
rules={[{ required: true, message: '请输入名称' }]}
|
||||||
|
/>
|
||||||
|
<ProFormText
|
||||||
|
name="unique_key"
|
||||||
|
width="md"
|
||||||
|
label="Key"
|
||||||
|
placeholder="请输入Key"
|
||||||
|
rules={[{ required: true, message: '请输入Key' }]}
|
||||||
|
/>
|
||||||
|
</DrawerForm>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const UpdateForm: React.FC<{
|
||||||
|
tableRef: React.MutableRefObject<ActionType | undefined>;
|
||||||
|
values: API.Category;
|
||||||
|
}> = ({ tableRef, values: initialValues }) => {
|
||||||
|
const { message } = App.useApp();
|
||||||
|
return (
|
||||||
|
<DrawerForm<API.UpdateCategoryDTO>
|
||||||
|
title="编辑"
|
||||||
|
initialValues={initialValues}
|
||||||
|
trigger={
|
||||||
|
<Button type="primary">
|
||||||
|
<EditOutlined />
|
||||||
|
编辑
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
autoFocusFirstInput
|
||||||
|
drawerProps={{
|
||||||
|
destroyOnHidden: true,
|
||||||
|
}}
|
||||||
|
onFinish={async (values) => {
|
||||||
|
try {
|
||||||
|
const { success, message: errMsg } =
|
||||||
|
await productcontrollerUpdatecategory(
|
||||||
|
{ id: initialValues.id },
|
||||||
|
values,
|
||||||
|
);
|
||||||
|
if (!success) {
|
||||||
|
throw new Error(errMsg);
|
||||||
|
}
|
||||||
|
message.success('提交成功');
|
||||||
|
tableRef.current?.reload();
|
||||||
|
return true;
|
||||||
|
} catch (error: any) {
|
||||||
|
message.error(error.message);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ProForm.Group>
|
||||||
|
<ProFormText
|
||||||
|
name="name"
|
||||||
|
width="md"
|
||||||
|
label="分类名称"
|
||||||
|
placeholder="请输入名称"
|
||||||
|
rules={[{ required: true, message: '请输入名称' }]}
|
||||||
|
/>
|
||||||
|
</ProForm.Group>
|
||||||
|
</DrawerForm>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default List;
|
||||||
|
|
|
||||||
|
|
@ -1,908 +0,0 @@
|
||||||
import { productcontrollerGetcategoriesall } from '@/servers/api/product';
|
|
||||||
import { UploadOutlined } from '@ant-design/icons';
|
|
||||||
import {
|
|
||||||
PageContainer,
|
|
||||||
ProForm,
|
|
||||||
ProFormSelect,
|
|
||||||
} from '@ant-design/pro-components';
|
|
||||||
import { request } from '@umijs/max';
|
|
||||||
import { Button, Card, Checkbox, Col, Input, message, Row, Upload } from 'antd';
|
|
||||||
import React, { useEffect, useState } from 'react';
|
|
||||||
import * as XLSX from 'xlsx';
|
|
||||||
|
|
||||||
// 定义站点接口
|
|
||||||
interface Site {
|
|
||||||
id: number;
|
|
||||||
name: string;
|
|
||||||
skuPrefix?: string;
|
|
||||||
isDisabled?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 定义选项接口,用于下拉选择框的选项
|
|
||||||
interface Option {
|
|
||||||
name: string; // 显示名称
|
|
||||||
shortName: string; // 短名称,用于生成SKU
|
|
||||||
}
|
|
||||||
|
|
||||||
// 定义配置接口
|
|
||||||
interface SkuConfig {
|
|
||||||
brands: Option[];
|
|
||||||
categories: Option[];
|
|
||||||
flavors: Option[];
|
|
||||||
strengths: Option[];
|
|
||||||
humidities: Option[];
|
|
||||||
versions: Option[];
|
|
||||||
sizes: Option[];
|
|
||||||
quantities: Option[];
|
|
||||||
}
|
|
||||||
|
|
||||||
// 定义通用属性映射接口,用于存储属性名称和shortName的对应关系
|
|
||||||
interface AttributeMapping {
|
|
||||||
[attributeName: string]: string; // key: 属性名称, value: 属性shortName
|
|
||||||
}
|
|
||||||
|
|
||||||
// 定义所有属性映射的接口
|
|
||||||
interface AttributeMappings {
|
|
||||||
brands: AttributeMapping;
|
|
||||||
categories: AttributeMapping;
|
|
||||||
flavors: AttributeMapping;
|
|
||||||
strengths: AttributeMapping;
|
|
||||||
humidities: AttributeMapping;
|
|
||||||
versions: AttributeMapping;
|
|
||||||
sizes: AttributeMapping;
|
|
||||||
quantities: AttributeMapping;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description 产品CSV工具页面,用于批量生成SKU
|
|
||||||
*/
|
|
||||||
const CsvTool: React.FC = () => {
|
|
||||||
// 状态管理
|
|
||||||
const [form] = ProForm.useForm();
|
|
||||||
const [file, setFile] = useState<File | null>(null);
|
|
||||||
const [csvData, setCsvData] = useState<any[]>([]);
|
|
||||||
const [processedData, setProcessedData] = useState<any[]>([]);
|
|
||||||
const [isProcessing, setIsProcessing] = useState(false);
|
|
||||||
const [sites, setSites] = useState<Site[]>([]);
|
|
||||||
const [selectedSites, setSelectedSites] = useState<Site[]>([]); // 现在使用多选
|
|
||||||
const [generateBundleSkuForSingle, setGenerateBundleSkuForSingle] =
|
|
||||||
useState(true); // 是否为type为single的记录生成包含quantity的bundle SKU
|
|
||||||
const [config, setConfig] = useState<SkuConfig>({
|
|
||||||
brands: [],
|
|
||||||
categories: [],
|
|
||||||
flavors: [],
|
|
||||||
strengths: [],
|
|
||||||
humidities: [],
|
|
||||||
versions: [],
|
|
||||||
sizes: [],
|
|
||||||
quantities: [],
|
|
||||||
});
|
|
||||||
// 所有属性名称到shortName的映射
|
|
||||||
const [attributeMappings, setAttributeMappings] = useState<AttributeMappings>(
|
|
||||||
{
|
|
||||||
brands: {},
|
|
||||||
categories: {},
|
|
||||||
flavors: {},
|
|
||||||
strengths: {},
|
|
||||||
humidities: {},
|
|
||||||
versions: {},
|
|
||||||
sizes: {},
|
|
||||||
quantities: {},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
// 在组件加载时获取站点列表和字典数据
|
|
||||||
useEffect(() => {
|
|
||||||
const fetchAllData = async () => {
|
|
||||||
try {
|
|
||||||
message.loading({ content: '正在加载数据...', key: 'loading' });
|
|
||||||
|
|
||||||
// 1. 获取站点列表
|
|
||||||
const sitesResponse = await request('/site/all');
|
|
||||||
const siteList = sitesResponse?.data || sitesResponse || [];
|
|
||||||
setSites(siteList);
|
|
||||||
// 默认选择所有站点
|
|
||||||
setSelectedSites(siteList);
|
|
||||||
|
|
||||||
// 2. 获取字典数据
|
|
||||||
const dictListResponse = await request('/dict/list');
|
|
||||||
const dictList = dictListResponse?.data || dictListResponse || [];
|
|
||||||
|
|
||||||
// 3. 根据字典名称获取字典项,返回包含name和shortName的完整对象数组
|
|
||||||
const getDictItems = async (dictName: string) => {
|
|
||||||
try {
|
|
||||||
const dict = dictList.find((d: any) => d.name === dictName);
|
|
||||||
if (!dict) {
|
|
||||||
console.warn(`Dictionary ${dictName} not found`);
|
|
||||||
return { options: [], mapping: {} };
|
|
||||||
}
|
|
||||||
const itemsResponse = await request('/dict/items', {
|
|
||||||
params: { dictId: dict.id },
|
|
||||||
});
|
|
||||||
const items = itemsResponse?.data || itemsResponse || [];
|
|
||||||
|
|
||||||
// 创建完整的选项数组
|
|
||||||
const options = items.map((item: any) => ({
|
|
||||||
name: item.name,
|
|
||||||
shortName: item.shortName || item.name,
|
|
||||||
}));
|
|
||||||
|
|
||||||
// 创建name到shortName的映射
|
|
||||||
const mapping = items.reduce((acc: AttributeMapping, item: any) => {
|
|
||||||
acc[item.name] = item.shortName || item.name;
|
|
||||||
return acc;
|
|
||||||
}, {});
|
|
||||||
|
|
||||||
return { options, mapping };
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`Failed to fetch items for ${dictName}:`, error);
|
|
||||||
return { options: [], mapping: {} };
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 4. 获取所有字典项(品牌、口味、强度、湿度、版本、尺寸、数量)
|
|
||||||
const [
|
|
||||||
brandResult,
|
|
||||||
flavorResult,
|
|
||||||
strengthResult,
|
|
||||||
humidityResult,
|
|
||||||
versionResult,
|
|
||||||
sizeResult,
|
|
||||||
quantityResult,
|
|
||||||
] = await Promise.all([
|
|
||||||
getDictItems('brand'),
|
|
||||||
getDictItems('flavor'),
|
|
||||||
getDictItems('strength'),
|
|
||||||
getDictItems('humidity'),
|
|
||||||
getDictItems('version'),
|
|
||||||
getDictItems('size'),
|
|
||||||
getDictItems('quantity'),
|
|
||||||
]);
|
|
||||||
|
|
||||||
// 5. 获取商品分类列表
|
|
||||||
const categoriesResponse = await productcontrollerGetcategoriesall();
|
|
||||||
const categoryOptions =
|
|
||||||
categoriesResponse?.data?.map((category: any) => ({
|
|
||||||
name: category.name,
|
|
||||||
shortName: category.shortName || category.name,
|
|
||||||
})) || [];
|
|
||||||
|
|
||||||
// 商品分类的映射(如果分类有shortName的话)
|
|
||||||
const categoryMapping =
|
|
||||||
categoriesResponse?.data?.reduce(
|
|
||||||
(acc: AttributeMapping, category: any) => {
|
|
||||||
acc[category.name] = category.shortName || category.name;
|
|
||||||
return acc;
|
|
||||||
},
|
|
||||||
{},
|
|
||||||
) || {};
|
|
||||||
|
|
||||||
// 6. 设置所有属性映射
|
|
||||||
setAttributeMappings({
|
|
||||||
brands: brandResult.mapping,
|
|
||||||
categories: categoryMapping,
|
|
||||||
flavors: flavorResult.mapping,
|
|
||||||
strengths: strengthResult.mapping,
|
|
||||||
humidities: humidityResult.mapping,
|
|
||||||
versions: versionResult.mapping,
|
|
||||||
sizes: sizeResult.mapping,
|
|
||||||
quantities: quantityResult.mapping,
|
|
||||||
});
|
|
||||||
|
|
||||||
// 更新配置状态
|
|
||||||
const newConfig = {
|
|
||||||
brands: brandResult.options,
|
|
||||||
categories: categoryOptions,
|
|
||||||
flavors: flavorResult.options,
|
|
||||||
strengths: strengthResult.options,
|
|
||||||
humidities: humidityResult.options,
|
|
||||||
versions: versionResult.options,
|
|
||||||
sizes: sizeResult.options,
|
|
||||||
quantities: quantityResult.options,
|
|
||||||
};
|
|
||||||
setConfig(newConfig);
|
|
||||||
// 设置表单值时只需要name数组
|
|
||||||
form.setFieldsValue({
|
|
||||||
brands: brandResult.options.map((opt) => opt.name),
|
|
||||||
categories: categoryOptions.map((opt) => opt.name),
|
|
||||||
flavors: flavorResult.options.map((opt) => opt.name),
|
|
||||||
strengths: strengthResult.options.map((opt) => opt.name),
|
|
||||||
humidities: humidityResult.options.map((opt) => opt.name),
|
|
||||||
versions: versionResult.options.map((opt) => opt.name),
|
|
||||||
sizes: sizeResult.options.map((opt) => opt.name),
|
|
||||||
quantities: quantityResult.options.map((opt) => opt.name),
|
|
||||||
generateBundleSkuForSingle: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
message.success({ content: '数据加载成功', key: 'loading' });
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to fetch data:', error);
|
|
||||||
message.error({
|
|
||||||
content: '数据加载失败,请刷新页面重试',
|
|
||||||
key: 'loading',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
fetchAllData();
|
|
||||||
}, [form]);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description 处理文件上传
|
|
||||||
*/
|
|
||||||
const handleFileUpload = (uploadedFile: File) => {
|
|
||||||
// 检查文件类型
|
|
||||||
if (!uploadedFile.name.match(/\.(csv|xlsx|xls)$/)) {
|
|
||||||
message.error('请上传 CSV 或 Excel 格式的文件!');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
setFile(uploadedFile);
|
|
||||||
|
|
||||||
const reader = new FileReader();
|
|
||||||
|
|
||||||
// 检查是否为CSV文件
|
|
||||||
const isCsvFile = uploadedFile.name.match(/\.csv$/i);
|
|
||||||
|
|
||||||
if (isCsvFile) {
|
|
||||||
// 对于CSV文件,使用readAsText并指定UTF-8编码以正确处理中文
|
|
||||||
reader.onload = (e) => {
|
|
||||||
try {
|
|
||||||
const textData = e.target?.result as string;
|
|
||||||
// 使用XLSX.read处理CSV文本数据,指定type为'csv'并设置编码
|
|
||||||
const workbook = XLSX.read(textData, {
|
|
||||||
type: 'string',
|
|
||||||
codepage: 65001, // UTF-8 encoding
|
|
||||||
cellText: true,
|
|
||||||
cellDates: true,
|
|
||||||
});
|
|
||||||
const sheetName = workbook.SheetNames[0];
|
|
||||||
const worksheet = workbook.Sheets[sheetName];
|
|
||||||
const jsonData = XLSX.utils.sheet_to_json(worksheet, { header: 1 });
|
|
||||||
|
|
||||||
if (jsonData.length < 2) {
|
|
||||||
message.error('文件为空或缺少表头!');
|
|
||||||
setCsvData([]);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 将数组转换为对象数组
|
|
||||||
const headers = jsonData[0] as string[];
|
|
||||||
const rows = jsonData.slice(1).map((rowArray: any) => {
|
|
||||||
const rowData: { [key: string]: any } = {};
|
|
||||||
headers.forEach((header, index) => {
|
|
||||||
rowData[header] = rowArray[index];
|
|
||||||
});
|
|
||||||
return rowData;
|
|
||||||
});
|
|
||||||
|
|
||||||
message.success(`成功解析 ${rows.length} 条数据.`);
|
|
||||||
setCsvData(rows);
|
|
||||||
setProcessedData([]); // 清空旧的处理结果
|
|
||||||
} catch (error) {
|
|
||||||
message.error('CSV文件解析失败,请检查文件格式和编码!');
|
|
||||||
console.error('CSV Parse Error:', error);
|
|
||||||
setCsvData([]);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
reader.readAsText(uploadedFile, 'UTF-8');
|
|
||||||
} else {
|
|
||||||
// 对于Excel文件,继续使用readAsArrayBuffer
|
|
||||||
reader.onload = (e) => {
|
|
||||||
try {
|
|
||||||
const data = e.target?.result;
|
|
||||||
// 如果是ArrayBuffer,使用type: 'array'来处理
|
|
||||||
const workbook = XLSX.read(data, { type: 'array' });
|
|
||||||
const sheetName = workbook.SheetNames[0];
|
|
||||||
const worksheet = workbook.Sheets[sheetName];
|
|
||||||
const jsonData = XLSX.utils.sheet_to_json(worksheet, { header: 1 });
|
|
||||||
|
|
||||||
if (jsonData.length < 2) {
|
|
||||||
message.error('文件为空或缺少表头!');
|
|
||||||
setCsvData([]);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 将数组转换为对象数组
|
|
||||||
const headers = jsonData[0] as string[];
|
|
||||||
const rows = jsonData.slice(1).map((rowArray: any) => {
|
|
||||||
const rowData: { [key: string]: any } = {};
|
|
||||||
headers.forEach((header, index) => {
|
|
||||||
rowData[header] = rowArray[index];
|
|
||||||
});
|
|
||||||
return rowData;
|
|
||||||
});
|
|
||||||
|
|
||||||
message.success(`成功解析 ${rows.length} 条数据.`);
|
|
||||||
setCsvData(rows);
|
|
||||||
setProcessedData([]); // 清空旧的处理结果
|
|
||||||
} catch (error) {
|
|
||||||
message.error('Excel文件解析失败,请检查文件格式!');
|
|
||||||
console.error('Excel Parse Error:', error);
|
|
||||||
setCsvData([]);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
reader.readAsArrayBuffer(uploadedFile);
|
|
||||||
}
|
|
||||||
|
|
||||||
reader.onerror = (error) => {
|
|
||||||
message.error('文件读取失败!');
|
|
||||||
console.error('File Read Error:', error);
|
|
||||||
};
|
|
||||||
|
|
||||||
return false; // 阻止antd Upload组件的默认上传行为
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description 将数据转换回CSV并触发下载
|
|
||||||
*/
|
|
||||||
const downloadData = (data: any[]) => {
|
|
||||||
if (data.length === 0) return;
|
|
||||||
|
|
||||||
const workbook = XLSX.utils.book_new();
|
|
||||||
const worksheet = XLSX.utils.json_to_sheet(data);
|
|
||||||
XLSX.utils.book_append_sheet(workbook, worksheet, 'Products with SKU');
|
|
||||||
|
|
||||||
const fileName = `products_with_sku_${Date.now()}.xlsx`;
|
|
||||||
XLSX.writeFile(workbook, fileName);
|
|
||||||
|
|
||||||
message.success('下载任务已开始!');
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description 根据配置生成SKU(不包含站点前缀)
|
|
||||||
* @param {string} brand - 品牌
|
|
||||||
* @param {string} category - 分类
|
|
||||||
* @param {string} flavor - 口味
|
|
||||||
* @param {string} strength - 强度
|
|
||||||
* @param {string} humidity - 湿度
|
|
||||||
* @param {string} version - 版本
|
|
||||||
* @param {string} type - 产品类型
|
|
||||||
* @returns {string} 生成的SKU
|
|
||||||
*/
|
|
||||||
const generateSku = (
|
|
||||||
brand: string,
|
|
||||||
version: string,
|
|
||||||
category: string,
|
|
||||||
flavor: string,
|
|
||||||
strength: string,
|
|
||||||
humidity: string,
|
|
||||||
size: string,
|
|
||||||
quantity?: any,
|
|
||||||
type?: string,
|
|
||||||
): string => {
|
|
||||||
// 构建SKU组件,不包含站点前缀
|
|
||||||
const skuComponents: string[] = [];
|
|
||||||
|
|
||||||
// 按顺序添加SKU组件,所有属性都使用shortName
|
|
||||||
if (brand) {
|
|
||||||
// 使用品牌的shortName,如果没有则使用品牌名称
|
|
||||||
const brandShortName = attributeMappings.brands[brand] || brand;
|
|
||||||
skuComponents.push(brandShortName);
|
|
||||||
}
|
|
||||||
if (version) {
|
|
||||||
// 使用版本的shortName,如果没有则使用版本名称
|
|
||||||
const versionShortName = attributeMappings.versions[version] || version;
|
|
||||||
skuComponents.push(versionShortName);
|
|
||||||
}
|
|
||||||
if (category) {
|
|
||||||
// 使用分类的shortName,如果没有则使用分类名称
|
|
||||||
const categoryShortName =
|
|
||||||
attributeMappings.categories[category] || category;
|
|
||||||
skuComponents.push(categoryShortName);
|
|
||||||
}
|
|
||||||
if (flavor) {
|
|
||||||
// 使用口味的shortName,如果没有则使用口味名称
|
|
||||||
const flavorShortName = attributeMappings.flavors[flavor] || flavor;
|
|
||||||
skuComponents.push(flavorShortName);
|
|
||||||
}
|
|
||||||
if (strength) {
|
|
||||||
// 使用强度的shortName,如果没有则使用强度名称
|
|
||||||
const strengthShortName =
|
|
||||||
attributeMappings.strengths[strength] || strength;
|
|
||||||
skuComponents.push(strengthShortName);
|
|
||||||
}
|
|
||||||
if (humidity) {
|
|
||||||
// 使用湿度的shortName,如果没有则使用湿度名称
|
|
||||||
const humidityShortName =
|
|
||||||
attributeMappings.humidities[humidity] || humidity;
|
|
||||||
skuComponents.push(humidityShortName);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (size) {
|
|
||||||
// 使用尺寸的shortName,如果没有则使用尺寸名称
|
|
||||||
const sizeShortName = attributeMappings.sizes[size] || size;
|
|
||||||
skuComponents.push(sizeShortName);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果type为single且启用了生成bundle SKU,则添加quantity
|
|
||||||
if (quantity) {
|
|
||||||
// 使用quantity的shortName,如果没有则使用quantity但匹配 4 个零
|
|
||||||
const quantityShortName =
|
|
||||||
attributeMappings.quantities[quantity] ||
|
|
||||||
Number(quantity).toString().padStart(4, '0');
|
|
||||||
skuComponents.push(quantityShortName);
|
|
||||||
}
|
|
||||||
// 合并所有组件,使用短横线分隔
|
|
||||||
return skuComponents.join('-').toUpperCase();
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description 根据配置生成产品名称(使用属性的完整名称,空格分隔)
|
|
||||||
* @param {string} brand - 品牌
|
|
||||||
* @param {string} version - 版本
|
|
||||||
* @param {string} category - 分类
|
|
||||||
* @param {string} flavor - 口味
|
|
||||||
* @param {string} strength - 强度
|
|
||||||
* @param {string} humidity - 湿度
|
|
||||||
* @param {string} size - 型号
|
|
||||||
* @param {any} quantity - 数量
|
|
||||||
* @param {string} type - 产品类型
|
|
||||||
* @returns {string} 生成的产品名称
|
|
||||||
*/
|
|
||||||
const generateName = (
|
|
||||||
brand: string,
|
|
||||||
version: string,
|
|
||||||
category: string,
|
|
||||||
flavor: string,
|
|
||||||
strength: string,
|
|
||||||
humidity: string,
|
|
||||||
size: string,
|
|
||||||
quantity?: any,
|
|
||||||
type?: string,
|
|
||||||
): string => {
|
|
||||||
// 构建产品名称组件数组
|
|
||||||
const nameComponents: string[] = [];
|
|
||||||
|
|
||||||
// 按顺序添加组件:品牌 -> 版本 -> 品类 -> 风味 -> 毫克数(强度) -> 湿度 -> 型号 -> 数量
|
|
||||||
if (brand) nameComponents.push(brand);
|
|
||||||
if (version) nameComponents.push(version);
|
|
||||||
if (category) nameComponents.push(category);
|
|
||||||
if (flavor) nameComponents.push(flavor);
|
|
||||||
if (strength) nameComponents.push(strength);
|
|
||||||
if (humidity) nameComponents.push(humidity);
|
|
||||||
if (size) nameComponents.push(size);
|
|
||||||
|
|
||||||
// 如果有数量且类型为bundle或者生成bundle的single产品,则添加数量
|
|
||||||
if (type === 'bundle' && quantity) {
|
|
||||||
nameComponents.push(String(quantity));
|
|
||||||
}
|
|
||||||
|
|
||||||
// 使用空格连接所有组件
|
|
||||||
return nameComponents.join(' ');
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description 为所有站点生成带前缀的siteSkus
|
|
||||||
* @param {string} baseSku - 基础SKU(不包含站点前缀)
|
|
||||||
* @returns {string} 所有站点的siteSkus,以分号分隔
|
|
||||||
*/
|
|
||||||
const generateSiteSkus = (baseSku: string): string => {
|
|
||||||
// 如果没有站点或基础SKU为空,返回空字符串
|
|
||||||
if (selectedSites.length === 0 || !baseSku) return '';
|
|
||||||
|
|
||||||
// 为每个站点生成siteSku
|
|
||||||
const siteSkus = selectedSites.map((site) => {
|
|
||||||
// 如果站点有shortName,则添加前缀,否则使用基础SKU
|
|
||||||
if (site.skuPrefix) {
|
|
||||||
return `${site.skuPrefix}-${baseSku}`;
|
|
||||||
}
|
|
||||||
return baseSku;
|
|
||||||
});
|
|
||||||
|
|
||||||
// 使用分号分隔所有站点的siteSkus
|
|
||||||
return [baseSku, ...siteSkus].join(';').toUpperCase();
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description 核心逻辑:根据配置处理CSV数据并生成SKU
|
|
||||||
*/
|
|
||||||
const handleProcessData = async () => {
|
|
||||||
if (csvData.length === 0) {
|
|
||||||
message.warning('请先上传并成功解析一个CSV文件.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (selectedSites.length === 0) {
|
|
||||||
message.warning('没有可用的站点.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsProcessing(true);
|
|
||||||
message.loading({ content: '正在生成SKU...', key: 'processing' });
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 获取表单中的最新配置
|
|
||||||
await form.validateFields();
|
|
||||||
|
|
||||||
// 处理每条数据,生成SKU和siteSkus
|
|
||||||
const dataWithSku = csvData.map((row) => {
|
|
||||||
const brand = row.attribute_brand || '';
|
|
||||||
const category = row.category || '';
|
|
||||||
const flavor = row.attribute_flavor || '';
|
|
||||||
const strength = row.attribute_strength || '';
|
|
||||||
const humidity = row.attribute_humidity || '';
|
|
||||||
const version = row.attribute_version || '';
|
|
||||||
const size = row.attribute_size || row.size || '';
|
|
||||||
// 将quantity保存到attribute_quantity字段
|
|
||||||
const quantity = row.attribute_quantity || row.quantity;
|
|
||||||
// 获取产品类型
|
|
||||||
const type = row.type || '';
|
|
||||||
|
|
||||||
// 生成基础SKU(不包含站点前缀)
|
|
||||||
const baseSku = generateSku(
|
|
||||||
brand,
|
|
||||||
version,
|
|
||||||
category,
|
|
||||||
flavor,
|
|
||||||
strength,
|
|
||||||
humidity,
|
|
||||||
size,
|
|
||||||
quantity,
|
|
||||||
type,
|
|
||||||
);
|
|
||||||
const name = generateName(
|
|
||||||
brand,
|
|
||||||
version,
|
|
||||||
category,
|
|
||||||
flavor,
|
|
||||||
strength,
|
|
||||||
humidity,
|
|
||||||
size,
|
|
||||||
quantity,
|
|
||||||
type,
|
|
||||||
);
|
|
||||||
// 为所有站点生成带前缀的siteSkus
|
|
||||||
const siteSkus = generateSiteSkus(baseSku);
|
|
||||||
|
|
||||||
// 返回包含新SKU和siteSkus的行数据,将SKU直接保存到sku栏
|
|
||||||
return {
|
|
||||||
...row,
|
|
||||||
sku: baseSku, // 直接生成在sku栏
|
|
||||||
generatedName: name,
|
|
||||||
// name: name, // 生成的产品名称
|
|
||||||
siteSkus,
|
|
||||||
attribute_quantity: quantity, // 确保quantity保存到attribute_quantity
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
// Determine which data to use for processing and download
|
|
||||||
let finalData = dataWithSku;
|
|
||||||
console.log('generateBundleSkuForSingle', generateBundleSkuForSingle);
|
|
||||||
// If generateBundleSkuForSingle is enabled, generate bundle products for single products
|
|
||||||
if (generateBundleSkuForSingle) {
|
|
||||||
// Filter out single records
|
|
||||||
const singleRecords = dataWithSku.filter(
|
|
||||||
(row) => row.type === 'single',
|
|
||||||
);
|
|
||||||
|
|
||||||
// Get quantity values from the config (same source as other attributes like brand)
|
|
||||||
const quantityValues = config.quantities.map(
|
|
||||||
(quantity) => quantity.name,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Generate bundle products for each single record and quantity
|
|
||||||
const generatedBundleRecords = singleRecords.flatMap((singleRecord) => {
|
|
||||||
return quantityValues.map((quantity) => {
|
|
||||||
// Extract all necessary attributes from the single record
|
|
||||||
const brand = singleRecord.attribute_brand || '';
|
|
||||||
const version = singleRecord.attribute_version || '';
|
|
||||||
const category = singleRecord.category || '';
|
|
||||||
const flavor = singleRecord.attribute_flavor || '';
|
|
||||||
const strength = singleRecord.attribute_strength || '';
|
|
||||||
const humidity = singleRecord.attribute_humidity || '';
|
|
||||||
const size = singleRecord.attribute_size || singleRecord.size || '';
|
|
||||||
// Generate bundle SKU with the quantity
|
|
||||||
const bundleSku = generateSku(
|
|
||||||
brand,
|
|
||||||
version,
|
|
||||||
category,
|
|
||||||
flavor,
|
|
||||||
strength,
|
|
||||||
humidity,
|
|
||||||
size,
|
|
||||||
quantity,
|
|
||||||
'bundle',
|
|
||||||
);
|
|
||||||
|
|
||||||
// Generate bundle name with the quantity
|
|
||||||
const bundleName = generateName(
|
|
||||||
brand,
|
|
||||||
version,
|
|
||||||
category,
|
|
||||||
flavor,
|
|
||||||
strength,
|
|
||||||
humidity,
|
|
||||||
size,
|
|
||||||
quantity,
|
|
||||||
'bundle',
|
|
||||||
);
|
|
||||||
|
|
||||||
// Generate siteSkus for the bundle
|
|
||||||
const bundleSiteSkus = generateSiteSkus(bundleSku);
|
|
||||||
|
|
||||||
// Create the bundle record
|
|
||||||
return {
|
|
||||||
...singleRecord,
|
|
||||||
type: 'bundle', // Change type to bundle
|
|
||||||
sku: bundleSku, // Use the new bundle SKU
|
|
||||||
name: bundleName, // Use the new bundle name
|
|
||||||
siteSkus: bundleSiteSkus,
|
|
||||||
attribute_quantity: quantity, // Set the attribute_quantity
|
|
||||||
component_1_sku: singleRecord.sku, // Set component_1_sku to the single product's sku
|
|
||||||
component_1_quantity: Number(quantity), // Set component_1_quantity to the same as attribute_quantity
|
|
||||||
};
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Combine original dataWithSku with generated bundle records
|
|
||||||
finalData = [...dataWithSku, ...generatedBundleRecords];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set the processed data
|
|
||||||
setProcessedData(finalData);
|
|
||||||
|
|
||||||
message.success({
|
|
||||||
content: 'SKU生成成功!正在自动下载...',
|
|
||||||
key: 'processing',
|
|
||||||
});
|
|
||||||
|
|
||||||
// 自动下载 the final data (with or without generated bundle products)
|
|
||||||
downloadData(finalData);
|
|
||||||
} catch (error) {
|
|
||||||
message.error({
|
|
||||||
content: '处理失败,请检查配置或文件.',
|
|
||||||
key: 'processing',
|
|
||||||
});
|
|
||||||
console.error('Processing Error:', error);
|
|
||||||
} finally {
|
|
||||||
setIsProcessing(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<PageContainer title="产品SKU批量生成工具">
|
|
||||||
<Row gutter={[16, 16]}>
|
|
||||||
{/* 左侧:配置表单 */}
|
|
||||||
<Col xs={24} md={10}>
|
|
||||||
<Card title="1. 配置SKU生成规则">
|
|
||||||
<ProForm
|
|
||||||
form={form}
|
|
||||||
initialValues={config}
|
|
||||||
onFinish={handleProcessData}
|
|
||||||
submitter={false}
|
|
||||||
>
|
|
||||||
<ProFormSelect
|
|
||||||
name="brands"
|
|
||||||
label="品牌列表"
|
|
||||||
mode="tags"
|
|
||||||
placeholder="请输入品牌,按回车确认"
|
|
||||||
rules={[{ required: true, message: '至少需要一个品牌' }]}
|
|
||||||
tooltip="品牌名称会作为SKU的第一个组成部分"
|
|
||||||
options={config.brands.map((opt) => ({
|
|
||||||
label: `${opt.name} (${opt.shortName})`,
|
|
||||||
value: opt.name,
|
|
||||||
}))}
|
|
||||||
/>
|
|
||||||
<ProFormSelect
|
|
||||||
name="categories"
|
|
||||||
label="商品分类"
|
|
||||||
mode="tags"
|
|
||||||
placeholder="请输入分类,按回车确认"
|
|
||||||
rules={[{ required: true, message: '至少需要一个分类' }]}
|
|
||||||
tooltip="分类名称会作为SKU的第二个组成部分"
|
|
||||||
options={config.categories.map((opt) => ({
|
|
||||||
label: `${opt.name} (${opt.shortName})`,
|
|
||||||
value: opt.name,
|
|
||||||
}))}
|
|
||||||
/>
|
|
||||||
<ProFormSelect
|
|
||||||
name="flavors"
|
|
||||||
label="口味列表"
|
|
||||||
mode="tags"
|
|
||||||
placeholder="请输入口味,按回车确认"
|
|
||||||
rules={[{ required: true, message: '至少需要一个口味' }]}
|
|
||||||
tooltip="口味名称会作为SKU的第三个组成部分"
|
|
||||||
options={config.flavors.map((opt) => ({
|
|
||||||
label: `${opt.name} (${opt.shortName})`,
|
|
||||||
value: opt.name,
|
|
||||||
}))}
|
|
||||||
/>
|
|
||||||
<ProFormSelect
|
|
||||||
name="strengths"
|
|
||||||
label="强度列表"
|
|
||||||
mode="tags"
|
|
||||||
placeholder="请输入强度,按回车确认"
|
|
||||||
tooltip="强度信息会作为SKU的第四个组成部分"
|
|
||||||
options={config.strengths.map((opt) => ({
|
|
||||||
label: `${opt.name} (${opt.shortName})`,
|
|
||||||
value: opt.name,
|
|
||||||
}))}
|
|
||||||
/>
|
|
||||||
<ProFormSelect
|
|
||||||
name="humidities"
|
|
||||||
label="湿度列表"
|
|
||||||
mode="tags"
|
|
||||||
placeholder="请输入湿度,按回车确认"
|
|
||||||
tooltip="湿度信息会作为SKU的第五个组成部分"
|
|
||||||
options={config.humidities.map((opt) => ({
|
|
||||||
label: `${opt.name} (${opt.shortName})`,
|
|
||||||
value: opt.name,
|
|
||||||
}))}
|
|
||||||
/>
|
|
||||||
<ProFormSelect
|
|
||||||
name="versions"
|
|
||||||
label="版本列表"
|
|
||||||
mode="tags"
|
|
||||||
placeholder="请输入版本,按回车确认"
|
|
||||||
tooltip="版本信息会作为SKU的第六个组成部分"
|
|
||||||
options={config.versions.map((opt) => ({
|
|
||||||
label: `${opt.name} (${opt.shortName})`,
|
|
||||||
value: opt.name,
|
|
||||||
}))}
|
|
||||||
/>
|
|
||||||
<ProFormSelect
|
|
||||||
name="sizes"
|
|
||||||
label="尺寸列表"
|
|
||||||
mode="tags"
|
|
||||||
placeholder="请输入尺寸,按回车确认"
|
|
||||||
tooltip="尺寸信息会作为SKU的第七个组成部分"
|
|
||||||
options={config.sizes.map((opt) => ({
|
|
||||||
label: `${opt.name} (${opt.shortName})`,
|
|
||||||
value: opt.name,
|
|
||||||
}))}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ProFormSelect
|
|
||||||
name="quantities"
|
|
||||||
label="数量列表"
|
|
||||||
mode="tags"
|
|
||||||
placeholder="请输入数量,按回车确认"
|
|
||||||
tooltip="数量信息会作为bundle SKU的组成部分"
|
|
||||||
options={config.quantities.map((opt) => ({
|
|
||||||
label: `${opt.name} (${opt.shortName})`,
|
|
||||||
value: opt.name,
|
|
||||||
}))}
|
|
||||||
fieldProps={{ allowClear: true }}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ProForm.Item
|
|
||||||
name="generateBundleSkuForSingle"
|
|
||||||
label="为type=single生成bundle产品数据行"
|
|
||||||
tooltip="为类型为single的记录生成包含quantity的bundle SKU"
|
|
||||||
valuePropName="checked"
|
|
||||||
initialValue={true}
|
|
||||||
>
|
|
||||||
<Checkbox
|
|
||||||
onChange={(e) =>
|
|
||||||
setGenerateBundleSkuForSingle(e.target.checked)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
启用为single类型生成bundle SKU
|
|
||||||
</Checkbox>
|
|
||||||
</ProForm.Item>
|
|
||||||
</ProForm>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* 显示所有站点及其shortname */}
|
|
||||||
<Card title="3. 所有站点信息" style={{ marginTop: '16px' }}>
|
|
||||||
<div style={{ maxHeight: '200px', overflowY: 'auto' }}>
|
|
||||||
{sites.length > 0 ? (
|
|
||||||
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
|
|
||||||
<thead>
|
|
||||||
<tr style={{ backgroundColor: '#fafafa' }}>
|
|
||||||
<th
|
|
||||||
style={{
|
|
||||||
padding: '8px',
|
|
||||||
textAlign: 'left',
|
|
||||||
borderBottom: '1px solid #e8e8e8',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
站点名称
|
|
||||||
</th>
|
|
||||||
<th
|
|
||||||
style={{
|
|
||||||
padding: '8px',
|
|
||||||
textAlign: 'left',
|
|
||||||
borderBottom: '1px solid #e8e8e8',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
ShortName
|
|
||||||
</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{sites.map((site) => (
|
|
||||||
<tr key={site.id}>
|
|
||||||
<td
|
|
||||||
style={{
|
|
||||||
padding: '8px',
|
|
||||||
borderBottom: '1px solid #e8e8e8',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{site.name}
|
|
||||||
</td>
|
|
||||||
<td
|
|
||||||
style={{
|
|
||||||
padding: '8px',
|
|
||||||
borderBottom: '1px solid #e8e8e8',
|
|
||||||
fontWeight: 'bold',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{site.skuPrefix}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
) : (
|
|
||||||
<p style={{ textAlign: 'center', color: '#999' }}>
|
|
||||||
暂无站点信息
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div style={{ marginTop: '10px', fontSize: '12px', color: '#666' }}>
|
|
||||||
<p>
|
|
||||||
说明:所有站点的shortName将作为前缀添加到生成的SKU中,以分号分隔。
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
</Col>
|
|
||||||
|
|
||||||
{/* 右侧:文件上传与操作 */}
|
|
||||||
<Col xs={24} md={14}>
|
|
||||||
<Card title="2. 上传文件并操作">
|
|
||||||
<Upload
|
|
||||||
beforeUpload={handleFileUpload}
|
|
||||||
maxCount={1}
|
|
||||||
showUploadList={!!file}
|
|
||||||
onRemove={() => {
|
|
||||||
setFile(null);
|
|
||||||
setCsvData([]);
|
|
||||||
setProcessedData([]);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button icon={<UploadOutlined />}>选择 CSV 文件</Button>
|
|
||||||
</Upload>
|
|
||||||
<div style={{ marginTop: 16 }}>
|
|
||||||
<label style={{ display: 'block', marginBottom: 8 }}>
|
|
||||||
已上传文件
|
|
||||||
</label>
|
|
||||||
<Input value={file ? file.name : '暂未选择文件'} readOnly />
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
type="primary"
|
|
||||||
onClick={handleProcessData}
|
|
||||||
disabled={
|
|
||||||
csvData.length === 0 ||
|
|
||||||
isProcessing ||
|
|
||||||
selectedSites.length === 0
|
|
||||||
}
|
|
||||||
loading={isProcessing}
|
|
||||||
style={{ marginTop: '20px' }}
|
|
||||||
>
|
|
||||||
生成并下载SKU
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
{/* 显示处理结果摘要 */}
|
|
||||||
{processedData.length > 0 && (
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
marginTop: '20px',
|
|
||||||
padding: '10px',
|
|
||||||
backgroundColor: '#f0f9eb',
|
|
||||||
borderRadius: '4px',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<p style={{ margin: 0, color: '#52c41a' }}>
|
|
||||||
已成功为 {processedData.length} 条产品记录生成SKU!
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Card>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
</PageContainer>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default CsvTool;
|
|
||||||
|
|
@ -0,0 +1,208 @@
|
||||||
|
import {
|
||||||
|
productcontrollerCreateflavors,
|
||||||
|
productcontrollerDeleteflavors,
|
||||||
|
productcontrollerGetflavors,
|
||||||
|
productcontrollerUpdateflavors,
|
||||||
|
} from '@/servers/api/product';
|
||||||
|
import { EditOutlined, PlusOutlined } from '@ant-design/icons';
|
||||||
|
import {
|
||||||
|
ActionType,
|
||||||
|
DrawerForm,
|
||||||
|
PageContainer,
|
||||||
|
ProColumns,
|
||||||
|
ProForm,
|
||||||
|
ProFormText,
|
||||||
|
ProTable,
|
||||||
|
} from '@ant-design/pro-components';
|
||||||
|
import { App, Button, Popconfirm } from 'antd';
|
||||||
|
import { useRef } from 'react';
|
||||||
|
|
||||||
|
const List: React.FC = () => {
|
||||||
|
const actionRef = useRef<ActionType>();
|
||||||
|
const { message } = App.useApp();
|
||||||
|
const columns: ProColumns<API.Category>[] = [
|
||||||
|
{
|
||||||
|
title: '名称',
|
||||||
|
dataIndex: 'name',
|
||||||
|
tip: '名称是唯一的 key',
|
||||||
|
formItemProps: {
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: '名称为必填项',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '标识',
|
||||||
|
dataIndex: 'unique_key',
|
||||||
|
hideInSearch: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '更新时间',
|
||||||
|
dataIndex: 'updatedAt',
|
||||||
|
valueType: 'dateTime',
|
||||||
|
hideInSearch: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '创建时间',
|
||||||
|
dataIndex: 'createdAt',
|
||||||
|
valueType: 'dateTime',
|
||||||
|
hideInSearch: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
dataIndex: 'option',
|
||||||
|
valueType: 'option',
|
||||||
|
render: (_, record) => (
|
||||||
|
<>
|
||||||
|
{/* <UpdateForm tableRef={actionRef} values={record} />
|
||||||
|
<Divider type="vertical" /> */}
|
||||||
|
<Popconfirm
|
||||||
|
title="删除"
|
||||||
|
description="确认删除?"
|
||||||
|
onConfirm={async () => {
|
||||||
|
try {
|
||||||
|
const { success, message: errMsg } =
|
||||||
|
await productcontrollerDeleteflavors({ id: record.id });
|
||||||
|
if (!success) {
|
||||||
|
throw new Error(errMsg);
|
||||||
|
}
|
||||||
|
actionRef.current?.reload();
|
||||||
|
} catch (error: any) {
|
||||||
|
message.error(error.message);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button type="primary" danger>
|
||||||
|
删除
|
||||||
|
</Button>
|
||||||
|
</Popconfirm>
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageContainer header={{ title: '口味列表' }}>
|
||||||
|
<ProTable<API.Category>
|
||||||
|
headerTitle="查询表格"
|
||||||
|
actionRef={actionRef}
|
||||||
|
rowKey="id"
|
||||||
|
toolBarRender={() => [<CreateForm tableRef={actionRef} />]}
|
||||||
|
request={async (params) => {
|
||||||
|
const { data, success } = await productcontrollerGetflavors(params);
|
||||||
|
return {
|
||||||
|
total: data?.total || 0,
|
||||||
|
data: data?.items || [],
|
||||||
|
success,
|
||||||
|
};
|
||||||
|
}}
|
||||||
|
columns={columns}
|
||||||
|
/>
|
||||||
|
</PageContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const CreateForm: React.FC<{
|
||||||
|
tableRef: React.MutableRefObject<ActionType | undefined>;
|
||||||
|
}> = ({ tableRef }) => {
|
||||||
|
const { message } = App.useApp();
|
||||||
|
return (
|
||||||
|
<DrawerForm<API.CreateCategoryDTO>
|
||||||
|
title="新建"
|
||||||
|
trigger={
|
||||||
|
<Button type="primary">
|
||||||
|
<PlusOutlined />
|
||||||
|
新建
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
autoFocusFirstInput
|
||||||
|
drawerProps={{
|
||||||
|
destroyOnHidden: true,
|
||||||
|
}}
|
||||||
|
onFinish={async (values) => {
|
||||||
|
try {
|
||||||
|
const { success, message: errMsg } =
|
||||||
|
await productcontrollerCreateflavors(values);
|
||||||
|
if (!success) {
|
||||||
|
throw new Error(errMsg);
|
||||||
|
}
|
||||||
|
tableRef.current?.reload();
|
||||||
|
message.success('提交成功');
|
||||||
|
return true;
|
||||||
|
} catch (error: any) {
|
||||||
|
message.error(error.message);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ProFormText
|
||||||
|
name="name"
|
||||||
|
width="md"
|
||||||
|
label="口味名称"
|
||||||
|
placeholder="请输入名称"
|
||||||
|
rules={[{ required: true, message: '请输入名称' }]}
|
||||||
|
/>
|
||||||
|
<ProFormText
|
||||||
|
name="unique_key"
|
||||||
|
width="md"
|
||||||
|
label="Key"
|
||||||
|
placeholder="请输入Key"
|
||||||
|
rules={[{ required: true, message: '请输入Key' }]}
|
||||||
|
/>
|
||||||
|
</DrawerForm>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const UpdateForm: React.FC<{
|
||||||
|
tableRef: React.MutableRefObject<ActionType | undefined>;
|
||||||
|
values: API.Category;
|
||||||
|
}> = ({ tableRef, values: initialValues }) => {
|
||||||
|
const { message } = App.useApp();
|
||||||
|
return (
|
||||||
|
<DrawerForm<API.UpdateCategoryDTO>
|
||||||
|
title="编辑"
|
||||||
|
initialValues={initialValues}
|
||||||
|
trigger={
|
||||||
|
<Button type="primary">
|
||||||
|
<EditOutlined />
|
||||||
|
编辑
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
autoFocusFirstInput
|
||||||
|
drawerProps={{
|
||||||
|
destroyOnHidden: true,
|
||||||
|
}}
|
||||||
|
onFinish={async (values) => {
|
||||||
|
try {
|
||||||
|
const { success, message: errMsg } =
|
||||||
|
await productcontrollerUpdateflavors(
|
||||||
|
{ id: initialValues.id },
|
||||||
|
values,
|
||||||
|
);
|
||||||
|
if (!success) {
|
||||||
|
throw new Error(errMsg);
|
||||||
|
}
|
||||||
|
message.success('提交成功');
|
||||||
|
tableRef.current?.reload();
|
||||||
|
return true;
|
||||||
|
} catch (error: any) {
|
||||||
|
message.error(error.message);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ProForm.Group>
|
||||||
|
<ProFormText
|
||||||
|
name="name"
|
||||||
|
width="md"
|
||||||
|
label="口味名称"
|
||||||
|
placeholder="请输入名称"
|
||||||
|
rules={[{ required: true, message: '请输入名称' }]}
|
||||||
|
/>
|
||||||
|
</ProForm.Group>
|
||||||
|
</DrawerForm>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default List;
|
||||||
|
|
@ -1,368 +0,0 @@
|
||||||
import React, { useEffect, useState, useMemo } from 'react';
|
|
||||||
import { PageContainer, ProFormSelect } from '@ant-design/pro-components';
|
|
||||||
import { Card, Collapse, Divider, Image, Select, Space, Typography, message } from 'antd';
|
|
||||||
import { categorycontrollerGetall } from '@/servers/api/category';
|
|
||||||
import { productcontrollerGetproductlistgrouped } from '@/servers/api/product';
|
|
||||||
import { dictcontrollerGetdictitems } from '@/servers/api/dict';
|
|
||||||
|
|
||||||
// Define interfaces
|
|
||||||
interface Category {
|
|
||||||
id: number;
|
|
||||||
name: string;
|
|
||||||
title: string;
|
|
||||||
attributes: string[]; // List of attribute names for this category
|
|
||||||
}
|
|
||||||
interface Attribute {
|
|
||||||
id: number;
|
|
||||||
name: string;
|
|
||||||
title: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface AttributeValue {
|
|
||||||
id: number;
|
|
||||||
name: string;
|
|
||||||
title: string;
|
|
||||||
titleCN?: string;
|
|
||||||
value?: string;
|
|
||||||
image?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Product {
|
|
||||||
id: number;
|
|
||||||
sku: string;
|
|
||||||
name: string;
|
|
||||||
image?: string;
|
|
||||||
brandId: number;
|
|
||||||
brandName: string;
|
|
||||||
attributes: { [key: string]: number }; // attribute name to attribute value id mapping
|
|
||||||
price?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Grouped products by attribute value
|
|
||||||
interface GroupedProducts {
|
|
||||||
[attributeValueId: string]: Product[];
|
|
||||||
}
|
|
||||||
|
|
||||||
// ProductCard component for displaying single product
|
|
||||||
const ProductCard: React.FC<{ product: Product }> = ({ product }) => {
|
|
||||||
return (
|
|
||||||
<Card hoverable style={{ width: 240 }}>
|
|
||||||
{/* <div style={{ height: 180, overflow: 'hidden', marginBottom: '12px' }}>
|
|
||||||
<Image
|
|
||||||
src={product.image || 'https://via.placeholder.com/240x180?text=No+Image'}
|
|
||||||
alt={product.name}
|
|
||||||
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
|
|
||||||
/>
|
|
||||||
</div> */}
|
|
||||||
<div>
|
|
||||||
<Typography.Text type="secondary" style={{ fontSize: 12, display: 'block', marginBottom: '4px' }}>
|
|
||||||
{product.sku}
|
|
||||||
</Typography.Text>
|
|
||||||
<Typography.Text ellipsis style={{ width: '100%', display: 'block', marginBottom: '8px' }}>
|
|
||||||
{product.name}
|
|
||||||
</Typography.Text>
|
|
||||||
<Typography.Text strong style={{ fontSize: 16, color: '#ff4d4f', display: 'block' }}>
|
|
||||||
¥{product.price || '--'}
|
|
||||||
</Typography.Text>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
// ProductGroup component for displaying grouped products
|
|
||||||
const ProductGroup: React.FC<{
|
|
||||||
attributeValueId: string;
|
|
||||||
groupProducts: Product[];
|
|
||||||
attributeValue: AttributeValue | undefined;
|
|
||||||
attributeName: string;
|
|
||||||
}> = ({ attributeValueId, groupProducts, attributeValue }) => {
|
|
||||||
// State for collapse control
|
|
||||||
const [isCollapsed, setIsCollapsed] = useState(false);
|
|
||||||
|
|
||||||
// Create collapse panel header
|
|
||||||
const panelHeader = (
|
|
||||||
<Space>
|
|
||||||
{attributeValue?.image && (
|
|
||||||
<Image
|
|
||||||
src={attributeValue.image}
|
|
||||||
style={{ width: 24, height: 24, objectFit: 'cover', borderRadius: 4 }}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<Typography.Title level={5} style={{ margin: 0 }}>
|
|
||||||
<span>
|
|
||||||
{attributeValue?.titleCN || attributeValue?.title || attributeValue?.name || attributeValueId||'未知'}
|
|
||||||
(共 {groupProducts.length} 个产品)
|
|
||||||
</span>
|
|
||||||
</Typography.Title>
|
|
||||||
</Space>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Collapse
|
|
||||||
activeKey={isCollapsed ? [] : [attributeValueId]}
|
|
||||||
onChange={(key) => setIsCollapsed(Array.isArray(key) && key.length === 0)}
|
|
||||||
ghost
|
|
||||||
bordered={false}
|
|
||||||
items={[
|
|
||||||
{
|
|
||||||
key: attributeValueId,
|
|
||||||
label: panelHeader,
|
|
||||||
children: (
|
|
||||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '16px', paddingTop: '8px' }}>
|
|
||||||
{groupProducts.map((product) => (
|
|
||||||
<ProductCard key={product.id} product={product} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Main ProductGroupBy component
|
|
||||||
const ProductGroupBy: React.FC = () => {
|
|
||||||
// State management
|
|
||||||
const [categories, setCategories] = useState<Category[]>([]);
|
|
||||||
const [selectedCategory, setSelectedCategory] = useState<string | null>(null);
|
|
||||||
// Store selected values for each attribute
|
|
||||||
const [attributeFilters, setAttributeFilters] = useState<{ [key: string]: number | null }>({});
|
|
||||||
|
|
||||||
// Group by attribute
|
|
||||||
const [groupByAttribute, setGroupByAttribute] = useState<string | null>(null);
|
|
||||||
|
|
||||||
// Products
|
|
||||||
const [products, setProducts] = useState<Product[]>([]);
|
|
||||||
const [groupedProducts, setGroupedProducts] = useState<GroupedProducts>({});
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
|
|
||||||
// Extract all unique attributes from categories
|
|
||||||
const categoryAttributes = useMemo(() => {
|
|
||||||
if (!selectedCategory) return [];
|
|
||||||
const categoryItem = categories.find((category: any) => category.name === selectedCategory);
|
|
||||||
if (!categoryItem) return [];
|
|
||||||
const attributesList: Attribute[] = categoryItem.attributes.map((attribute: any, index) => ({
|
|
||||||
...attribute.attributeDict,
|
|
||||||
id: index + 1,
|
|
||||||
}));
|
|
||||||
return attributesList;
|
|
||||||
}, [selectedCategory]);
|
|
||||||
|
|
||||||
// Fetch categories list
|
|
||||||
const fetchCategories = async () => {
|
|
||||||
try {
|
|
||||||
const response = await categorycontrollerGetall();
|
|
||||||
const rawCategories = Array.isArray(response) ? response : response?.data || [];
|
|
||||||
setCategories(rawCategories);
|
|
||||||
|
|
||||||
// Set default category
|
|
||||||
if (rawCategories.length > 0) {
|
|
||||||
const defaultCategory = rawCategories.find((category: any) => category.name === 'nicotine-pouches');
|
|
||||||
setSelectedCategory(defaultCategory?.name || rawCategories[0].name);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to fetch categories:', error);
|
|
||||||
message.error('获取分类列表失败');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Update category attributes when selected category changes
|
|
||||||
useEffect(() => {
|
|
||||||
if (!selectedCategory) return;
|
|
||||||
|
|
||||||
const category = categories.find(cat => cat.name === selectedCategory);
|
|
||||||
if (!category) return;
|
|
||||||
|
|
||||||
// Get attributes for this category
|
|
||||||
const attributesForCategory = categoryAttributes.filter(attr =>
|
|
||||||
attr.name === 'brand' || category.attributes.includes(attr.name)
|
|
||||||
);
|
|
||||||
// Reset attribute filters when category changes
|
|
||||||
const newFilters: { [key: string]: number | null } = {};
|
|
||||||
attributesForCategory.forEach(attr => {
|
|
||||||
newFilters[attr.name] = null;
|
|
||||||
});
|
|
||||||
setAttributeFilters(newFilters);
|
|
||||||
|
|
||||||
// Set default group by attribute
|
|
||||||
if (attributesForCategory.length > 0) {
|
|
||||||
setGroupByAttribute(attributesForCategory[0].name);
|
|
||||||
}
|
|
||||||
}, [selectedCategory, categories, categoryAttributes]);
|
|
||||||
|
|
||||||
// Handle attribute filter change
|
|
||||||
const handleAttributeFilterChange = (attributeName: string, value: number | null) => {
|
|
||||||
setAttributeFilters(prev => ({ ...prev, [attributeName]: value }));
|
|
||||||
};
|
|
||||||
|
|
||||||
// Fetch products based on filters
|
|
||||||
const fetchProducts = async () => {
|
|
||||||
if (!selectedCategory || !groupByAttribute) return;
|
|
||||||
|
|
||||||
setLoading(true);
|
|
||||||
try {
|
|
||||||
const params: any = {
|
|
||||||
category: selectedCategory,
|
|
||||||
groupBy: groupByAttribute
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
const response = await productcontrollerGetproductlistgrouped(params);
|
|
||||||
const grouped = response?.data || {};
|
|
||||||
setGroupedProducts(grouped);
|
|
||||||
|
|
||||||
// Flatten grouped products to get all products
|
|
||||||
const allProducts = Object.values(grouped).flat() as Product[];
|
|
||||||
setProducts(allProducts);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to fetch grouped products:', error);
|
|
||||||
message.error('获取分组产品列表失败');
|
|
||||||
setProducts([]);
|
|
||||||
setGroupedProducts({});
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Initial data fetch
|
|
||||||
useEffect(() => {
|
|
||||||
fetchCategories();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Fetch products when filters change
|
|
||||||
useEffect(() => {
|
|
||||||
fetchProducts();
|
|
||||||
}, [selectedCategory, attributeFilters, groupByAttribute]);
|
|
||||||
|
|
||||||
// Destructure antd components
|
|
||||||
const { Title, Text } = Typography;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<PageContainer title="品牌空间">
|
|
||||||
<div style={{ padding: '16px', background: '#fff' }}>
|
|
||||||
{/* Filter Section */}
|
|
||||||
<div style={{ marginBottom: '24px' }}>
|
|
||||||
<Title level={4} style={{ marginBottom: '16px' }}>筛选条件</Title>
|
|
||||||
<Space direction="vertical" size="large">
|
|
||||||
{/* Category Filter */}
|
|
||||||
<div>
|
|
||||||
<Text strong>选择分类:</Text>
|
|
||||||
<Select
|
|
||||||
placeholder="请选择分类"
|
|
||||||
style={{ width: 300, marginLeft: '8px' }}
|
|
||||||
value={selectedCategory}
|
|
||||||
onChange={setSelectedCategory}
|
|
||||||
allowClear
|
|
||||||
showSearch
|
|
||||||
optionFilterProp="children"
|
|
||||||
>
|
|
||||||
{categories.map(category => (
|
|
||||||
<Option key={category.id} value={category.name}>
|
|
||||||
{category.title}
|
|
||||||
</Option>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Attribute Filters */}
|
|
||||||
{categoryAttributes.length > 0 && (
|
|
||||||
<div>
|
|
||||||
<Text strong>属性筛选:</Text>
|
|
||||||
<Space direction="vertical" style={{ marginTop: '8px', width: '100%' }}>
|
|
||||||
{categoryAttributes.map(attr => (
|
|
||||||
<div key={attr.id} style={{ display: 'flex', alignItems: 'center' }}>
|
|
||||||
<Text style={{ width: '100px' }}>{attr.title}:</Text>
|
|
||||||
<ProFormSelect
|
|
||||||
placeholder={`请选择${attr.title}`}
|
|
||||||
style={{ width: 300 }}
|
|
||||||
value={attributeFilters[attr.name] || null}
|
|
||||||
onChange={value => handleAttributeFilterChange(attr.name, value)}
|
|
||||||
allowClear
|
|
||||||
showSearch
|
|
||||||
optionFilterProp="children"
|
|
||||||
request={async (params) => {
|
|
||||||
try {
|
|
||||||
console.log('params', params,attr);
|
|
||||||
const response = await dictcontrollerGetdictitems({ dictId: attr.name });
|
|
||||||
const rawValues = Array.isArray(response) ? response : response?.data?.items || [];
|
|
||||||
const filteredValues = rawValues.filter((value: any) =>
|
|
||||||
value.dictId === attr.name || value.dict?.id === attr.name || value.dict?.name === attr.name
|
|
||||||
);
|
|
||||||
return {
|
|
||||||
options: filteredValues.map((value: any) => ({
|
|
||||||
label: `${value.name}${value.titleCN || value.title}`,
|
|
||||||
value: value.id
|
|
||||||
}))
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`Failed to fetch ${attr.title} values:`, error);
|
|
||||||
message.error(`获取${attr.title}属性值失败`);
|
|
||||||
return { options: [] };
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</Space>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Group By Attribute */}
|
|
||||||
{categoryAttributes.length > 0 && (
|
|
||||||
<div>
|
|
||||||
<Text strong>分组依据:</Text>
|
|
||||||
<Select
|
|
||||||
placeholder="请选择分组属性"
|
|
||||||
style={{ width: 300, marginLeft: '8px' }}
|
|
||||||
value={groupByAttribute}
|
|
||||||
onChange={setGroupByAttribute}
|
|
||||||
showSearch
|
|
||||||
optionFilterProp="children"
|
|
||||||
>
|
|
||||||
{categoryAttributes.map(attr => (
|
|
||||||
<Option key={attr.id} value={attr.name}>
|
|
||||||
{attr.title}
|
|
||||||
</Option>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Space>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Divider />
|
|
||||||
|
|
||||||
{/* Products Section */}
|
|
||||||
<div>
|
|
||||||
<Title level={4} style={{ marginBottom: '16px' }}>产品列表 ({products.length} 个产品)</Title>
|
|
||||||
|
|
||||||
{loading ? (
|
|
||||||
<div style={{ textAlign: 'center', padding: '64px' }}>
|
|
||||||
<Text>加载中...</Text>
|
|
||||||
</div>
|
|
||||||
) : groupByAttribute && Object.keys(groupedProducts).length > 0 ? (
|
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '24px' }}>
|
|
||||||
{Object.entries(groupedProducts).map(([attrValueId, groupProducts]) => {
|
|
||||||
return (
|
|
||||||
<ProductGroup
|
|
||||||
key={attrValueId}
|
|
||||||
attributeValueId={attrValueId}
|
|
||||||
groupProducts={groupProducts}
|
|
||||||
// attributeValue={}
|
|
||||||
attributeName={groupByAttribute!}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div style={{ textAlign: 'center', padding: '64px', background: '#fafafa', borderRadius: 8 }}>
|
|
||||||
<Text type="secondary">暂无产品</Text>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</PageContainer>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ProductGroupBy;
|
|
||||||
|
|
@ -1,419 +0,0 @@
|
||||||
import AttributeFormItem from '@/pages/Product/Attribute/components/AttributeFormItem';
|
|
||||||
import {
|
|
||||||
productcontrollerCreateproduct,
|
|
||||||
productcontrollerGetcategoriesall,
|
|
||||||
productcontrollerGetcategoryattributes,
|
|
||||||
productcontrollerGetproductlist,
|
|
||||||
} from '@/servers/api/product';
|
|
||||||
import { stockcontrollerGetstocks as getStocks } from '@/servers/api/stock';
|
|
||||||
import { templatecontrollerRendertemplate } from '@/servers/api/template';
|
|
||||||
import { PlusOutlined } from '@ant-design/icons';
|
|
||||||
import {
|
|
||||||
ActionType,
|
|
||||||
DrawerForm,
|
|
||||||
ProForm,
|
|
||||||
ProFormDigit,
|
|
||||||
ProFormInstance,
|
|
||||||
ProFormList,
|
|
||||||
ProFormSelect,
|
|
||||||
ProFormText,
|
|
||||||
ProFormTextArea,
|
|
||||||
} from '@ant-design/pro-components';
|
|
||||||
import { App, Button, Tag } from 'antd';
|
|
||||||
import React, { useEffect, useRef, useState } from 'react';
|
|
||||||
|
|
||||||
const capitalize = (s: string) => s.charAt(0).toLocaleUpperCase() + s.slice(1);
|
|
||||||
|
|
||||||
const CreateForm: React.FC<{
|
|
||||||
tableRef: React.MutableRefObject<ActionType | undefined>;
|
|
||||||
}> = ({ tableRef }) => {
|
|
||||||
// antd 的消息提醒
|
|
||||||
const { message } = App.useApp();
|
|
||||||
// 表单引用
|
|
||||||
const formRef = useRef<ProFormInstance>();
|
|
||||||
const [stockStatus, setStockStatus] = useState<
|
|
||||||
'in-stock' | 'out-of-stock' | null
|
|
||||||
>(null);
|
|
||||||
|
|
||||||
const [categories, setCategories] = useState<any[]>([]);
|
|
||||||
const [activeAttributes, setActiveAttributes] = useState<any[]>([]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
productcontrollerGetcategoriesall().then((res: any) => {
|
|
||||||
setCategories(res?.data || []);
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleCategoryChange = async (categoryId: number) => {
|
|
||||||
if (!categoryId) {
|
|
||||||
setActiveAttributes([]);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const res: any = await productcontrollerGetcategoryattributes({
|
|
||||||
id: categoryId,
|
|
||||||
});
|
|
||||||
setActiveAttributes(res?.data || []);
|
|
||||||
} catch (error) {
|
|
||||||
message.error('获取分类属性失败');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description 生成 SKU
|
|
||||||
*/
|
|
||||||
const handleGenerateSku = async () => {
|
|
||||||
try {
|
|
||||||
// 从表单引用中获取当前表单的值
|
|
||||||
const formValues = formRef.current?.getFieldsValue();
|
|
||||||
const { humidityValues, brandValues, strengthValues, flavorValues } =
|
|
||||||
formValues;
|
|
||||||
// 检查是否所有必需的字段都已选择
|
|
||||||
// 注意:这里仅检查标准属性,如果当前分类没有这些属性,可能需要调整逻辑
|
|
||||||
// 暂时保持原样,假设常用属性会被配置
|
|
||||||
|
|
||||||
// 所选值(用于 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] || '');
|
|
||||||
console.log(formValues);
|
|
||||||
// 调用模板渲染API来生成SKU
|
|
||||||
const {
|
|
||||||
data: rendered,
|
|
||||||
message: msg,
|
|
||||||
success,
|
|
||||||
} = await templatecontrollerRendertemplate(
|
|
||||||
{ name: 'product.sku' },
|
|
||||||
{
|
|
||||||
category: formValues.category,
|
|
||||||
attributes: [
|
|
||||||
{
|
|
||||||
dict: { name: 'brand' },
|
|
||||||
shortName: brandName || '',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
dict: { name: 'flavor' },
|
|
||||||
shortName: flavorName || '',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
dict: { name: 'strength' },
|
|
||||||
shortName: strengthName || '',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
dict: { name: 'humidity' },
|
|
||||||
shortName: humidityName ? capitalize(humidityName) : '',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
);
|
|
||||||
if (!success) {
|
|
||||||
throw new Error(msg);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 将生成的SKU设置到表单字段中
|
|
||||||
formRef.current?.setFieldsValue({ sku: rendered });
|
|
||||||
} catch (error: any) {
|
|
||||||
message.error(`生成失败: ${error.message}`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description 生成产品名称
|
|
||||||
*/
|
|
||||||
const handleGenerateName = async () => {
|
|
||||||
try {
|
|
||||||
// 从表单引用中获取当前表单的值
|
|
||||||
const formValues = formRef.current?.getFieldsValue();
|
|
||||||
const { humidityValues, brandValues, strengthValues, flavorValues } =
|
|
||||||
formValues;
|
|
||||||
|
|
||||||
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;
|
|
||||||
|
|
||||||
// 调用模板渲染API来生成产品名称
|
|
||||||
const {
|
|
||||||
message: msg,
|
|
||||||
data: rendered,
|
|
||||||
success,
|
|
||||||
} = await templatecontrollerRendertemplate(
|
|
||||||
{ name: 'product.title' },
|
|
||||||
{
|
|
||||||
brand: brandTitle,
|
|
||||||
strength: strengthTitle,
|
|
||||||
flavor: flavorTitle,
|
|
||||||
model: '',
|
|
||||||
humidity:
|
|
||||||
humidityName === 'dry'
|
|
||||||
? 'Dry'
|
|
||||||
: humidityName === 'moisture'
|
|
||||||
? 'Moisture'
|
|
||||||
: capitalize(humidityName),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
if (!success) {
|
|
||||||
throw new Error(msg);
|
|
||||||
}
|
|
||||||
// 将生成的名称设置到表单字段中
|
|
||||||
formRef.current?.setFieldsValue({ name: rendered });
|
|
||||||
} catch (error: any) {
|
|
||||||
message.error(`生成失败: ${error.message}`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
// TODO 可以输入brand等
|
|
||||||
return (
|
|
||||||
<DrawerForm<any>
|
|
||||||
title="新建"
|
|
||||||
formRef={formRef} // Pass formRef
|
|
||||||
trigger={
|
|
||||||
<Button type="primary">
|
|
||||||
<PlusOutlined />
|
|
||||||
新建
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
autoFocusFirstInput
|
|
||||||
drawerProps={{
|
|
||||||
destroyOnHidden: true,
|
|
||||||
}}
|
|
||||||
onValuesChange={async (changedValues) => {
|
|
||||||
// 当 Category 发生变化时
|
|
||||||
if ('categoryId' in changedValues) {
|
|
||||||
handleCategoryChange(changedValues.categoryId);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 当 SKU 发生变化时
|
|
||||||
if ('sku' in changedValues) {
|
|
||||||
const sku = changedValues.sku;
|
|
||||||
// 如果 sku 存在
|
|
||||||
if (sku) {
|
|
||||||
// 获取库存信息
|
|
||||||
const { data } = await getStocks({
|
|
||||||
sku: sku,
|
|
||||||
} as any);
|
|
||||||
// 如果库存信息存在且不为空
|
|
||||||
if (data && data.items && data.items.length > 0) {
|
|
||||||
// 设置在库状态
|
|
||||||
setStockStatus('in-stock');
|
|
||||||
// 设置产品类型为单品
|
|
||||||
formRef.current?.setFieldsValue({ type: 'single' });
|
|
||||||
} else {
|
|
||||||
// 设置未在库状态
|
|
||||||
setStockStatus('out-of-stock');
|
|
||||||
// 设置产品类型为套装
|
|
||||||
formRef.current?.setFieldsValue({ type: 'bundle' });
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// 如果 sku 不存在,则重置状态
|
|
||||||
setStockStatus(null);
|
|
||||||
formRef.current?.setFieldsValue({ type: null });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onFinish={async (values: any) => {
|
|
||||||
// 根据产品类型决定是否组装 attributes
|
|
||||||
// 如果产品类型为 bundle,则 attributes 为空数组
|
|
||||||
// 如果产品类型为 single,则根据 activeAttributes 动态组装 attributes
|
|
||||||
const attributes =
|
|
||||||
values.type === 'bundle'
|
|
||||||
? []
|
|
||||||
: activeAttributes.flatMap((attr: any) => {
|
|
||||||
const dictName = attr.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,
|
|
||||||
description: (values as any).description,
|
|
||||||
shortDescription: (values as any).shortDescription,
|
|
||||||
sku: (values as any).sku,
|
|
||||||
price: (values as any).price,
|
|
||||||
promotionPrice: (values as any).promotionPrice,
|
|
||||||
attributes,
|
|
||||||
type: values.type, // 直接使用 type
|
|
||||||
components: values.components,
|
|
||||||
categoryId: values.categoryId,
|
|
||||||
siteSkus: values.siteSkus,
|
|
||||||
};
|
|
||||||
const { success, message: errMsg } =
|
|
||||||
await productcontrollerCreateproduct(payload);
|
|
||||||
if (success) {
|
|
||||||
message.success('提交成功');
|
|
||||||
tableRef.current?.reloadAndRest?.();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
message.error(errMsg);
|
|
||||||
return false;
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ProForm.Group>
|
|
||||||
<ProFormText
|
|
||||||
name="sku"
|
|
||||||
label="SKU"
|
|
||||||
width="md"
|
|
||||||
placeholder="请输入SKU"
|
|
||||||
rules={[{ required: true, message: '请输入SKU' }]}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Button style={{ marginTop: '32px' }} onClick={handleGenerateSku}>
|
|
||||||
自动生成
|
|
||||||
</Button>
|
|
||||||
{stockStatus && (
|
|
||||||
<Tag
|
|
||||||
style={{ marginTop: '32px' }}
|
|
||||||
color={stockStatus === 'in-stock' ? 'green' : 'orange'}
|
|
||||||
>
|
|
||||||
{stockStatus === 'in-stock' ? '在库' : '未在库'}
|
|
||||||
</Tag>
|
|
||||||
)}
|
|
||||||
</ProForm.Group>
|
|
||||||
<ProFormSelect
|
|
||||||
name="siteSkus"
|
|
||||||
initialValue={[]}
|
|
||||||
label="站点 SKU 列表"
|
|
||||||
width="md"
|
|
||||||
mode="tags"
|
|
||||||
placeholder="输入站点 SKU,回车添加"
|
|
||||||
/>
|
|
||||||
<ProForm.Group>
|
|
||||||
<ProFormText
|
|
||||||
name="name"
|
|
||||||
label="名称"
|
|
||||||
width="md"
|
|
||||||
placeholder="请输入名称"
|
|
||||||
rules={[{ required: true, message: '请输入名称' }]}
|
|
||||||
/>
|
|
||||||
<Button style={{ marginTop: '32px' }} onClick={handleGenerateName}>
|
|
||||||
自动生成
|
|
||||||
</Button>
|
|
||||||
</ProForm.Group>
|
|
||||||
<ProFormSelect
|
|
||||||
name="type"
|
|
||||||
label="产品类型"
|
|
||||||
options={[
|
|
||||||
{ value: 'single', label: '单品' },
|
|
||||||
{ value: 'bundle', label: '套装' },
|
|
||||||
]}
|
|
||||||
rules={[{ required: true, message: '请选择产品类型' }]}
|
|
||||||
/>
|
|
||||||
<ProForm.Item
|
|
||||||
shouldUpdate={(prevValues: any, curValues: any) =>
|
|
||||||
prevValues.type !== curValues.type
|
|
||||||
}
|
|
||||||
noStyle
|
|
||||||
>
|
|
||||||
{({ getFieldValue }: { getFieldValue: (name: string) => any }) =>
|
|
||||||
getFieldValue('type') === 'bundle' ? (
|
|
||||||
<ProFormList
|
|
||||||
name="components"
|
|
||||||
label="产品组成"
|
|
||||||
initialValue={[{ sku: '', quantity: 1 }]}
|
|
||||||
creatorButtonProps={{
|
|
||||||
creatorButtonText: '添加子产品',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ProForm.Group>
|
|
||||||
<ProFormSelect
|
|
||||||
name="sku"
|
|
||||||
label="子产品SKU"
|
|
||||||
width="md"
|
|
||||||
showSearch
|
|
||||||
debounceTime={300}
|
|
||||||
placeholder="请输入子产品SKU"
|
|
||||||
rules={[{ required: true, message: '请输入子产品SKU' }]}
|
|
||||||
request={async ({ keyWords }) => {
|
|
||||||
const params = keyWords
|
|
||||||
? { sku: keyWords, name: keyWords, type: 'single' }
|
|
||||||
: { pageSize: 9999, type: 'single' };
|
|
||||||
const { data } = await productcontrollerGetproductlist(
|
|
||||||
params as any,
|
|
||||||
);
|
|
||||||
if (!data || !data.items) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
// 只返回类型为单品的产品
|
|
||||||
return data.items
|
|
||||||
.filter((item: any) => item.type === 'single' && item.sku)
|
|
||||||
.map((item: any) => ({
|
|
||||||
label: `${item.sku} - ${item.name}`,
|
|
||||||
value: item.sku,
|
|
||||||
}));
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<ProFormDigit
|
|
||||||
name="quantity"
|
|
||||||
label="数量"
|
|
||||||
width="xs"
|
|
||||||
min={1}
|
|
||||||
initialValue={1}
|
|
||||||
rules={[{ required: true, message: '请输入数量' }]}
|
|
||||||
/>
|
|
||||||
</ProForm.Group>
|
|
||||||
</ProFormList>
|
|
||||||
) : null
|
|
||||||
}
|
|
||||||
</ProForm.Item>
|
|
||||||
|
|
||||||
<ProFormSelect
|
|
||||||
name="categoryId"
|
|
||||||
label="分类"
|
|
||||||
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.name}
|
|
||||||
name={`${attr.name}Values`}
|
|
||||||
label={attr.title}
|
|
||||||
isTag
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
|
|
||||||
<ProFormText
|
|
||||||
name="price"
|
|
||||||
label="价格"
|
|
||||||
width="md"
|
|
||||||
placeholder="请输入价格"
|
|
||||||
rules={[{ required: false }]}
|
|
||||||
/>
|
|
||||||
<ProFormText
|
|
||||||
name="promotionPrice"
|
|
||||||
label="促销价"
|
|
||||||
width="md"
|
|
||||||
placeholder="请输入促销价"
|
|
||||||
rules={[{ required: false }]}
|
|
||||||
/>
|
|
||||||
<ProFormTextArea
|
|
||||||
name="shortDescription"
|
|
||||||
style={{ width: '100%' }}
|
|
||||||
label="产品简短描述"
|
|
||||||
placeholder="请输入产品简短描述"
|
|
||||||
/>
|
|
||||||
<ProFormTextArea
|
|
||||||
name="description"
|
|
||||||
style={{ width: '100%' }}
|
|
||||||
label="产品描述"
|
|
||||||
placeholder="请输入产品描述"
|
|
||||||
/>
|
|
||||||
</DrawerForm>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default CreateForm;
|
|
||||||
|
|
@ -1,403 +0,0 @@
|
||||||
import AttributeFormItem from '@/pages/Product/Attribute/components/AttributeFormItem';
|
|
||||||
import {
|
|
||||||
productcontrollerGetcategoriesall,
|
|
||||||
productcontrollerGetcategoryattributes,
|
|
||||||
productcontrollerGetproductcomponents,
|
|
||||||
productcontrollerGetproductlist,
|
|
||||||
productcontrollerUpdateproduct,
|
|
||||||
} from '@/servers/api/product';
|
|
||||||
import { sitecontrollerAll } from '@/servers/api/site';
|
|
||||||
import { stockcontrollerGetstocks as getStocks } from '@/servers/api/stock';
|
|
||||||
import {
|
|
||||||
ActionType,
|
|
||||||
DrawerForm,
|
|
||||||
ProForm,
|
|
||||||
ProFormInstance,
|
|
||||||
ProFormList,
|
|
||||||
ProFormSelect,
|
|
||||||
ProFormText,
|
|
||||||
ProFormTextArea,
|
|
||||||
} from '@ant-design/pro-components';
|
|
||||||
import { App, Button, Tag } from 'antd';
|
|
||||||
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
|
||||||
|
|
||||||
const EditForm: React.FC<{
|
|
||||||
record: API.Product;
|
|
||||||
tableRef: React.MutableRefObject<ActionType | undefined>;
|
|
||||||
trigger?: JSX.Element;
|
|
||||||
}> = ({ record, tableRef, trigger }) => {
|
|
||||||
const { message } = App.useApp();
|
|
||||||
const formRef = useRef<ProFormInstance>();
|
|
||||||
const [components, setComponents] = useState<
|
|
||||||
{ sku: string; quantity: number }[]
|
|
||||||
>([]);
|
|
||||||
const [type, setType] = useState<'single' | 'bundle' | null>(null);
|
|
||||||
const [stockStatus, setStockStatus] = useState<
|
|
||||||
'in-stock' | 'out-of-stock' | null
|
|
||||||
>(null);
|
|
||||||
const [sites, setSites] = useState<any[]>([]);
|
|
||||||
|
|
||||||
const [categories, setCategories] = useState<any[]>([]);
|
|
||||||
const [activeAttributes, setActiveAttributes] = useState<any[]>([]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
productcontrollerGetcategoriesall().then((res: any) => {
|
|
||||||
setCategories(res?.data || []);
|
|
||||||
});
|
|
||||||
// 获取站点列表用于站点SKU选择
|
|
||||||
sitecontrollerAll().then((res: any) => {
|
|
||||||
setSites(res?.data || []);
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const categoryId =
|
|
||||||
(record as any).categoryId || (record as any).category?.id;
|
|
||||||
if (categoryId) {
|
|
||||||
productcontrollerGetcategoryattributes({ id: categoryId }).then(
|
|
||||||
(res: any) => {
|
|
||||||
setActiveAttributes(res?.data || []);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
setActiveAttributes([]);
|
|
||||||
}
|
|
||||||
}, [record]);
|
|
||||||
|
|
||||||
const handleCategoryChange = async (categoryId: number) => {
|
|
||||||
if (!categoryId) {
|
|
||||||
setActiveAttributes([]);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const res: any = await productcontrollerGetcategoryattributes({
|
|
||||||
id: categoryId,
|
|
||||||
});
|
|
||||||
setActiveAttributes(res?.data || []);
|
|
||||||
} catch (error) {
|
|
||||||
message.error('获取分类属性失败');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
(async () => {
|
|
||||||
const { data: stockData } = await getStocks({
|
|
||||||
sku: record.sku,
|
|
||||||
} as any);
|
|
||||||
if (stockData && stockData.items && stockData.items.length > 0) {
|
|
||||||
// 如果有库存,则为单品
|
|
||||||
setType('single');
|
|
||||||
setStockStatus('in-stock');
|
|
||||||
formRef.current?.setFieldsValue({ type: 'single' });
|
|
||||||
} else {
|
|
||||||
// 如果没有库存,则为套装
|
|
||||||
setType('bundle');
|
|
||||||
setStockStatus('out-of-stock');
|
|
||||||
formRef.current?.setFieldsValue({ type: 'bundle' });
|
|
||||||
}
|
|
||||||
const { data: componentsData } =
|
|
||||||
await productcontrollerGetproductcomponents({ id: record.id });
|
|
||||||
setComponents(componentsData || []);
|
|
||||||
})();
|
|
||||||
}, [record]);
|
|
||||||
|
|
||||||
const initialValues = useMemo(() => {
|
|
||||||
return {
|
|
||||||
...record,
|
|
||||||
...((record as any).attributes || []).reduce((acc: any, cur: any) => {
|
|
||||||
const dictName = cur.dict?.name;
|
|
||||||
if (dictName) {
|
|
||||||
const key = `${dictName}Values`;
|
|
||||||
if (!acc[key]) {
|
|
||||||
acc[key] = [];
|
|
||||||
}
|
|
||||||
acc[key].push(cur.name);
|
|
||||||
}
|
|
||||||
return acc;
|
|
||||||
}, {} as any),
|
|
||||||
components: components,
|
|
||||||
type: type,
|
|
||||||
categoryId: (record as any).categoryId || (record as any).category?.id,
|
|
||||||
// 初始化站点SKU为字符串数组
|
|
||||||
// 修改后代码:
|
|
||||||
siteSkus: (record.siteSkus || []).map((code) => ({ code })),
|
|
||||||
};
|
|
||||||
}, [record, components, type]);
|
|
||||||
return (
|
|
||||||
<DrawerForm<any>
|
|
||||||
title="编辑"
|
|
||||||
formRef={formRef}
|
|
||||||
trigger={trigger || <Button type="link">编辑</Button>}
|
|
||||||
autoFocusFirstInput
|
|
||||||
drawerProps={{
|
|
||||||
destroyOnHidden: true,
|
|
||||||
}}
|
|
||||||
initialValues={initialValues}
|
|
||||||
onValuesChange={async (changedValues) => {
|
|
||||||
// 当 Category 发生变化时
|
|
||||||
if ('categoryId' in changedValues) {
|
|
||||||
handleCategoryChange(changedValues.categoryId);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 当 SKU 发生变化时
|
|
||||||
if ('sku' in changedValues) {
|
|
||||||
const sku = changedValues.sku;
|
|
||||||
// 如果 sku 存在
|
|
||||||
if (sku) {
|
|
||||||
// 获取库存信息
|
|
||||||
const { data } = await getStocks({
|
|
||||||
sku: sku,
|
|
||||||
} as any);
|
|
||||||
// 如果库存信息存在且不为空
|
|
||||||
if (data && data.items && data.items.length > 0) {
|
|
||||||
// 设置产品类型为单品
|
|
||||||
formRef.current?.setFieldsValue({ type: 'single' });
|
|
||||||
} else {
|
|
||||||
// 设置产品类型为套装
|
|
||||||
formRef.current?.setFieldsValue({ type: 'bundle' });
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// 如果 sku 不存在,则重置状态
|
|
||||||
formRef.current?.setFieldsValue({ type: null });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onFinish={async (values) => {
|
|
||||||
// 组装 attributes
|
|
||||||
const attributes = activeAttributes.flatMap((attr: any) => {
|
|
||||||
const dictName = attr.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,
|
|
||||||
description: (values as any).description,
|
|
||||||
shortDescription: (values as any).shortDescription,
|
|
||||||
sku: (values as any).sku,
|
|
||||||
price: (values as any).price,
|
|
||||||
promotionPrice: (values as any).promotionPrice,
|
|
||||||
attributes,
|
|
||||||
type: values.type, // 直接使用 type
|
|
||||||
categoryId: values.categoryId,
|
|
||||||
siteSkus: values.siteSkus.map((v: { code: string }) => v.code) || [], // 直接传递字符串数组
|
|
||||||
// 连带更新 components
|
|
||||||
components:
|
|
||||||
values.type === 'bundle'
|
|
||||||
? (values.components || []).map((c: any) => ({
|
|
||||||
sku: c.sku,
|
|
||||||
quantity: Number(c.quantity),
|
|
||||||
}))
|
|
||||||
: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
const { success, message: errMsg } =
|
|
||||||
await productcontrollerUpdateproduct({ id: record.id }, payload);
|
|
||||||
|
|
||||||
if (success) {
|
|
||||||
message.success('提交成功');
|
|
||||||
tableRef.current?.reloadAndRest?.();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
message.error(errMsg);
|
|
||||||
return false;
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{/* {JSON.stringify(record)}
|
|
||||||
{JSON.stringify(initialValues)} */}
|
|
||||||
<ProForm.Group>
|
|
||||||
<ProFormText
|
|
||||||
name="sku"
|
|
||||||
label="SKU"
|
|
||||||
width="md"
|
|
||||||
placeholder="请输入SKU"
|
|
||||||
rules={[{ required: true, message: '请输入SKU' }]}
|
|
||||||
/>
|
|
||||||
{stockStatus && (
|
|
||||||
<Tag
|
|
||||||
style={{ marginTop: '32px' }}
|
|
||||||
color={stockStatus === 'in-stock' ? 'green' : 'orange'}
|
|
||||||
>
|
|
||||||
{stockStatus === 'in-stock' ? '在库' : '未在库'}
|
|
||||||
</Tag>
|
|
||||||
)}
|
|
||||||
</ProForm.Group>
|
|
||||||
<ProFormList
|
|
||||||
name="siteSkus"
|
|
||||||
label="站点SKU"
|
|
||||||
creatorButtonProps={{
|
|
||||||
position: 'bottom',
|
|
||||||
creatorButtonText: '新增站点SKU',
|
|
||||||
}}
|
|
||||||
itemRender={({ listDom, action }) => (
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
marginBottom: 8,
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'end',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{listDom}
|
|
||||||
{action}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<ProFormText
|
|
||||||
name="code"
|
|
||||||
width="md"
|
|
||||||
placeholder="请输入站点SKU"
|
|
||||||
rules={[{ required: true, message: '请输入站点SKU' }]}
|
|
||||||
/>
|
|
||||||
</ProFormList>
|
|
||||||
<ProForm.Group>
|
|
||||||
<ProFormText
|
|
||||||
name="name"
|
|
||||||
label="名称"
|
|
||||||
width="md"
|
|
||||||
placeholder="请输入名称"
|
|
||||||
rules={[{ required: true, message: '请输入名称' }]}
|
|
||||||
/>
|
|
||||||
</ProForm.Group>
|
|
||||||
<ProFormSelect
|
|
||||||
name="type"
|
|
||||||
label="产品类型"
|
|
||||||
options={[
|
|
||||||
{ value: 'single', label: '单品' },
|
|
||||||
{ value: 'bundle', label: '套装' },
|
|
||||||
]}
|
|
||||||
rules={[{ required: true, message: '请选择产品类型' }]}
|
|
||||||
/>
|
|
||||||
<ProForm.Item
|
|
||||||
shouldUpdate={(prevValues: any, curValues: any) =>
|
|
||||||
prevValues.type !== curValues.type
|
|
||||||
}
|
|
||||||
noStyle
|
|
||||||
>
|
|
||||||
{({ getFieldValue }: { getFieldValue: (name: string) => any }) =>
|
|
||||||
getFieldValue('type') === 'bundle' ? (
|
|
||||||
<ProFormList
|
|
||||||
name="components"
|
|
||||||
label="组成项"
|
|
||||||
creatorButtonProps={{
|
|
||||||
position: 'bottom',
|
|
||||||
creatorButtonText: '新增组成项',
|
|
||||||
}}
|
|
||||||
itemRender={({ listDom, action }) => (
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
marginBottom: 8,
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'end',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{listDom}
|
|
||||||
{action}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<ProForm.Group>
|
|
||||||
<ProFormSelect
|
|
||||||
name="sku"
|
|
||||||
label="单品SKU"
|
|
||||||
width="md"
|
|
||||||
showSearch
|
|
||||||
debounceTime={300}
|
|
||||||
placeholder="请输入单品SKU"
|
|
||||||
rules={[{ required: true, message: '请输入单品SKU' }]}
|
|
||||||
request={async ({ keyWords }) => {
|
|
||||||
const params = keyWords
|
|
||||||
? {
|
|
||||||
where: {
|
|
||||||
sku: keyWords,
|
|
||||||
name: keyWords,
|
|
||||||
type: 'single',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
: { per_page: 9999, where: { type: 'single' } };
|
|
||||||
const { data } = await productcontrollerGetproductlist(
|
|
||||||
params,
|
|
||||||
);
|
|
||||||
if (!data || !data.items) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
return data.items
|
|
||||||
.filter((item) => item.sku)
|
|
||||||
.map((item) => ({
|
|
||||||
label: `${item.sku} - ${item.name}`,
|
|
||||||
value: item.sku,
|
|
||||||
}));
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<ProFormText
|
|
||||||
name="quantity"
|
|
||||||
label="数量"
|
|
||||||
width="md"
|
|
||||||
placeholder="请输入数量"
|
|
||||||
rules={[{ required: true, message: '请输入数量' }]}
|
|
||||||
/>
|
|
||||||
</ProForm.Group>
|
|
||||||
</ProFormList>
|
|
||||||
) : null
|
|
||||||
}
|
|
||||||
</ProForm.Item>
|
|
||||||
|
|
||||||
<ProForm.Group>
|
|
||||||
<ProFormText
|
|
||||||
name="price"
|
|
||||||
label="价格"
|
|
||||||
width="md"
|
|
||||||
placeholder="请输入价格"
|
|
||||||
rules={[{ required: true, message: '请输入价格' }]}
|
|
||||||
/>
|
|
||||||
<ProFormText
|
|
||||||
name="promotionPrice"
|
|
||||||
label="促销价"
|
|
||||||
width="md"
|
|
||||||
placeholder="请输入促销价"
|
|
||||||
/>
|
|
||||||
</ProForm.Group>
|
|
||||||
|
|
||||||
<ProFormSelect
|
|
||||||
name="categoryId"
|
|
||||||
label="分类"
|
|
||||||
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.name}
|
|
||||||
name={`${attr.name}Values`}
|
|
||||||
label={attr.title}
|
|
||||||
isTag
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
<ProFormTextArea
|
|
||||||
name="shortDescription"
|
|
||||||
width="lg"
|
|
||||||
label="产品简短描述"
|
|
||||||
placeholder="请输入产品简短描述"
|
|
||||||
/>
|
|
||||||
<ProFormTextArea
|
|
||||||
name="description"
|
|
||||||
width="lg"
|
|
||||||
label="产品描述"
|
|
||||||
placeholder="请输入产品描述"
|
|
||||||
/>
|
|
||||||
</DrawerForm>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default EditForm;
|
|
||||||
|
|
@ -1,204 +0,0 @@
|
||||||
import { productcontrollerBatchsynctosite } from '@/servers/api/product';
|
|
||||||
import { sitecontrollerAll } from '@/servers/api/site';
|
|
||||||
import { templatecontrollerRendertemplate } from '@/servers/api/template';
|
|
||||||
import { showBatchOperationResult } from '@/utils/showResult';
|
|
||||||
import {
|
|
||||||
ModalForm,
|
|
||||||
ProFormDependency,
|
|
||||||
ProFormSelect,
|
|
||||||
ProFormText,
|
|
||||||
} from '@ant-design/pro-components';
|
|
||||||
import { App, Button, Tag } from 'antd';
|
|
||||||
import React, { useEffect, useRef, useState } from 'react';
|
|
||||||
|
|
||||||
interface SyncToSiteModalProps {
|
|
||||||
visible: boolean;
|
|
||||||
onClose: () => void;
|
|
||||||
products: API.Product[];
|
|
||||||
site?: any;
|
|
||||||
onSuccess: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const SyncToSiteModal: React.FC<SyncToSiteModalProps> = ({
|
|
||||||
visible,
|
|
||||||
onClose,
|
|
||||||
products,
|
|
||||||
site,
|
|
||||||
onSuccess,
|
|
||||||
}) => {
|
|
||||||
const { message } = App.useApp();
|
|
||||||
const [sites, setSites] = useState<any[]>([]);
|
|
||||||
const formRef = useRef<any>();
|
|
||||||
|
|
||||||
// 生成单个产品的站点SKU
|
|
||||||
const generateSingleSiteSku = async (
|
|
||||||
currentSite: API.Site,
|
|
||||||
product: API.Product,
|
|
||||||
): Promise<string> => {
|
|
||||||
try {
|
|
||||||
console.log('site', currentSite);
|
|
||||||
const { data: renderedSku } = await templatecontrollerRendertemplate(
|
|
||||||
{ name: 'site.product.sku' },
|
|
||||||
{ site: currentSite, product },
|
|
||||||
);
|
|
||||||
return (
|
|
||||||
renderedSku || `${currentSite.skuPrefix || ''}${product.sku || ''}`
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
return `${currentSite.skuPrefix || ''}${product.sku || ''}`;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 生成所有产品的站点SKU并设置到表单
|
|
||||||
const generateAndSetSiteSkus = async (currentSite: any) => {
|
|
||||||
const siteSkus: Record<string, string> = {};
|
|
||||||
for (const product of products) {
|
|
||||||
const siteSku = await generateSingleSiteSku(currentSite, product);
|
|
||||||
siteSkus[product.id] = siteSku;
|
|
||||||
}
|
|
||||||
// 设置表单值
|
|
||||||
formRef.current?.setFieldsValue({ siteSkus });
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (visible) {
|
|
||||||
sitecontrollerAll().then((res: any) => {
|
|
||||||
const siteList = res?.data || [];
|
|
||||||
setSites(siteList);
|
|
||||||
// 如果有站点列表,默认选择第一个站点或传入的site
|
|
||||||
const targetSite = site || (siteList.length > 0 ? siteList[0] : null);
|
|
||||||
if (targetSite) {
|
|
||||||
// 使用 setTimeout 确保 formRef 已经准备好
|
|
||||||
setTimeout(() => {
|
|
||||||
if (formRef.current) {
|
|
||||||
formRef.current.setFieldsValue({ siteId: targetSite.id });
|
|
||||||
// 自动生成所有产品的站点 SKU
|
|
||||||
generateAndSetSiteSkus(targetSite);
|
|
||||||
}
|
|
||||||
}, 0);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [visible, products, site]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ModalForm
|
|
||||||
title={`同步到站点 (${products.length} 项)`}
|
|
||||||
open={visible}
|
|
||||||
onOpenChange={(open) => !open && onClose()}
|
|
||||||
modalProps={{ destroyOnClose: true }}
|
|
||||||
formRef={formRef}
|
|
||||||
onValuesChange={async (changedValues) => {
|
|
||||||
if ('siteId' in changedValues && changedValues.siteId) {
|
|
||||||
const siteId = changedValues.siteId;
|
|
||||||
const currentSite = sites.find((s: any) => s.id === siteId) || {};
|
|
||||||
// 站点改变时,重新生成所有产品的站点SKU
|
|
||||||
generateAndSetSiteSkus(currentSite);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onFinish={async (values) => {
|
|
||||||
console.log(`values`, values);
|
|
||||||
if (!values.siteId) return false;
|
|
||||||
try {
|
|
||||||
const siteSkusMap = values.siteSkus || {};
|
|
||||||
const data = products.map((product) => ({
|
|
||||||
productId: product.id,
|
|
||||||
siteSku:
|
|
||||||
siteSkusMap[product.id] || `${values.siteId}-${product.sku}`,
|
|
||||||
}));
|
|
||||||
console.log(`data`, data);
|
|
||||||
const result = await productcontrollerBatchsynctosite({
|
|
||||||
siteId: values.siteId,
|
|
||||||
data,
|
|
||||||
});
|
|
||||||
|
|
||||||
showBatchOperationResult(result, '同步到站点');
|
|
||||||
onSuccess();
|
|
||||||
return true;
|
|
||||||
} catch (error: any) {
|
|
||||||
message.error(error.message || '同步失败');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ProFormSelect
|
|
||||||
name="siteId"
|
|
||||||
label="选择站点"
|
|
||||||
options={sites.map((site) => ({ label: site.name, value: site.id }))}
|
|
||||||
rules={[{ required: true, message: '请选择站点' }]}
|
|
||||||
/>
|
|
||||||
{products.map((row) => (
|
|
||||||
<ProFormDependency key={row.id} name={['siteId']}>
|
|
||||||
{({ siteId }) => (
|
|
||||||
<div style={{ marginBottom: 16 }}>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: 'flex',
|
|
||||||
gap: 8,
|
|
||||||
alignItems: 'center',
|
|
||||||
marginBottom: 8,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div style={{ minWidth: 220 }}>原始SKU: {row.sku || '-'}</div>
|
|
||||||
<div style={{ minWidth: 150 }}>
|
|
||||||
已有商品SKU:{' '}
|
|
||||||
{row.siteSkus && row.siteSkus.length > 0
|
|
||||||
? row.siteSkus.map((siteSku: string, idx: number) => (
|
|
||||||
<Tag key={idx} color="cyan">
|
|
||||||
{siteSku}
|
|
||||||
</Tag>
|
|
||||||
))
|
|
||||||
: '-'}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
|
|
||||||
<div style={{ flex: 1 }}>
|
|
||||||
<ProFormText
|
|
||||||
name={['siteSkus', row.id]}
|
|
||||||
label={`商品 ${row.sku} 站点SKU`}
|
|
||||||
placeholder="请输入站点SKU"
|
|
||||||
fieldProps={{
|
|
||||||
onChange: (e) => {
|
|
||||||
// 手动输入时更新表单值
|
|
||||||
const currentValues =
|
|
||||||
formRef.current?.getFieldValue('siteSkus') || {};
|
|
||||||
currentValues[row.id] = e.target.value;
|
|
||||||
formRef.current?.setFieldsValue({
|
|
||||||
siteSkus: currentValues,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
type="primary"
|
|
||||||
size="small"
|
|
||||||
onClick={async () => {
|
|
||||||
if (siteId) {
|
|
||||||
const currentSite =
|
|
||||||
sites.find((s: any) => s.id === siteId) || {};
|
|
||||||
const siteSku = await generateSingleSiteSku(
|
|
||||||
currentSite,
|
|
||||||
row,
|
|
||||||
);
|
|
||||||
const currentValues =
|
|
||||||
formRef.current?.getFieldValue('siteSkus') || {};
|
|
||||||
currentValues[row.id] = siteSku;
|
|
||||||
formRef.current?.setFieldsValue({
|
|
||||||
siteSkus: currentValues,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
自动生成
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</ProFormDependency>
|
|
||||||
))}
|
|
||||||
</ModalForm>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default SyncToSiteModal;
|
|
||||||
|
|
@ -1,53 +0,0 @@
|
||||||
import React from "react";
|
|
||||||
import { ProTable, ProColumns } from "@ant-design/pro-components";
|
|
||||||
|
|
||||||
interface ProductComponentListProps {
|
|
||||||
record: API.Product;
|
|
||||||
columns: ProColumns<API.Product>[];
|
|
||||||
dataSource?: API.Product[];
|
|
||||||
}
|
|
||||||
|
|
||||||
const ProductComponentList: React.FC<ProductComponentListProps> = ({ record, columns, dataSource }) => {
|
|
||||||
if (record.type !== "bundle" || !record.components || record.components.length === 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const componentSkus = record.components.map(component => component.sku);
|
|
||||||
|
|
||||||
const includedProducts = [];
|
|
||||||
|
|
||||||
if (dataSource) {
|
|
||||||
includedProducts = dataSource
|
|
||||||
.filter(product => product.type === "single" && componentSkus.includes(product.sku));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (includedProducts.length === 0) {
|
|
||||||
return (
|
|
||||||
<div style={{ padding: "16px", textAlign: "center", color: "#999" }}>
|
|
||||||
未找到包含的单品信息
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const componentColumns = columns.filter(col =>
|
|
||||||
[200~cd ../api"option", "siteSkus", "category", "type"].includes(col.dataIndex as string)
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div style={{ padding: "8px 16px", backgroundColor: "#fafafa" }}>
|
|
||||||
<ProTable
|
|
||||||
dataSource={includedProducts}
|
|
||||||
columns={componentColumns}
|
|
||||||
pagination={false}
|
|
||||||
rowKey="id"
|
|
||||||
bordered
|
|
||||||
size="small"
|
|
||||||
scroll={{ x: "max-content" }}
|
|
||||||
headerTitle={null}
|
|
||||||
toolBarRender={false}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ProductComponentList;
|
|
||||||
|
|
@ -1,249 +1,57 @@
|
||||||
import {
|
import {
|
||||||
productcontrollerBatchdeleteproduct,
|
productcontrollerCreateproduct,
|
||||||
productcontrollerBatchupdateproduct,
|
|
||||||
productcontrollerDeleteproduct,
|
productcontrollerDeleteproduct,
|
||||||
productcontrollerGetcategoriesall,
|
productcontrollerGetcategorieall,
|
||||||
|
productcontrollerGetflavorsall,
|
||||||
productcontrollerGetproductlist,
|
productcontrollerGetproductlist,
|
||||||
productcontrollerUpdatenamecn,
|
productcontrollerGetstrengthall,
|
||||||
|
productcontrollerUpdateproductnamecn,
|
||||||
} from '@/servers/api/product';
|
} from '@/servers/api/product';
|
||||||
|
import { PlusOutlined } from '@ant-design/icons';
|
||||||
import {
|
import {
|
||||||
ActionType,
|
ActionType,
|
||||||
ModalForm,
|
DrawerForm,
|
||||||
PageContainer,
|
PageContainer,
|
||||||
ProColumns,
|
ProColumns,
|
||||||
|
ProForm,
|
||||||
ProFormSelect,
|
ProFormSelect,
|
||||||
ProFormText,
|
ProFormText,
|
||||||
|
ProFormTextArea,
|
||||||
ProTable,
|
ProTable,
|
||||||
} from '@ant-design/pro-components';
|
} from '@ant-design/pro-components';
|
||||||
import { request } from '@umijs/max';
|
import { App, Button, Popconfirm } from 'antd';
|
||||||
import { App, Button, Modal, Popconfirm, Tag, Upload } from 'antd';
|
import React, { useRef } from 'react';
|
||||||
import React, { useEffect, useRef, useState } from 'react';
|
|
||||||
import CreateForm from './CreateForm';
|
|
||||||
import EditForm from './EditForm';
|
|
||||||
import SyncToSiteModal from './SyncToSiteModal';
|
|
||||||
|
|
||||||
const NameCn: React.FC<{
|
const NameCn: React.FC<{
|
||||||
id: number;
|
id: number;
|
||||||
value: string | undefined;
|
value: string;
|
||||||
tableRef: React.MutableRefObject<ActionType | undefined>;
|
tableRef: React.MutableRefObject<ActionType | undefined>;
|
||||||
}> = ({ value, tableRef, id }) => {
|
}> = ({value,tableRef, id}) => {
|
||||||
const { message } = App.useApp();
|
const { message } = App.useApp();
|
||||||
const [editable, setEditable] = React.useState<boolean>(false);
|
const [editable, setEditable] = React.useState<boolean>(false);
|
||||||
if (!editable)
|
if (!editable) return <div onClick={() => setEditable(true)}>{value||'-'}</div>;
|
||||||
return <div onClick={() => setEditable(true)}>{value || '-'}</div>;
|
return <ProFormText fieldProps={{autoFocus:true}} initialValue={value} onBlur={async(e) => {
|
||||||
return (
|
if(!e.target.value) return setEditable(false)
|
||||||
<ProFormText
|
|
||||||
initialValue={value}
|
|
||||||
fieldProps={{
|
|
||||||
autoFocus: true,
|
|
||||||
onBlur: async (e: React.FocusEvent<HTMLInputElement>) => {
|
|
||||||
if (!e.target.value) return setEditable(false);
|
|
||||||
const { success, message: errMsg } =
|
const { success, message: errMsg } =
|
||||||
await productcontrollerUpdatenamecn({
|
await productcontrollerUpdateproductnamecn({
|
||||||
id,
|
id,
|
||||||
nameCn: e.target.value,
|
nameCn: e.target.value,
|
||||||
});
|
})
|
||||||
setEditable(false);
|
setEditable(false)
|
||||||
if (!success) {
|
if (!success) {
|
||||||
return message.error(errMsg);
|
return message.error(errMsg)
|
||||||
}
|
}
|
||||||
tableRef?.current?.reloadAndRest?.();
|
tableRef?.current?.reload()
|
||||||
},
|
}} />
|
||||||
}}
|
}
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const AttributesCell: React.FC<{ record: any }> = ({ record }) => {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
{(record.attributes || []).map((data: any, idx: number) => (
|
|
||||||
<Tag key={idx} color="purple" style={{ marginBottom: 4 }}>
|
|
||||||
{data?.dict?.name}: {data.name}
|
|
||||||
</Tag>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const ComponentsCell: React.FC<{ components?: any[] }> = ({ components }) => {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
{components && components.length ? (
|
|
||||||
components.map((component: any) => (
|
|
||||||
<Tag key={component.id} color="blue" style={{ marginBottom: 4 }}>
|
|
||||||
{component.sku || `#${component.id}`} × {component.quantity}
|
|
||||||
(库存:
|
|
||||||
{component.stock
|
|
||||||
?.map((s: any) => `${s.name}:${s.quantity}`)
|
|
||||||
.join(', ') || '-'}
|
|
||||||
)
|
|
||||||
</Tag>
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<span>-</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const BatchEditModal: React.FC<{
|
|
||||||
visible: boolean;
|
|
||||||
onClose: () => void;
|
|
||||||
selectedRows: API.Product[];
|
|
||||||
tableRef: React.MutableRefObject<ActionType | undefined>;
|
|
||||||
onSuccess: () => void;
|
|
||||||
}> = ({ visible, onClose, selectedRows, tableRef, onSuccess }) => {
|
|
||||||
const { message } = App.useApp();
|
|
||||||
const [categories, setCategories] = useState<any[]>([]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (visible) {
|
|
||||||
productcontrollerGetcategoriesall().then((res: any) => {
|
|
||||||
setCategories(res?.data || []);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [visible]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ModalForm
|
|
||||||
title={`批量修改 (${selectedRows.length} 项)`}
|
|
||||||
open={visible}
|
|
||||||
onOpenChange={(open) => !open && onClose()}
|
|
||||||
modalProps={{ destroyOnClose: true }}
|
|
||||||
onFinish={async (values) => {
|
|
||||||
const ids = selectedRows.map((row) => row.id);
|
|
||||||
const updateData: any = { ids };
|
|
||||||
// 只有当用户输入了值才进行更新
|
|
||||||
if (values.price) updateData.price = Number(values.price);
|
|
||||||
if (values.promotionPrice)
|
|
||||||
updateData.promotionPrice = Number(values.promotionPrice);
|
|
||||||
if (values.categoryId) updateData.categoryId = values.categoryId;
|
|
||||||
|
|
||||||
if (Object.keys(updateData).length <= 1) {
|
|
||||||
message.warning('未修改任何属性');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { success, message: errMsg } =
|
|
||||||
await productcontrollerBatchupdateproduct(updateData);
|
|
||||||
if (success) {
|
|
||||||
message.success('批量修改成功');
|
|
||||||
onSuccess();
|
|
||||||
tableRef.current?.reload();
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
message.error(errMsg);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ProFormText name="price" label="价格" placeholder="不修改请留空" />
|
|
||||||
<ProFormText
|
|
||||||
name="promotionPrice"
|
|
||||||
label="促销价格"
|
|
||||||
placeholder="不修改请留空"
|
|
||||||
/>
|
|
||||||
<ProFormSelect
|
|
||||||
name="categoryId"
|
|
||||||
label="分类"
|
|
||||||
options={categories.map((c) => ({ label: c.title, value: c.id }))}
|
|
||||||
placeholder="不修改请留空"
|
|
||||||
/>
|
|
||||||
</ModalForm>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
const ProductList = ({
|
|
||||||
filter,
|
|
||||||
columns,
|
|
||||||
}: {
|
|
||||||
filter: { skus: string[] };
|
|
||||||
columns: any[];
|
|
||||||
}) => {
|
|
||||||
return (
|
|
||||||
<ProTable
|
|
||||||
request={async (pag) => {
|
|
||||||
const { data, success } = await productcontrollerGetproductlist({
|
|
||||||
where: filter,
|
|
||||||
});
|
|
||||||
if (!success) return [];
|
|
||||||
return data || [];
|
|
||||||
}}
|
|
||||||
columns={columns}
|
|
||||||
pagination={false}
|
|
||||||
rowKey="id"
|
|
||||||
bordered
|
|
||||||
size="small"
|
|
||||||
scroll={{ x: 'max-content' }}
|
|
||||||
headerTitle={null}
|
|
||||||
toolBarRender={false}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const List: React.FC = () => {
|
const List: React.FC = () => {
|
||||||
const actionRef = useRef<ActionType>();
|
const actionRef = useRef<ActionType>();
|
||||||
// 状态:存储当前选中的行
|
|
||||||
const [selectedRows, setSelectedRows] = React.useState<API.Product[]>([]);
|
|
||||||
const [batchEditModalVisible, setBatchEditModalVisible] = useState(false);
|
|
||||||
const [syncProducts, setSyncProducts] = useState<API.Product[]>([]);
|
|
||||||
const [syncModalVisible, setSyncModalVisible] = useState(false);
|
|
||||||
|
|
||||||
const { message } = App.useApp();
|
const { message } = App.useApp();
|
||||||
// 导出产品 CSV(带认证请求)
|
|
||||||
const handleDownloadProductsCSV = async () => {
|
|
||||||
try {
|
|
||||||
// 发起认证请求获取 CSV Blob
|
|
||||||
const blob = await request('/product/export', { responseType: 'blob' });
|
|
||||||
// 构建下载文件名
|
|
||||||
const d = new Date();
|
|
||||||
const pad = (n: number) => String(n).padStart(2, '0');
|
|
||||||
const filename = `products-${d.getFullYear()}${pad(
|
|
||||||
d.getMonth() + 1,
|
|
||||||
)}${pad(d.getDate())}.csv`;
|
|
||||||
// 创建临时链接并触发下载
|
|
||||||
const url = window.URL.createObjectURL(blob);
|
|
||||||
const a = document.createElement('a');
|
|
||||||
a.href = url;
|
|
||||||
a.download = filename;
|
|
||||||
document.body.appendChild(a);
|
|
||||||
a.click();
|
|
||||||
a.remove();
|
|
||||||
window.URL.revokeObjectURL(url);
|
|
||||||
} catch (error) {
|
|
||||||
message.error('导出失败');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const columns: ProColumns<API.Product>[] = [
|
const columns: ProColumns<API.Product>[] = [
|
||||||
{
|
|
||||||
title: 'sku',
|
|
||||||
dataIndex: 'sku',
|
|
||||||
sorter: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '关联商品',
|
|
||||||
dataIndex: 'siteSkus',
|
|
||||||
width: 200,
|
|
||||||
render: (_, record) => (
|
|
||||||
<>
|
|
||||||
{record.siteSkus?.map((siteSku, index) => (
|
|
||||||
<Tag key={index} color="cyan">
|
|
||||||
{siteSku}
|
|
||||||
</Tag>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '图片',
|
|
||||||
dataIndex: 'image',
|
|
||||||
width: 100,
|
|
||||||
valueType: 'image',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
title: '名称',
|
title: '名称',
|
||||||
dataIndex: 'name',
|
dataIndex: 'name',
|
||||||
sorter: true,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '中文名',
|
title: '中文名',
|
||||||
|
|
@ -251,102 +59,56 @@ const List: React.FC = () => {
|
||||||
render: (_, record) => {
|
render: (_, record) => {
|
||||||
return (
|
return (
|
||||||
<NameCn value={record.nameCn} id={record.id} tableRef={actionRef} />
|
<NameCn value={record.nameCn} id={record.id} tableRef={actionRef} />
|
||||||
);
|
)
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
title: '价格',
|
|
||||||
dataIndex: 'price',
|
|
||||||
hideInSearch: true,
|
|
||||||
sorter: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '促销价',
|
|
||||||
dataIndex: 'promotionPrice',
|
|
||||||
hideInSearch: true,
|
|
||||||
sorter: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '商品类型',
|
|
||||||
dataIndex: 'category',
|
|
||||||
render: (_, record: any) => {
|
|
||||||
return record.category?.title || record.category?.name || '-';
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '属性',
|
title: '产品描述',
|
||||||
dataIndex: 'attributes',
|
|
||||||
hideInSearch: true,
|
|
||||||
render: (_, record) => <AttributesCell record={record} />,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '产品类型',
|
|
||||||
dataIndex: 'type',
|
|
||||||
valueType: 'select',
|
|
||||||
valueEnum: {
|
|
||||||
single: { text: '单品' },
|
|
||||||
bundle: { text: '套装' },
|
|
||||||
},
|
|
||||||
render: (_, record) => {
|
|
||||||
// 如果类型不存在,则返回-
|
|
||||||
if (!record.type) return '-';
|
|
||||||
// 判断是否为单品
|
|
||||||
const isSingle = record.type === 'single';
|
|
||||||
// 根据类型显示不同颜色的标签
|
|
||||||
return (
|
|
||||||
<Tag color={isSingle ? 'green' : 'orange'}>
|
|
||||||
{isSingle ? '单品' : '套装'}
|
|
||||||
</Tag>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '构成',
|
|
||||||
dataIndex: 'components',
|
|
||||||
hideInSearch: true,
|
|
||||||
render: (_, record) => <ComponentsCell components={record.components} />,
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
title: '描述',
|
|
||||||
dataIndex: 'description',
|
dataIndex: 'description',
|
||||||
hideInSearch: true,
|
hideInSearch: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: '产品分类',
|
||||||
|
dataIndex: 'categoryName',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '强度',
|
||||||
|
dataIndex: 'strengthName',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '口味',
|
||||||
|
dataIndex: 'flavorsName',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '湿度',
|
||||||
|
dataIndex: 'humidity',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'sku',
|
||||||
|
dataIndex: 'sku',
|
||||||
|
hideInSearch: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: '更新时间',
|
title: '更新时间',
|
||||||
dataIndex: 'updatedAt',
|
dataIndex: 'updatedAt',
|
||||||
valueType: 'dateTime',
|
valueType: 'dateTime',
|
||||||
hideInSearch: true,
|
hideInSearch: true,
|
||||||
sorter: true,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '创建时间',
|
title: '创建时间',
|
||||||
dataIndex: 'createdAt',
|
dataIndex: 'createdAt',
|
||||||
valueType: 'dateTime',
|
valueType: 'dateTime',
|
||||||
hideInSearch: true,
|
hideInSearch: true,
|
||||||
sorter: true,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '操作',
|
title: '操作',
|
||||||
dataIndex: 'option',
|
dataIndex: 'option',
|
||||||
valueType: 'option',
|
valueType: 'option',
|
||||||
fixed: 'right',
|
|
||||||
render: (_, record) => (
|
render: (_, record) => (
|
||||||
<>
|
<>
|
||||||
<EditForm record={record} tableRef={actionRef} />
|
|
||||||
<Button
|
|
||||||
type="link"
|
|
||||||
onClick={() => {
|
|
||||||
setSyncProducts([record]);
|
|
||||||
setSyncModalVisible(true);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
同步到站点
|
|
||||||
</Button>
|
|
||||||
<Popconfirm
|
<Popconfirm
|
||||||
title="删除"
|
title="删除"
|
||||||
description="确认删除?"
|
description="确认删除?"
|
||||||
onConfirm={async () => {
|
onConfirm={async () => {
|
||||||
try {
|
try {
|
||||||
const { success, message: errMsg } =
|
const { success, message: errMsg } =
|
||||||
|
|
@ -360,7 +122,7 @@ const List: React.FC = () => {
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button type="link" danger>
|
<Button type="primary" danger>
|
||||||
删除
|
删除
|
||||||
</Button>
|
</Button>
|
||||||
</Popconfirm>
|
</Popconfirm>
|
||||||
|
|
@ -372,158 +134,14 @@ const List: React.FC = () => {
|
||||||
return (
|
return (
|
||||||
<PageContainer header={{ title: '产品列表' }}>
|
<PageContainer header={{ title: '产品列表' }}>
|
||||||
<ProTable<API.Product>
|
<ProTable<API.Product>
|
||||||
scroll={{ x: 'max-content' }}
|
|
||||||
headerTitle="查询表格"
|
headerTitle="查询表格"
|
||||||
actionRef={actionRef}
|
actionRef={actionRef}
|
||||||
rowKey="id"
|
rowKey="id"
|
||||||
toolBarRender={() => [
|
toolBarRender={() => [<CreateForm tableRef={actionRef} />]}
|
||||||
// 新建按钮
|
request={async (params) => {
|
||||||
<CreateForm tableRef={actionRef} />,
|
const { data, success } = await productcontrollerGetproductlist(
|
||||||
// 导入 CSV(使用 customRequest 以支持 request 拦截器和鉴权)
|
params,
|
||||||
<Upload
|
);
|
||||||
name="file"
|
|
||||||
accept=".csv"
|
|
||||||
showUploadList={false}
|
|
||||||
maxCount={1}
|
|
||||||
customRequest={async (options) => {
|
|
||||||
const { file, onSuccess, onError } = options;
|
|
||||||
const formData = new FormData();
|
|
||||||
formData.append('file', file);
|
|
||||||
try {
|
|
||||||
const res = await request('/product/import', {
|
|
||||||
method: 'POST',
|
|
||||||
data: formData,
|
|
||||||
requestType: 'form',
|
|
||||||
});
|
|
||||||
|
|
||||||
const {
|
|
||||||
created = 0,
|
|
||||||
updated = 0,
|
|
||||||
errors = [],
|
|
||||||
} = res.data || {};
|
|
||||||
|
|
||||||
if (errors && errors.length > 0) {
|
|
||||||
Modal.warning({
|
|
||||||
title: '导入结果 (存在错误)',
|
|
||||||
width: 600,
|
|
||||||
content: (
|
|
||||||
<div>
|
|
||||||
<p>创建成功: {created}</p>
|
|
||||||
<p>更新成功: {updated}</p>
|
|
||||||
<p style={{ color: 'red', fontWeight: 'bold' }}>
|
|
||||||
失败数量: {errors.length}
|
|
||||||
</p>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
maxHeight: '300px',
|
|
||||||
overflowY: 'auto',
|
|
||||||
background: '#f5f5f5',
|
|
||||||
padding: '8px',
|
|
||||||
marginTop: '8px',
|
|
||||||
borderRadius: '4px',
|
|
||||||
border: '1px solid #d9d9d9',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{errors.map((err: string, idx: number) => (
|
|
||||||
<div
|
|
||||||
key={idx}
|
|
||||||
style={{
|
|
||||||
fontSize: '12px',
|
|
||||||
marginBottom: '4px',
|
|
||||||
borderBottom: '1px solid #e8e8e8',
|
|
||||||
paddingBottom: '2px',
|
|
||||||
color: '#ff4d4f',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{idx + 1}. {err}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
message.success(`导入成功: 创建 ${created}, 更新 ${updated}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
onSuccess?.('ok');
|
|
||||||
actionRef.current?.reload();
|
|
||||||
} catch (error: any) {
|
|
||||||
message.error('导入失败: ' + (error.message || '未知错误'));
|
|
||||||
onError?.(error);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button>批量导入</Button>
|
|
||||||
</Upload>,
|
|
||||||
// 批量编辑按钮
|
|
||||||
<Button
|
|
||||||
disabled={selectedRows.length <= 0}
|
|
||||||
onClick={() => setBatchEditModalVisible(true)}
|
|
||||||
>
|
|
||||||
批量修改
|
|
||||||
</Button>,
|
|
||||||
// 批量同步按钮
|
|
||||||
<Button
|
|
||||||
disabled={selectedRows.length <= 0}
|
|
||||||
onClick={() => {
|
|
||||||
setSyncProducts(selectedRows);
|
|
||||||
setSyncModalVisible(true);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
批量同步到站点
|
|
||||||
</Button>,
|
|
||||||
// 批量删除按钮
|
|
||||||
<Button
|
|
||||||
danger
|
|
||||||
disabled={selectedRows.length <= 0}
|
|
||||||
onClick={() => {
|
|
||||||
Modal.confirm({
|
|
||||||
title: '确认删除',
|
|
||||||
content: `确定要删除选中的 ${selectedRows.length} 个产品吗?此操作不可恢复。`,
|
|
||||||
onOk: async () => {
|
|
||||||
try {
|
|
||||||
const { success, message: errMsg } =
|
|
||||||
await productcontrollerBatchdeleteproduct({
|
|
||||||
ids: selectedRows.map((row) => row.id),
|
|
||||||
});
|
|
||||||
if (success) {
|
|
||||||
message.success('批量删除成功');
|
|
||||||
setSelectedRows([]);
|
|
||||||
actionRef.current?.reload();
|
|
||||||
} else {
|
|
||||||
message.error(errMsg || '删除失败');
|
|
||||||
}
|
|
||||||
} catch (error: any) {
|
|
||||||
message.error(error.message || '删除失败');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
批量删除
|
|
||||||
</Button>,
|
|
||||||
// 导出 CSV(后端返回 text/csv,直接新窗口下载)
|
|
||||||
<Button onClick={handleDownloadProductsCSV}>导出CSV</Button>,
|
|
||||||
]}
|
|
||||||
request={async (params, sort) => {
|
|
||||||
let sortField = undefined;
|
|
||||||
let sortOrder = undefined;
|
|
||||||
|
|
||||||
if (sort && Object.keys(sort).length > 0) {
|
|
||||||
const field = Object.keys(sort)[0];
|
|
||||||
sortField = field;
|
|
||||||
sortOrder = sort[field];
|
|
||||||
}
|
|
||||||
const { current, pageSize, ...where } = params;
|
|
||||||
console.log(`params`, params);
|
|
||||||
const { data, success } = await productcontrollerGetproductlist({
|
|
||||||
where,
|
|
||||||
page: current || 1,
|
|
||||||
per_page: pageSize || 10,
|
|
||||||
sortField,
|
|
||||||
sortOrder,
|
|
||||||
} as any);
|
|
||||||
return {
|
return {
|
||||||
total: data?.total || 0,
|
total: data?.total || 0,
|
||||||
data: data?.items || [],
|
data: data?.items || [],
|
||||||
|
|
@ -531,18 +149,6 @@ const List: React.FC = () => {
|
||||||
};
|
};
|
||||||
}}
|
}}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
// expandable={{
|
|
||||||
// expandedRowRender: (record) => {
|
|
||||||
// return <ProductList filter={{
|
|
||||||
// skus: record.components?.map(component => component.sku) || [],
|
|
||||||
// }}
|
|
||||||
// columns={columns}
|
|
||||||
// ></ProductList>
|
|
||||||
// }
|
|
||||||
// ,
|
|
||||||
// rowExpandable: (record) =>
|
|
||||||
// !!(record.type==='bundle'),
|
|
||||||
// }}
|
|
||||||
editable={{
|
editable={{
|
||||||
type: 'single',
|
type: 'single',
|
||||||
onSave: async (key, record, originRow) => {
|
onSave: async (key, record, originRow) => {
|
||||||
|
|
@ -552,34 +158,113 @@ const List: React.FC = () => {
|
||||||
rowSelection={{
|
rowSelection={{
|
||||||
onChange: (_, selectedRows) => setSelectedRows(selectedRows),
|
onChange: (_, selectedRows) => setSelectedRows(selectedRows),
|
||||||
}}
|
}}
|
||||||
pagination={{
|
|
||||||
showSizeChanger: true,
|
|
||||||
showQuickJumper: true,
|
|
||||||
pageSizeOptions: ['10', '20', '50', '100', '1000', '2000'],
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<BatchEditModal
|
|
||||||
visible={batchEditModalVisible}
|
|
||||||
onClose={() => setBatchEditModalVisible(false)}
|
|
||||||
selectedRows={selectedRows}
|
|
||||||
tableRef={actionRef}
|
|
||||||
onSuccess={() => {
|
|
||||||
setBatchEditModalVisible(false);
|
|
||||||
setSelectedRows([]);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<SyncToSiteModal
|
|
||||||
visible={syncModalVisible}
|
|
||||||
onClose={() => setSyncModalVisible(false)}
|
|
||||||
products={syncProducts}
|
|
||||||
onSuccess={() => {
|
|
||||||
setSyncModalVisible(false);
|
|
||||||
setSelectedRows([]);
|
|
||||||
actionRef.current?.reload();
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</PageContainer>
|
</PageContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const CreateForm: React.FC<{
|
||||||
|
tableRef: React.MutableRefObject<ActionType | undefined>;
|
||||||
|
}> = ({ tableRef }) => {
|
||||||
|
const { message } = App.useApp();
|
||||||
|
return (
|
||||||
|
<DrawerForm<API.CreateProductDTO>
|
||||||
|
title="新建"
|
||||||
|
trigger={
|
||||||
|
<Button type="primary">
|
||||||
|
<PlusOutlined />
|
||||||
|
新建
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
autoFocusFirstInput
|
||||||
|
drawerProps={{
|
||||||
|
destroyOnHidden: true,
|
||||||
|
}}
|
||||||
|
onFinish={async (values) => {
|
||||||
|
try {
|
||||||
|
const { success, message: errMsg } =
|
||||||
|
await productcontrollerCreateproduct(values);
|
||||||
|
if (!success) {
|
||||||
|
throw new Error(errMsg);
|
||||||
|
}
|
||||||
|
tableRef.current?.reload();
|
||||||
|
message.success('提交成功');
|
||||||
|
return true;
|
||||||
|
} catch (error: any) {
|
||||||
|
message.error(error.message);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ProForm.Group>
|
||||||
|
<ProFormText
|
||||||
|
name="name"
|
||||||
|
label="名称"
|
||||||
|
width="lg"
|
||||||
|
placeholder="请输入名称"
|
||||||
|
rules={[{ required: true, message: '请输入名称' }]}
|
||||||
|
/>
|
||||||
|
<ProFormTextArea
|
||||||
|
name="description"
|
||||||
|
width="lg"
|
||||||
|
label="产品描述"
|
||||||
|
placeholder="请输入产品描述"
|
||||||
|
/>
|
||||||
|
<ProFormSelect
|
||||||
|
name="categoryId"
|
||||||
|
width="lg"
|
||||||
|
label="产品分类"
|
||||||
|
placeholder="请选择产品分类"
|
||||||
|
request={async () => {
|
||||||
|
const { data = [] } = await productcontrollerGetcategorieall();
|
||||||
|
return data.map((item) => ({
|
||||||
|
label: item.name,
|
||||||
|
value: item.id,
|
||||||
|
}));
|
||||||
|
}}
|
||||||
|
rules={[{ required: true, message: '请选择产品分类' }]}
|
||||||
|
/>
|
||||||
|
<ProFormSelect
|
||||||
|
name="strengthId"
|
||||||
|
width="lg"
|
||||||
|
label="强度"
|
||||||
|
placeholder="请选择强度"
|
||||||
|
request={async () => {
|
||||||
|
const { data = [] } = await productcontrollerGetstrengthall();
|
||||||
|
return data.map((item) => ({
|
||||||
|
label: item.name,
|
||||||
|
value: item.id,
|
||||||
|
}));
|
||||||
|
}}
|
||||||
|
rules={[{ required: true, message: '请选择强度' }]}
|
||||||
|
/>
|
||||||
|
<ProFormSelect
|
||||||
|
name="flavorsId"
|
||||||
|
width="lg"
|
||||||
|
label="口味"
|
||||||
|
placeholder="请选择口味"
|
||||||
|
request={async () => {
|
||||||
|
const { data = [] } = await productcontrollerGetflavorsall();
|
||||||
|
return data.map((item) => ({
|
||||||
|
label: item.name,
|
||||||
|
value: item.id,
|
||||||
|
}));
|
||||||
|
}}
|
||||||
|
rules={[{ required: true, message: '请选择口味' }]}
|
||||||
|
/>
|
||||||
|
<ProFormSelect
|
||||||
|
name="humidity"
|
||||||
|
width="lg"
|
||||||
|
label="干湿"
|
||||||
|
placeholder="请选择干湿"
|
||||||
|
valueEnum={{
|
||||||
|
dry: '干',
|
||||||
|
wet: '湿',
|
||||||
|
}}
|
||||||
|
rules={[{ required: true, message: '请选择干湿' }]}
|
||||||
|
/>
|
||||||
|
</ProForm.Group>
|
||||||
|
</DrawerForm>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export default List;
|
export default List;
|
||||||
|
|
|
||||||
|
|
@ -1,162 +0,0 @@
|
||||||
import { productcontrollerCreateproduct } from '@/servers/api/product';
|
|
||||||
import { templatecontrollerRendertemplate } from '@/servers/api/template';
|
|
||||||
import { ModalForm, ProFormText } from '@ant-design/pro-components';
|
|
||||||
import { App, Descriptions, Form, Tag } from 'antd';
|
|
||||||
import React, { useEffect } from 'react';
|
|
||||||
|
|
||||||
interface CreateModalProps {
|
|
||||||
visible: boolean;
|
|
||||||
onClose: () => void;
|
|
||||||
onSuccess: () => void;
|
|
||||||
category: { id: number; name: string } | null;
|
|
||||||
permutation: Record<string, any>;
|
|
||||||
attributes: any[]; // The attribute definitions
|
|
||||||
}
|
|
||||||
|
|
||||||
const capitalize = (s: string) => s.charAt(0).toLocaleUpperCase() + s.slice(1);
|
|
||||||
|
|
||||||
const CreateModal: React.FC<CreateModalProps> = ({
|
|
||||||
visible,
|
|
||||||
onClose,
|
|
||||||
onSuccess,
|
|
||||||
category,
|
|
||||||
permutation,
|
|
||||||
attributes,
|
|
||||||
}) => {
|
|
||||||
const { message } = App.useApp();
|
|
||||||
const [form] = Form.useForm();
|
|
||||||
|
|
||||||
// Helper to generate default name based on attributes
|
|
||||||
const generateDefaultName = () => {
|
|
||||||
if (!category) return '';
|
|
||||||
const parts = [category.name];
|
|
||||||
attributes.forEach((attr) => {
|
|
||||||
const val = permutation[attr.name];
|
|
||||||
if (val) parts.push(val.name);
|
|
||||||
});
|
|
||||||
return parts.join(' - ');
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (visible && permutation) {
|
|
||||||
const generateSku = async () => {
|
|
||||||
try {
|
|
||||||
// Extract values from permutation based on known keys
|
|
||||||
// Keys in permutation are dict names (e.g. 'brand', 'strength')
|
|
||||||
const brand = permutation['brand']?.name || '';
|
|
||||||
const strength = permutation['strength']?.name || '';
|
|
||||||
const flavor = permutation['flavor']?.name || '';
|
|
||||||
const humidity = permutation['humidity']?.name || '';
|
|
||||||
const model = permutation['model']?.name || '';
|
|
||||||
|
|
||||||
const variables = {
|
|
||||||
brand,
|
|
||||||
strength,
|
|
||||||
flavor,
|
|
||||||
model,
|
|
||||||
humidity: humidity ? capitalize(humidity) : '',
|
|
||||||
};
|
|
||||||
|
|
||||||
const { success, data: rendered } =
|
|
||||||
await templatecontrollerRendertemplate(
|
|
||||||
{ name: 'product.sku' },
|
|
||||||
variables,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (success && rendered) {
|
|
||||||
form.setFieldValue('sku', rendered);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to generate SKU', error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
generateSku();
|
|
||||||
form.setFieldValue('name', generateDefaultName());
|
|
||||||
}
|
|
||||||
}, [visible, permutation, category]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ModalForm
|
|
||||||
title="创建产品"
|
|
||||||
open={visible}
|
|
||||||
form={form}
|
|
||||||
modalProps={{
|
|
||||||
onCancel: onClose,
|
|
||||||
destroyOnClose: true,
|
|
||||||
}}
|
|
||||||
onFinish={async (values) => {
|
|
||||||
if (!category) return false;
|
|
||||||
|
|
||||||
// Construct attributes payload
|
|
||||||
// Expected format: [{ dictName: 'Size', name: 'S' }, ...]
|
|
||||||
const payloadAttributes = attributes
|
|
||||||
.filter((attr) => permutation[attr.name])
|
|
||||||
.map((attr) => ({
|
|
||||||
dictName: attr.name,
|
|
||||||
name: permutation[attr.name].name,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const payload = {
|
|
||||||
name: values.name,
|
|
||||||
sku: values.sku,
|
|
||||||
categoryId: category.id,
|
|
||||||
attributes: payloadAttributes,
|
|
||||||
type: 'single', // Default to single
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
|
||||||
const { success, message: errMsg } =
|
|
||||||
await productcontrollerCreateproduct(payload as any);
|
|
||||||
if (success) {
|
|
||||||
message.success('产品创建成功');
|
|
||||||
onSuccess();
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
message.error(errMsg || '创建产品失败');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
message.error('发生错误');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Descriptions
|
|
||||||
column={1}
|
|
||||||
bordered
|
|
||||||
size="small"
|
|
||||||
style={{ marginBottom: 24 }}
|
|
||||||
>
|
|
||||||
<Descriptions.Item label="分类">{category?.name}</Descriptions.Item>
|
|
||||||
<Descriptions.Item label="属性">
|
|
||||||
{attributes.map((attr) => {
|
|
||||||
const val = permutation[attr.name];
|
|
||||||
if (!val) return null;
|
|
||||||
return (
|
|
||||||
<Tag key={attr.name}>
|
|
||||||
{attr.title || attr.name}: {val.name}
|
|
||||||
</Tag>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</Descriptions.Item>
|
|
||||||
</Descriptions>
|
|
||||||
|
|
||||||
<ProFormText
|
|
||||||
name="sku"
|
|
||||||
label="SKU"
|
|
||||||
placeholder="请输入 SKU"
|
|
||||||
rules={[{ required: true, message: '请输入 SKU' }]}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ProFormText
|
|
||||||
name="name"
|
|
||||||
label="产品名称"
|
|
||||||
placeholder="请输入产品名称"
|
|
||||||
rules={[{ required: true, message: '请输入产品名称' }]}
|
|
||||||
/>
|
|
||||||
</ModalForm>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default CreateModal;
|
|
||||||
|
|
@ -1,439 +0,0 @@
|
||||||
import {
|
|
||||||
productcontrollerGetcategoriesall,
|
|
||||||
productcontrollerGetcategoryattributes,
|
|
||||||
productcontrollerGetproductlist,
|
|
||||||
} from '@/servers/api/product';
|
|
||||||
import { DownloadOutlined } from '@ant-design/icons';
|
|
||||||
import {
|
|
||||||
ActionType,
|
|
||||||
PageContainer,
|
|
||||||
ProCard,
|
|
||||||
ProForm,
|
|
||||||
ProFormSelect,
|
|
||||||
ProTable,
|
|
||||||
} from '@ant-design/pro-components';
|
|
||||||
import { request } from '@umijs/max';
|
|
||||||
import { Button, Tag, message } from 'antd';
|
|
||||||
import React, { useEffect, useRef, useState } from 'react';
|
|
||||||
import EditForm from '../List/EditForm';
|
|
||||||
import CreateModal from './components/CreateModal';
|
|
||||||
|
|
||||||
const PermutationPage: React.FC = () => {
|
|
||||||
const [categoryId, setCategoryId] = useState<number>();
|
|
||||||
const [attributes, setAttributes] = useState<any[]>([]);
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const [attributeValues, setAttributeValues] = useState<Record<string, any[]>>(
|
|
||||||
{},
|
|
||||||
);
|
|
||||||
const [permutations, setPermutations] = useState<any[]>([]);
|
|
||||||
const [existingProducts, setExistingProducts] = useState<
|
|
||||||
Map<string, API.Product>
|
|
||||||
>(new Map());
|
|
||||||
const [productsLoading, setProductsLoading] = useState(false);
|
|
||||||
|
|
||||||
const [createModalVisible, setCreateModalVisible] = useState(false);
|
|
||||||
const [selectedPermutation, setSelectedPermutation] = useState<any>(null);
|
|
||||||
const [categories, setCategories] = useState<any[]>([]);
|
|
||||||
const [form] = ProForm.useForm();
|
|
||||||
|
|
||||||
// Create a ref to mock ActionType for EditForm
|
|
||||||
const actionRef = useRef<ActionType>();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
productcontrollerGetcategoriesall().then((res) => {
|
|
||||||
const list = Array.isArray(res) ? res : res?.data || [];
|
|
||||||
setCategories(list);
|
|
||||||
if (list.length > 0) {
|
|
||||||
setCategoryId(list[0].id);
|
|
||||||
form.setFieldValue('categoryId', list[0].id);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const fetchProducts = async (catId: number) => {
|
|
||||||
setProductsLoading(true);
|
|
||||||
try {
|
|
||||||
const productRes = await productcontrollerGetproductlist({
|
|
||||||
categoryId: catId,
|
|
||||||
pageSize: 2000,
|
|
||||||
current: 1,
|
|
||||||
});
|
|
||||||
const products = productRes.data?.items || [];
|
|
||||||
|
|
||||||
const productMap = new Map<string, API.Product>();
|
|
||||||
products.forEach((p: any) => {
|
|
||||||
if (p.attributes && Array.isArray(p.attributes)) {
|
|
||||||
const key = generateAttributeKey(p.attributes);
|
|
||||||
if (key) productMap.set(key, p);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
setExistingProducts(productMap);
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
message.error('获取现有产品失败');
|
|
||||||
} finally {
|
|
||||||
setProductsLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Assign reload method to actionRef
|
|
||||||
useEffect(() => {
|
|
||||||
actionRef.current = {
|
|
||||||
reload: async () => {
|
|
||||||
if (categoryId) await fetchProducts(categoryId);
|
|
||||||
},
|
|
||||||
reloadAndRest: async () => {
|
|
||||||
if (categoryId) await fetchProducts(categoryId);
|
|
||||||
},
|
|
||||||
reset: () => {},
|
|
||||||
clearSelected: () => {},
|
|
||||||
} as any;
|
|
||||||
}, [categoryId]);
|
|
||||||
|
|
||||||
// Fetch attributes and products when category changes
|
|
||||||
useEffect(() => {
|
|
||||||
if (!categoryId) {
|
|
||||||
setAttributes([]);
|
|
||||||
setAttributeValues({});
|
|
||||||
setPermutations([]);
|
|
||||||
setExistingProducts(new Map());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const fetchData = async () => {
|
|
||||||
setLoading(true);
|
|
||||||
try {
|
|
||||||
// 1. Fetch Attributes
|
|
||||||
const attrRes = await productcontrollerGetcategoryattributes({
|
|
||||||
id: categoryId,
|
|
||||||
});
|
|
||||||
const attrs = Array.isArray(attrRes) ? attrRes : attrRes?.data || [];
|
|
||||||
setAttributes(attrs);
|
|
||||||
|
|
||||||
// 2. Fetch Attribute Values (Dict Items)
|
|
||||||
const valuesMap: Record<string, any[]> = {};
|
|
||||||
for (const attr of attrs) {
|
|
||||||
// 使用属性中直接包含的items,而不是额外请求
|
|
||||||
if (attr.items && Array.isArray(attr.items)) {
|
|
||||||
valuesMap[attr.name] = attr.items;
|
|
||||||
} else {
|
|
||||||
// 如果没有items,尝试通过dictId获取
|
|
||||||
const dictId = attr.dict?.id || attr.dictId;
|
|
||||||
if (dictId) {
|
|
||||||
const itemsRes = await request('/dict/items', {
|
|
||||||
params: { dictId },
|
|
||||||
});
|
|
||||||
valuesMap[attr.name] = itemsRes || [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setAttributeValues(valuesMap);
|
|
||||||
|
|
||||||
// 3. Fetch Existing Products
|
|
||||||
await fetchProducts(categoryId);
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
message.error('获取数据失败');
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
fetchData();
|
|
||||||
}, [categoryId]);
|
|
||||||
|
|
||||||
// Generate Permutations when attributes or values change
|
|
||||||
useEffect(() => {
|
|
||||||
if (attributes.length === 0 || Object.keys(attributeValues).length === 0) {
|
|
||||||
setPermutations([]);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const validAttributes = attributes.filter(
|
|
||||||
(attr) => attributeValues[attr.name]?.length > 0,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (validAttributes.length === 0) {
|
|
||||||
setPermutations([]);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const generateCombinations = (index: number, current: any): any[] => {
|
|
||||||
if (index === validAttributes.length) {
|
|
||||||
return [current];
|
|
||||||
}
|
|
||||||
|
|
||||||
const attr = validAttributes[index];
|
|
||||||
const values = attributeValues[attr.name];
|
|
||||||
let res: any[] = [];
|
|
||||||
|
|
||||||
for (const val of values) {
|
|
||||||
res = res.concat(
|
|
||||||
generateCombinations(index + 1, { ...current, [attr.name]: val }),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return res;
|
|
||||||
};
|
|
||||||
|
|
||||||
const combos = generateCombinations(0, {});
|
|
||||||
setPermutations(combos);
|
|
||||||
}, [attributes, attributeValues]);
|
|
||||||
|
|
||||||
const generateAttributeKey = (attrs: any[]) => {
|
|
||||||
const parts = attrs.map((a) => {
|
|
||||||
const key = a.dict?.name || a.dictName;
|
|
||||||
const val = a.name || a.value;
|
|
||||||
return `${key}:${val}`;
|
|
||||||
});
|
|
||||||
return parts.sort().join('|');
|
|
||||||
};
|
|
||||||
|
|
||||||
const generateKeyFromPermutation = (perm: any) => {
|
|
||||||
const parts = Object.keys(perm).map((attrName) => {
|
|
||||||
const valItem = perm[attrName];
|
|
||||||
const val = valItem.name;
|
|
||||||
return `${attrName}:${val}`;
|
|
||||||
});
|
|
||||||
return parts.sort().join('|');
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleAdd = (record: any) => {
|
|
||||||
setSelectedPermutation(record);
|
|
||||||
setCreateModalVisible(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 处理导出CSV功能
|
|
||||||
const handleExport = () => {
|
|
||||||
try {
|
|
||||||
// 如果没有数据则提示用户
|
|
||||||
if (permutations.length === 0) {
|
|
||||||
message.warning('暂无数据可导出');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 生成CSV表头(包含所有属性列和SKU列)
|
|
||||||
const headers = [
|
|
||||||
...attributes.map((attr) => attr.title || attr.name),
|
|
||||||
'SKU',
|
|
||||||
'状态',
|
|
||||||
];
|
|
||||||
|
|
||||||
// 生成CSV数据行
|
|
||||||
const rows = permutations.map((perm) => {
|
|
||||||
const key = generateKeyFromPermutation(perm);
|
|
||||||
const product = existingProducts.get(key);
|
|
||||||
|
|
||||||
// 获取每个属性值
|
|
||||||
const attrValues = attributes.map((attr) => {
|
|
||||||
const value = perm[attr.name]?.name || '';
|
|
||||||
return value;
|
|
||||||
});
|
|
||||||
|
|
||||||
// 获取SKU和状态
|
|
||||||
const sku = product?.sku || '';
|
|
||||||
const status = product ? '已存在' : '未创建';
|
|
||||||
|
|
||||||
return [...attrValues, sku, status];
|
|
||||||
});
|
|
||||||
|
|
||||||
// 将表头和数据行合并
|
|
||||||
const csvContent = [headers, ...rows]
|
|
||||||
.map((row) =>
|
|
||||||
// 处理CSV中的特殊字符(逗号、双引号、换行符)
|
|
||||||
row
|
|
||||||
.map((cell) => {
|
|
||||||
const cellStr = String(cell || '');
|
|
||||||
// 如果包含逗号、双引号或换行符,需要用双引号包裹,并将内部的双引号转义
|
|
||||||
if (
|
|
||||||
cellStr.includes(',') ||
|
|
||||||
cellStr.includes('"') ||
|
|
||||||
cellStr.includes('\n')
|
|
||||||
) {
|
|
||||||
return `"${cellStr.replace(/"/g, '""')}"`;
|
|
||||||
}
|
|
||||||
return cellStr;
|
|
||||||
})
|
|
||||||
.join(','),
|
|
||||||
)
|
|
||||||
.join('\n');
|
|
||||||
|
|
||||||
// 添加BOM以支持Excel正确显示中文
|
|
||||||
const BOM = '\uFEFF';
|
|
||||||
const blob = new Blob([BOM + csvContent], {
|
|
||||||
type: 'text/csv;charset=utf-8;',
|
|
||||||
});
|
|
||||||
|
|
||||||
// 创建下载链接并触发下载
|
|
||||||
const link = document.createElement('a');
|
|
||||||
const url = URL.createObjectURL(blob);
|
|
||||||
link.setAttribute('href', url);
|
|
||||||
|
|
||||||
// 生成文件名(包含当前分类名称和日期)
|
|
||||||
const category = categories.find((c) => c.id === categoryId);
|
|
||||||
const categoryName = category?.name || '产品';
|
|
||||||
const date = new Date().toISOString().slice(0, 10);
|
|
||||||
link.setAttribute('download', `${categoryName}_排列组合_${date}.csv`);
|
|
||||||
|
|
||||||
document.body.appendChild(link);
|
|
||||||
link.click();
|
|
||||||
|
|
||||||
// 清理
|
|
||||||
document.body.removeChild(link);
|
|
||||||
URL.revokeObjectURL(url);
|
|
||||||
|
|
||||||
message.success('导出成功');
|
|
||||||
} catch (error) {
|
|
||||||
console.error('导出失败:', error);
|
|
||||||
message.error('导出失败');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const columns: any[] = [
|
|
||||||
...attributes.map((attr) => ({
|
|
||||||
title: attr.title || attr.name,
|
|
||||||
dataIndex: attr.name,
|
|
||||||
width: 100, // Make columns narrower
|
|
||||||
render: (item: any) => item?.name || '-',
|
|
||||||
sorter: (a: any, b: any) => {
|
|
||||||
const valA = a[attr.name]?.name || '';
|
|
||||||
const valB = b[attr.name]?.name || '';
|
|
||||||
return valA.localeCompare(valB);
|
|
||||||
},
|
|
||||||
filters: attributeValues?.[attr.name]?.map?.((v: any) => ({
|
|
||||||
text: v.name,
|
|
||||||
value: v.name,
|
|
||||||
})),
|
|
||||||
onFilter: (value: any, record: any) => record[attr.name]?.name === value,
|
|
||||||
})),
|
|
||||||
{
|
|
||||||
title: '现有 SKU',
|
|
||||||
key: 'sku',
|
|
||||||
width: 150,
|
|
||||||
sorter: (a: any, b: any) => {
|
|
||||||
const keyA = generateKeyFromPermutation(a);
|
|
||||||
const productA = existingProducts.get(keyA);
|
|
||||||
const skuA = productA?.sku || '';
|
|
||||||
|
|
||||||
const keyB = generateKeyFromPermutation(b);
|
|
||||||
const productB = existingProducts.get(keyB);
|
|
||||||
const skuB = productB?.sku || '';
|
|
||||||
|
|
||||||
return skuA.localeCompare(skuB);
|
|
||||||
},
|
|
||||||
filters: [
|
|
||||||
{ text: '已存在', value: 'exists' },
|
|
||||||
{ text: '未创建', value: 'missing' },
|
|
||||||
],
|
|
||||||
onFilter: (value: any, record: any) => {
|
|
||||||
const key = generateKeyFromPermutation(record);
|
|
||||||
const exists = existingProducts.has(key);
|
|
||||||
if (value === 'exists') return exists;
|
|
||||||
if (value === 'missing') return !exists;
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
render: (_: any, record: any) => {
|
|
||||||
const key = generateKeyFromPermutation(record);
|
|
||||||
const product = existingProducts.get(key);
|
|
||||||
return product ? <Tag color="green">{product.sku}</Tag> : '-';
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '操作',
|
|
||||||
key: 'action',
|
|
||||||
width: 100,
|
|
||||||
render: (_: any, record: any) => {
|
|
||||||
const key = generateKeyFromPermutation(record);
|
|
||||||
const product = existingProducts.get(key);
|
|
||||||
if (product) {
|
|
||||||
return (
|
|
||||||
<EditForm
|
|
||||||
record={product}
|
|
||||||
tableRef={actionRef}
|
|
||||||
trigger={
|
|
||||||
<Button type="link" size="small">
|
|
||||||
编辑
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Button type="primary" size="small" onClick={() => handleAdd(record)}>
|
|
||||||
添加
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<PageContainer>
|
|
||||||
<ProCard>
|
|
||||||
<ProForm
|
|
||||||
form={form}
|
|
||||||
layout="inline"
|
|
||||||
submitter={false}
|
|
||||||
style={{ marginBottom: 24 }}
|
|
||||||
>
|
|
||||||
<ProFormSelect
|
|
||||||
name="categoryId"
|
|
||||||
label="选择分类"
|
|
||||||
width="md"
|
|
||||||
options={categories.map((item: any) => ({
|
|
||||||
label: item.name,
|
|
||||||
value: item.id,
|
|
||||||
}))}
|
|
||||||
fieldProps={{
|
|
||||||
onChange: (val) => setCategoryId(val as number),
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</ProForm>
|
|
||||||
|
|
||||||
{categoryId && (
|
|
||||||
<ProTable
|
|
||||||
size="small"
|
|
||||||
dataSource={permutations}
|
|
||||||
columns={columns}
|
|
||||||
loading={loading || productsLoading}
|
|
||||||
rowKey={(record) => generateKeyFromPermutation(record)}
|
|
||||||
pagination={{
|
|
||||||
defaultPageSize: 50,
|
|
||||||
showSizeChanger: true,
|
|
||||||
showQuickJumper: true,
|
|
||||||
pageSizeOptions: ['50', '100', '200', '500', '1000', '2000'],
|
|
||||||
}}
|
|
||||||
scroll={{ x: 'max-content' }}
|
|
||||||
search={false}
|
|
||||||
toolBarRender={() => [
|
|
||||||
<Button
|
|
||||||
key="export"
|
|
||||||
type="default"
|
|
||||||
icon={<DownloadOutlined />}
|
|
||||||
onClick={handleExport}
|
|
||||||
>
|
|
||||||
导出列表
|
|
||||||
</Button>,
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</ProCard>
|
|
||||||
|
|
||||||
{selectedPermutation && (
|
|
||||||
<CreateModal
|
|
||||||
visible={createModalVisible}
|
|
||||||
onClose={() => setCreateModalVisible(false)}
|
|
||||||
onSuccess={() => {
|
|
||||||
setCreateModalVisible(false);
|
|
||||||
if (categoryId) fetchProducts(categoryId);
|
|
||||||
}}
|
|
||||||
category={categories.find((c) => c.id === categoryId) || null}
|
|
||||||
permutation={selectedPermutation}
|
|
||||||
attributes={attributes}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</PageContainer>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default PermutationPage;
|
|
||||||
|
|
@ -0,0 +1,206 @@
|
||||||
|
import {
|
||||||
|
productcontrollerCreatestrength,
|
||||||
|
productcontrollerDeletestrength,
|
||||||
|
productcontrollerGetstrength,
|
||||||
|
} from '@/servers/api/product';
|
||||||
|
import { PlusOutlined } from '@ant-design/icons';
|
||||||
|
import {
|
||||||
|
ActionType,
|
||||||
|
DrawerForm,
|
||||||
|
PageContainer,
|
||||||
|
ProColumns,
|
||||||
|
ProFormText,
|
||||||
|
ProTable,
|
||||||
|
} from '@ant-design/pro-components';
|
||||||
|
import { App, Button, Popconfirm } from 'antd';
|
||||||
|
import { useRef } from 'react';
|
||||||
|
|
||||||
|
const List: React.FC = () => {
|
||||||
|
const actionRef = useRef<ActionType>();
|
||||||
|
const { message } = App.useApp();
|
||||||
|
const columns: ProColumns<API.Category>[] = [
|
||||||
|
{
|
||||||
|
title: '名称',
|
||||||
|
dataIndex: 'name',
|
||||||
|
tip: '名称是唯一的 key',
|
||||||
|
formItemProps: {
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: '名称为必填项',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '标识',
|
||||||
|
dataIndex: 'unique_key',
|
||||||
|
hideInSearch: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '更新时间',
|
||||||
|
dataIndex: 'updatedAt',
|
||||||
|
valueType: 'dateTime',
|
||||||
|
hideInSearch: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '创建时间',
|
||||||
|
dataIndex: 'createdAt',
|
||||||
|
valueType: 'dateTime',
|
||||||
|
hideInSearch: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
dataIndex: 'option',
|
||||||
|
valueType: 'option',
|
||||||
|
render: (_, record) => (
|
||||||
|
<>
|
||||||
|
{/* <UpdateForm tableRef={actionRef} values={record} />
|
||||||
|
<Divider type="vertical" /> */}
|
||||||
|
<Popconfirm
|
||||||
|
title="删除"
|
||||||
|
description="确认删除?"
|
||||||
|
onConfirm={async () => {
|
||||||
|
try {
|
||||||
|
const { success, message: errMsg } =
|
||||||
|
await productcontrollerDeletestrength({ id: record.id });
|
||||||
|
if (!success) {
|
||||||
|
throw new Error(errMsg);
|
||||||
|
}
|
||||||
|
actionRef.current?.reload();
|
||||||
|
} catch (error: any) {
|
||||||
|
message.error(error.message);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button type="primary" danger>
|
||||||
|
删除
|
||||||
|
</Button>
|
||||||
|
</Popconfirm>
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageContainer header={{ title: '强度列表' }}>
|
||||||
|
<ProTable<API.Category>
|
||||||
|
headerTitle="查询表格"
|
||||||
|
actionRef={actionRef}
|
||||||
|
rowKey="id"
|
||||||
|
toolBarRender={() => [<CreateForm tableRef={actionRef} />]}
|
||||||
|
request={async (params) => {
|
||||||
|
const { data, success } = await productcontrollerGetstrength(params);
|
||||||
|
return {
|
||||||
|
total: data?.total || 0,
|
||||||
|
data: data?.items || [],
|
||||||
|
success,
|
||||||
|
};
|
||||||
|
}}
|
||||||
|
columns={columns}
|
||||||
|
/>
|
||||||
|
</PageContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const CreateForm: React.FC<{
|
||||||
|
tableRef: React.MutableRefObject<ActionType | undefined>;
|
||||||
|
}> = ({ tableRef }) => {
|
||||||
|
const { message } = App.useApp();
|
||||||
|
return (
|
||||||
|
<DrawerForm<API.CreateCategoryDTO>
|
||||||
|
title="新建"
|
||||||
|
trigger={
|
||||||
|
<Button type="primary">
|
||||||
|
<PlusOutlined />
|
||||||
|
新建
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
autoFocusFirstInput
|
||||||
|
drawerProps={{
|
||||||
|
destroyOnHidden: true,
|
||||||
|
}}
|
||||||
|
onFinish={async (values) => {
|
||||||
|
try {
|
||||||
|
const { success, message: errMsg } =
|
||||||
|
await productcontrollerCreatestrength(values);
|
||||||
|
if (!success) {
|
||||||
|
throw new Error(errMsg);
|
||||||
|
}
|
||||||
|
tableRef.current?.reload();
|
||||||
|
message.success('提交成功');
|
||||||
|
return true;
|
||||||
|
} catch (error: any) {
|
||||||
|
message.error(error.message);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ProFormText
|
||||||
|
name="name"
|
||||||
|
width="md"
|
||||||
|
label="强度名称"
|
||||||
|
placeholder="请输入名称"
|
||||||
|
rules={[{ required: true, message: '请输入名称' }]}
|
||||||
|
/>
|
||||||
|
<ProFormText
|
||||||
|
name="unique_key"
|
||||||
|
width="md"
|
||||||
|
label="Key"
|
||||||
|
placeholder="请输入Key"
|
||||||
|
rules={[{ required: true, message: '请输入Key' }]}
|
||||||
|
/>
|
||||||
|
</DrawerForm>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// const UpdateForm: React.FC<{
|
||||||
|
// tableRef: React.MutableRefObject<ActionType | undefined>;
|
||||||
|
// values: API.Category;
|
||||||
|
// }> = ({ tableRef, values: initialValues }) => {
|
||||||
|
// const { message } = App.useApp();
|
||||||
|
// return (
|
||||||
|
// <DrawerForm<API.UpdateCategoryDTO>
|
||||||
|
// title="编辑"
|
||||||
|
// initialValues={initialValues}
|
||||||
|
// trigger={
|
||||||
|
// <Button type="primary">
|
||||||
|
// <EditOutlined />
|
||||||
|
// 编辑
|
||||||
|
// </Button>
|
||||||
|
// }
|
||||||
|
// autoFocusFirstInput
|
||||||
|
// drawerProps={{
|
||||||
|
// destroyOnHidden: true,
|
||||||
|
// }}
|
||||||
|
// onFinish={async (values) => {
|
||||||
|
// try {
|
||||||
|
// const { success, message: errMsg } =
|
||||||
|
// await productcontrollerUpdatestrength(
|
||||||
|
// { id: initialValues.id },
|
||||||
|
// values,
|
||||||
|
// );
|
||||||
|
// if (!success) {
|
||||||
|
// throw new Error(errMsg);
|
||||||
|
// }
|
||||||
|
// message.success('提交成功');
|
||||||
|
// tableRef.current?.reload();
|
||||||
|
// return true;
|
||||||
|
// } catch (error: any) {
|
||||||
|
// message.error(error.message);
|
||||||
|
// }
|
||||||
|
// }}
|
||||||
|
// >
|
||||||
|
// <ProForm.Group>
|
||||||
|
// <ProFormText
|
||||||
|
// name="name"
|
||||||
|
// width="md"
|
||||||
|
// label="强度名称"
|
||||||
|
// placeholder="请输入名称"
|
||||||
|
// rules={[{ required: true, message: '请输入名称' }]}
|
||||||
|
// />
|
||||||
|
// </ProForm.Group>
|
||||||
|
// </DrawerForm>
|
||||||
|
// );
|
||||||
|
// };
|
||||||
|
|
||||||
|
export default List;
|
||||||
|
|
@ -1,461 +0,0 @@
|
||||||
import { productcontrollerGetproductlist } from '@/servers/api/product';
|
|
||||||
import {
|
|
||||||
siteapicontrollerGetproducts,
|
|
||||||
siteapicontrollerUpsertproduct,
|
|
||||||
} from '@/servers/api/siteApi';
|
|
||||||
import { templatecontrollerRendertemplate } from '@/servers/api/template';
|
|
||||||
import { SyncOutlined } from '@ant-design/icons';
|
|
||||||
import { ModalForm, ProFormText } from '@ant-design/pro-components';
|
|
||||||
import { Button, message, Spin, Tag } from 'antd';
|
|
||||||
import React, { useEffect, useState } from 'react';
|
|
||||||
|
|
||||||
// 定义站点接口
|
|
||||||
interface Site {
|
|
||||||
id: number;
|
|
||||||
name: string;
|
|
||||||
skuPrefix?: string;
|
|
||||||
isDisabled?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 定义本地产品接口(与后端 Product 实体匹配)
|
|
||||||
interface SiteProduct {
|
|
||||||
id: number;
|
|
||||||
sku: string;
|
|
||||||
name: string;
|
|
||||||
nameCn: string;
|
|
||||||
shortDescription?: string;
|
|
||||||
description?: string;
|
|
||||||
price: number;
|
|
||||||
promotionPrice: number;
|
|
||||||
type: string;
|
|
||||||
categoryId?: number;
|
|
||||||
category?: any;
|
|
||||||
attributes?: any[];
|
|
||||||
components?: any[];
|
|
||||||
siteSkus: string[];
|
|
||||||
source: number;
|
|
||||||
createdAt: Date;
|
|
||||||
updatedAt: Date;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 定义本地产品完整接口
|
|
||||||
interface LocalProduct {
|
|
||||||
id: number;
|
|
||||||
sku: string;
|
|
||||||
name: string;
|
|
||||||
nameCn: string;
|
|
||||||
shortDescription?: string;
|
|
||||||
description?: string;
|
|
||||||
price: number;
|
|
||||||
promotionPrice: number;
|
|
||||||
type: string;
|
|
||||||
categoryId?: number;
|
|
||||||
category?: any;
|
|
||||||
attributes?: any[];
|
|
||||||
components?: any[];
|
|
||||||
siteSkus: string[];
|
|
||||||
source: number;
|
|
||||||
images?: string[];
|
|
||||||
weight?: number;
|
|
||||||
dimensions?: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 定义站点产品数据接口
|
|
||||||
interface SiteProductData {
|
|
||||||
sku: string;
|
|
||||||
regular_price?: number;
|
|
||||||
price?: number;
|
|
||||||
sale_price?: number;
|
|
||||||
stock_quantity?: number;
|
|
||||||
stockQuantity?: number;
|
|
||||||
status?: string;
|
|
||||||
externalProductId?: string;
|
|
||||||
name?: string;
|
|
||||||
description?: string;
|
|
||||||
images?: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
interface SiteProductCellProps {
|
|
||||||
// 产品行数据
|
|
||||||
product: SiteProduct;
|
|
||||||
// 站点列数据
|
|
||||||
site: Site;
|
|
||||||
// 同步成功后的回调
|
|
||||||
onSyncSuccess?: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const SiteProductCell: React.FC<SiteProductCellProps> = ({
|
|
||||||
product,
|
|
||||||
site,
|
|
||||||
onSyncSuccess,
|
|
||||||
}) => {
|
|
||||||
// 存储该站点对应的产品数据
|
|
||||||
const [siteProduct, setSiteProduct] = useState<SiteProductData | null>(null);
|
|
||||||
// 存储本地产品完整数据
|
|
||||||
const [localProduct, setLocalProduct] = useState<LocalProduct | null>(null);
|
|
||||||
// 加载状态
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
// 是否已加载过数据
|
|
||||||
const [loaded, setLoaded] = useState(false);
|
|
||||||
// 同步中状态
|
|
||||||
const [syncing, setSyncing] = useState(false);
|
|
||||||
|
|
||||||
// 组件挂载时加载数据
|
|
||||||
useEffect(() => {
|
|
||||||
loadSiteProduct();
|
|
||||||
}, [product.id, site.id]);
|
|
||||||
|
|
||||||
// 加载站点产品数据
|
|
||||||
const loadSiteProduct = async () => {
|
|
||||||
// 如果已经加载过,则不再重复加载
|
|
||||||
if (loaded) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setLoading(true);
|
|
||||||
try {
|
|
||||||
// 首先查找该产品在该站点的实际SKU
|
|
||||||
// 注意:siteSkus 现在是字符串数组,无法直接匹配站点
|
|
||||||
// 这里使用模板生成的 SKU 作为默认值
|
|
||||||
let siteProductSku = '';
|
|
||||||
// 如果需要更精确的站点 SKU 匹配,需要后端提供额外的接口
|
|
||||||
|
|
||||||
// 如果没有找到实际的siteSku,则根据模板或默认规则生成期望的SKU
|
|
||||||
const expectedSku =
|
|
||||||
siteProductSku || `${site.skuPrefix || ''}-${product.sku}`;
|
|
||||||
|
|
||||||
// 使用 siteapicontrollerGetproducts 获取该站点的所有产品
|
|
||||||
const productsRes = await siteapicontrollerGetproducts({
|
|
||||||
siteId: site.id,
|
|
||||||
current: 1,
|
|
||||||
pageSize: 10000,
|
|
||||||
} as any);
|
|
||||||
|
|
||||||
if (productsRes.data?.items) {
|
|
||||||
// 在该站点的产品数据中查找匹配的产品
|
|
||||||
let foundProduct = productsRes.data.items.find(
|
|
||||||
(item: any) => item.sku === expectedSku,
|
|
||||||
);
|
|
||||||
|
|
||||||
// 如果根据实际SKU没找到,再尝试用模板生成的SKU查找
|
|
||||||
if (!foundProduct && siteProductSku) {
|
|
||||||
foundProduct = productsRes.data.items.find(
|
|
||||||
(item: any) => item.sku === siteProductSku,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (foundProduct) {
|
|
||||||
setSiteProduct(foundProduct as SiteProductData);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 标记为已加载
|
|
||||||
setLoaded(true);
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`加载站点 ${site.name} 的产品数据失败:`, error);
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 获取本地产品完整信息
|
|
||||||
const getLocalProduct = async (): Promise<LocalProduct | null> => {
|
|
||||||
try {
|
|
||||||
// 如果已经有本地产品数据,直接返回
|
|
||||||
if (localProduct) {
|
|
||||||
return localProduct;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 使用 productcontrollerGetproductlist 获取本地产品完整信息
|
|
||||||
const res = await productcontrollerGetproductlist({
|
|
||||||
where: {
|
|
||||||
id: product.id,
|
|
||||||
},
|
|
||||||
} as any);
|
|
||||||
|
|
||||||
if (res.success && res.data) {
|
|
||||||
const productData = res.data as LocalProduct;
|
|
||||||
setLocalProduct(productData);
|
|
||||||
return productData;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('获取本地产品信息失败:', error);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 渲染站点SKU
|
|
||||||
const renderSiteSku = async (data: any): Promise<string> => {
|
|
||||||
try {
|
|
||||||
// 使用 templatecontrollerRendertemplate API 渲染模板
|
|
||||||
const res = await templatecontrollerRendertemplate(
|
|
||||||
{ name: 'siteproduct-sku' } as any,
|
|
||||||
data,
|
|
||||||
);
|
|
||||||
|
|
||||||
return res?.template || res?.result || '';
|
|
||||||
} catch (error) {
|
|
||||||
console.error('渲染SKU模板失败:', error);
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 同步产品到站点
|
|
||||||
const syncProductToSite = async (values: any) => {
|
|
||||||
try {
|
|
||||||
setSyncing(true);
|
|
||||||
const hide = message.loading('正在同步...', 0);
|
|
||||||
|
|
||||||
// 获取本地产品完整信息
|
|
||||||
const productDetail = await getLocalProduct();
|
|
||||||
|
|
||||||
if (!productDetail) {
|
|
||||||
hide();
|
|
||||||
message.error('获取本地产品信息失败');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 构造要同步的产品数据
|
|
||||||
const productData: any = {
|
|
||||||
sku: values.sku,
|
|
||||||
name: productDetail.name,
|
|
||||||
description: productDetail.description || '',
|
|
||||||
regular_price: productDetail.price,
|
|
||||||
price: productDetail.price,
|
|
||||||
stock_quantity: productDetail.stock,
|
|
||||||
status: 'publish',
|
|
||||||
};
|
|
||||||
|
|
||||||
// 如果有图片,添加图片信息
|
|
||||||
if (productDetail.images && productDetail.images.length > 0) {
|
|
||||||
productData.images = productDetail.images;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果有重量,添加重量信息
|
|
||||||
if (productDetail.weight) {
|
|
||||||
productData.weight = productDetail.weight;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果有尺寸,添加尺寸信息
|
|
||||||
if (productDetail.dimensions) {
|
|
||||||
productData.dimensions = productDetail.dimensions;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 使用 siteapicontrollerUpsertproduct API 同步产品到站点
|
|
||||||
const res = await siteapicontrollerUpsertproduct(
|
|
||||||
{ siteId: site.id } as any,
|
|
||||||
productData as any,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!res.success) {
|
|
||||||
hide();
|
|
||||||
throw new Error(res.message || '同步失败');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新本地状态
|
|
||||||
if (res.data && typeof res.data === 'object') {
|
|
||||||
setSiteProduct(res.data as SiteProductData);
|
|
||||||
}
|
|
||||||
|
|
||||||
hide();
|
|
||||||
message.success('同步成功');
|
|
||||||
|
|
||||||
// 触发回调
|
|
||||||
if (onSyncSuccess) {
|
|
||||||
onSyncSuccess();
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
} catch (error: any) {
|
|
||||||
message.error('同步失败: ' + (error.message || error.toString()));
|
|
||||||
return false;
|
|
||||||
} finally {
|
|
||||||
setSyncing(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 更新同步产品到站点
|
|
||||||
const updateSyncProduct = async (values: any) => {
|
|
||||||
try {
|
|
||||||
setSyncing(true);
|
|
||||||
const hide = message.loading('正在更新...', 0);
|
|
||||||
|
|
||||||
// 获取本地产品完整信息
|
|
||||||
const productDetail = await getLocalProduct();
|
|
||||||
|
|
||||||
if (!productDetail) {
|
|
||||||
hide();
|
|
||||||
message.error('获取本地产品信息失败');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 构造要更新的产品数据
|
|
||||||
const productData: any = {
|
|
||||||
...siteProduct,
|
|
||||||
sku: values.sku,
|
|
||||||
name: productDetail.name,
|
|
||||||
description: productDetail.description || '',
|
|
||||||
regular_price: productDetail.price,
|
|
||||||
price: productDetail.price,
|
|
||||||
stock_quantity: productDetail.stock,
|
|
||||||
status: 'publish',
|
|
||||||
};
|
|
||||||
|
|
||||||
// 如果有图片,添加图片信息
|
|
||||||
if (productDetail.images && productDetail.images.length > 0) {
|
|
||||||
productData.images = productDetail.images;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果有重量,添加重量信息
|
|
||||||
if (productDetail.weight) {
|
|
||||||
productData.weight = productDetail.weight;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果有尺寸,添加尺寸信息
|
|
||||||
if (productDetail.dimensions) {
|
|
||||||
productData.dimensions = productDetail.dimensions;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 使用 siteapicontrollerUpsertproduct API 更新产品到站点
|
|
||||||
const res = await siteapicontrollerUpsertproduct(
|
|
||||||
{ siteId: site.id } as any,
|
|
||||||
productData as any,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!res.success) {
|
|
||||||
hide();
|
|
||||||
throw new Error(res.message || '更新失败');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新本地状态
|
|
||||||
if (res.data && typeof res.data === 'object') {
|
|
||||||
setSiteProduct(res.data as SiteProductData);
|
|
||||||
}
|
|
||||||
|
|
||||||
hide();
|
|
||||||
message.success('更新成功');
|
|
||||||
|
|
||||||
// 触发回调
|
|
||||||
if (onSyncSuccess) {
|
|
||||||
onSyncSuccess();
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
} catch (error: any) {
|
|
||||||
message.error('更新失败: ' + (error.message || error.toString()));
|
|
||||||
return false;
|
|
||||||
} finally {
|
|
||||||
setSyncing(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 如果正在加载,显示加载状态
|
|
||||||
if (loading) {
|
|
||||||
return (
|
|
||||||
<div style={{ textAlign: 'center', padding: 10 }}>
|
|
||||||
<Spin size="small" />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果没有找到站点产品,显示同步按钮
|
|
||||||
if (!siteProduct) {
|
|
||||||
// 首先查找该产品在该站点的实际SKU
|
|
||||||
// 注意:siteSkus 现在是字符串数组,无法直接匹配站点
|
|
||||||
// 这里使用模板生成的 SKU 作为默认值
|
|
||||||
let siteProductSku = '';
|
|
||||||
// 如果需要更精确的站点 SKU 匹配,需要后端提供额外的接口
|
|
||||||
|
|
||||||
const defaultSku =
|
|
||||||
siteProductSku || `${site.skuPrefix || ''}-${product.sku}`;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ModalForm
|
|
||||||
title="同步产品"
|
|
||||||
trigger={
|
|
||||||
<Button type="link" icon={<SyncOutlined />}>
|
|
||||||
同步到站点
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
width={400}
|
|
||||||
onFinish={async (values) => {
|
|
||||||
return await syncProductToSite(values);
|
|
||||||
}}
|
|
||||||
initialValues={{
|
|
||||||
sku: defaultSku,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ProFormText
|
|
||||||
name="sku"
|
|
||||||
label="商店 SKU"
|
|
||||||
placeholder="请输入商店 SKU"
|
|
||||||
rules={[{ required: true, message: '请输入 SKU' }]}
|
|
||||||
/>
|
|
||||||
</ModalForm>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 显示站点产品信息
|
|
||||||
return (
|
|
||||||
<div style={{ fontSize: 12 }}>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: 'flex',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
alignItems: 'start',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div style={{ fontWeight: 'bold' }}>{siteProduct.sku}</div>
|
|
||||||
<ModalForm
|
|
||||||
title="更新同步"
|
|
||||||
trigger={
|
|
||||||
<Button
|
|
||||||
type="link"
|
|
||||||
size="small"
|
|
||||||
icon={<SyncOutlined spin={false} />}
|
|
||||||
>
|
|
||||||
更新
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
width={400}
|
|
||||||
onFinish={async (values) => {
|
|
||||||
return await updateSyncProduct(values);
|
|
||||||
}}
|
|
||||||
initialValues={{
|
|
||||||
sku: siteProduct.sku,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ProFormText
|
|
||||||
name="sku"
|
|
||||||
label="商店 SKU"
|
|
||||||
placeholder="请输入商店 SKU"
|
|
||||||
rules={[{ required: true, message: '请输入 SKU' }]}
|
|
||||||
disabled
|
|
||||||
/>
|
|
||||||
<div style={{ marginBottom: 16, color: '#666' }}>
|
|
||||||
确定要将本地产品数据更新到站点吗?
|
|
||||||
</div>
|
|
||||||
</ModalForm>
|
|
||||||
</div>
|
|
||||||
<div>Price: {siteProduct.regular_price ?? siteProduct.price}</div>
|
|
||||||
{siteProduct.sale_price && (
|
|
||||||
<div style={{ color: 'red' }}>Sale: {siteProduct.sale_price}</div>
|
|
||||||
)}
|
|
||||||
<div>
|
|
||||||
Stock: {siteProduct.stock_quantity ?? siteProduct.stockQuantity}
|
|
||||||
</div>
|
|
||||||
<div style={{ marginTop: 2 }}>
|
|
||||||
Status:{' '}
|
|
||||||
{siteProduct.status === 'publish' ? (
|
|
||||||
<Tag color="green">Published</Tag>
|
|
||||||
) : (
|
|
||||||
<Tag>{siteProduct.status}</Tag>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default SiteProductCell;
|
|
||||||
|
|
@ -1,503 +0,0 @@
|
||||||
import {
|
|
||||||
productcontrollerBatchsynctosite,
|
|
||||||
productcontrollerGetproductlist,
|
|
||||||
productcontrollerSynctosite,
|
|
||||||
} from '@/servers/api/product';
|
|
||||||
import { EditOutlined, SyncOutlined } from '@ant-design/icons';
|
|
||||||
import { ActionType, ProColumns, ProTable } from '@ant-design/pro-components';
|
|
||||||
import { request } from '@umijs/max';
|
|
||||||
import {
|
|
||||||
Button,
|
|
||||||
Card,
|
|
||||||
message,
|
|
||||||
Modal,
|
|
||||||
Progress,
|
|
||||||
Select,
|
|
||||||
Spin,
|
|
||||||
Tag,
|
|
||||||
} from 'antd';
|
|
||||||
import React, { useEffect, useRef, useState } from 'react';
|
|
||||||
import EditForm from '../List/EditForm';
|
|
||||||
import SiteProductCell from './SiteProductCell';
|
|
||||||
|
|
||||||
// 定义站点接口
|
|
||||||
interface Site {
|
|
||||||
id: number;
|
|
||||||
name: string;
|
|
||||||
skuPrefix?: string;
|
|
||||||
isDisabled?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 定义本地产品接口(与后端 Product 实体匹配)
|
|
||||||
interface SiteProduct {
|
|
||||||
id: number;
|
|
||||||
sku: string;
|
|
||||||
name: string;
|
|
||||||
nameCn: string;
|
|
||||||
shortDescription?: string;
|
|
||||||
description?: string;
|
|
||||||
price: number;
|
|
||||||
promotionPrice: number;
|
|
||||||
type: string;
|
|
||||||
categoryId?: number;
|
|
||||||
category?: any;
|
|
||||||
attributes?: any[];
|
|
||||||
components?: any[];
|
|
||||||
siteSkus: string[];
|
|
||||||
source: number;
|
|
||||||
createdAt: Date;
|
|
||||||
updatedAt: Date;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 定义API响应接口
|
|
||||||
interface ApiResponse<T> {
|
|
||||||
data: T[];
|
|
||||||
success: boolean;
|
|
||||||
message?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 模拟API请求函数
|
|
||||||
const getSites = async (): Promise<ApiResponse<Site>> => {
|
|
||||||
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 ProductSyncPage: React.FC = () => {
|
|
||||||
const [sites, setSites] = useState<Site[]>([]);
|
|
||||||
|
|
||||||
const [initialLoading, setInitialLoading] = useState(true);
|
|
||||||
const actionRef = useRef<ActionType>();
|
|
||||||
const [selectedSiteId, setSelectedSiteId] = useState<string>('');
|
|
||||||
const [batchSyncModalVisible, setBatchSyncModalVisible] = useState(false);
|
|
||||||
const [syncProgress, setSyncProgress] = useState(0);
|
|
||||||
const [syncing, setSyncing] = useState(false);
|
|
||||||
const [syncResults, setSyncResults] = useState<{
|
|
||||||
success: number;
|
|
||||||
failed: number;
|
|
||||||
errors: string[];
|
|
||||||
}>({ success: 0, failed: 0, errors: [] });
|
|
||||||
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
|
|
||||||
const [selectedRows, setSelectedRows] = useState<SiteProduct[]>([]);
|
|
||||||
// 初始化加载站点列表
|
|
||||||
useEffect(() => {
|
|
||||||
const initializeData = async () => {
|
|
||||||
try {
|
|
||||||
// 获取站点列表
|
|
||||||
const sitesRes = await getSites();
|
|
||||||
if (sitesRes.success && sitesRes.data.length > 0) {
|
|
||||||
setSites(sitesRes.data);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('初始化数据失败:', error);
|
|
||||||
message.error('初始化数据失败');
|
|
||||||
} finally {
|
|
||||||
setInitialLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
initializeData();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const syncProductToSite = async (
|
|
||||||
values: any,
|
|
||||||
record: SiteProduct,
|
|
||||||
site: Site,
|
|
||||||
siteProductId?: string,
|
|
||||||
) => {
|
|
||||||
try {
|
|
||||||
const hide = message.loading('正在同步...', 0);
|
|
||||||
|
|
||||||
// 使用 productcontrollerSynctosite API 同步产品到站点
|
|
||||||
const res = await productcontrollerSynctosite({
|
|
||||||
productId: Number(record.id),
|
|
||||||
siteId: Number(site.id),
|
|
||||||
} as any);
|
|
||||||
|
|
||||||
if (!res.success) {
|
|
||||||
hide();
|
|
||||||
throw new Error(res.message || '同步失败');
|
|
||||||
}
|
|
||||||
|
|
||||||
hide();
|
|
||||||
message.success('同步成功');
|
|
||||||
return true;
|
|
||||||
} catch (error: any) {
|
|
||||||
message.error('同步失败: ' + (error.message || error.toString()));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 批量同步产品到指定站点
|
|
||||||
const batchSyncProducts = async (productsToSync?: SiteProduct[]) => {
|
|
||||||
if (!selectedSiteId) {
|
|
||||||
message.error('请选择要同步到的站点');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const targetSite = sites.find((site) => site.id === selectedSiteId);
|
|
||||||
if (!targetSite) {
|
|
||||||
message.error('选择的站点不存在');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果没有传入产品列表,则使用选中的产品
|
|
||||||
let products = productsToSync || selectedRows;
|
|
||||||
|
|
||||||
// 如果既没有传入产品也没有选中产品,则同步所有产品
|
|
||||||
if (!products || products.length === 0) {
|
|
||||||
try {
|
|
||||||
const { data, success } = await productcontrollerGetproductlist({
|
|
||||||
current: 1,
|
|
||||||
pageSize: 10000, // 获取所有产品
|
|
||||||
} as any);
|
|
||||||
|
|
||||||
if (!success || !data?.items) {
|
|
||||||
message.error('获取产品列表失败');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
products = data.items as SiteProduct[];
|
|
||||||
} catch (error) {
|
|
||||||
message.error('获取产品列表失败');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setSyncing(true);
|
|
||||||
setSyncProgress(0);
|
|
||||||
setSyncResults({ success: 0, failed: 0, errors: [] });
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 使用 productcontrollerBatchsynctosite API 批量同步
|
|
||||||
const productIds = products.map((product) => Number(product.id));
|
|
||||||
|
|
||||||
// 更新进度为50%,表示正在处理
|
|
||||||
setSyncProgress(50);
|
|
||||||
|
|
||||||
const res = await productcontrollerBatchsynctosite({
|
|
||||||
productIds: productIds,
|
|
||||||
siteId: Number(targetSite.id),
|
|
||||||
} as any);
|
|
||||||
|
|
||||||
if (res.success) {
|
|
||||||
const syncedCount = res.data?.synced || 0;
|
|
||||||
const errors = res.data?.errors || [];
|
|
||||||
|
|
||||||
// 更新进度为100%,表示完成
|
|
||||||
setSyncProgress(100);
|
|
||||||
|
|
||||||
setSyncResults({
|
|
||||||
success: syncedCount,
|
|
||||||
failed: errors.length,
|
|
||||||
errors: errors.map((err: any) => err.error || '未知错误'),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (errors.length === 0) {
|
|
||||||
message.success(`批量同步完成,成功同步 ${syncedCount} 个产品`);
|
|
||||||
} else {
|
|
||||||
message.warning(
|
|
||||||
`批量同步完成,成功 ${syncedCount} 个,失败 ${errors.length} 个`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 刷新表格
|
|
||||||
actionRef.current?.reload();
|
|
||||||
} else {
|
|
||||||
throw new Error(res.message || '批量同步失败');
|
|
||||||
}
|
|
||||||
} catch (error: any) {
|
|
||||||
message.error('批量同步失败: ' + (error.message || error.toString()));
|
|
||||||
} finally {
|
|
||||||
setSyncing(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 生成表格列配置
|
|
||||||
const generateColumns = (): ProColumns<Site>[] => {
|
|
||||||
const columns: ProColumns<SiteProduct>[] = [
|
|
||||||
{
|
|
||||||
title: 'SKU',
|
|
||||||
dataIndex: 'sku',
|
|
||||||
key: 'sku',
|
|
||||||
width: 150,
|
|
||||||
fixed: 'left',
|
|
||||||
copyable: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '商品信息',
|
|
||||||
key: 'profile',
|
|
||||||
width: 300,
|
|
||||||
fixed: 'left',
|
|
||||||
render: (_, record) => (
|
|
||||||
<div>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: 'flex',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
alignItems: 'center',
|
|
||||||
marginBottom: 4,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div style={{ fontWeight: 'bold', fontSize: 14 }}>
|
|
||||||
{record.name}
|
|
||||||
</div>
|
|
||||||
<EditForm
|
|
||||||
record={record}
|
|
||||||
tableRef={actionRef}
|
|
||||||
trigger={
|
|
||||||
<EditOutlined
|
|
||||||
style={{
|
|
||||||
cursor: 'pointer',
|
|
||||||
fontSize: 16,
|
|
||||||
color: '#1890ff',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div style={{ fontSize: 12, color: '#666' }}>
|
|
||||||
<span style={{ marginRight: 8 }}>价格: {record.price}</span>
|
|
||||||
{record.promotionPrice && (
|
|
||||||
<span style={{ color: 'red' }}>
|
|
||||||
促销价: {record.promotionPrice}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 属性 */}
|
|
||||||
<div style={{ marginTop: 4 }}>
|
|
||||||
{record.attributes?.map((attr: any, idx: number) => (
|
|
||||||
<Tag
|
|
||||||
key={idx}
|
|
||||||
style={{ fontSize: 10, marginRight: 4, marginBottom: 2 }}
|
|
||||||
>
|
|
||||||
{attr.dict?.name || attr.name}: {attr.name}
|
|
||||||
</Tag>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 组成 (如果是 Bundle) */}
|
|
||||||
{record.type === 'bundle' &&
|
|
||||||
record.components &&
|
|
||||||
record.components.length > 0 && (
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
marginTop: 8,
|
|
||||||
fontSize: 12,
|
|
||||||
background: '#f5f5f5',
|
|
||||||
padding: 4,
|
|
||||||
borderRadius: 4,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div style={{ fontWeight: 'bold', marginBottom: 2 }}>
|
|
||||||
Components:
|
|
||||||
</div>
|
|
||||||
{record.components.map((comp: any, idx: number) => (
|
|
||||||
<div key={idx}>
|
|
||||||
{comp.sku} × {comp.quantity}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
// 为每个站点生成列
|
|
||||||
sites.forEach((site: Site) => {
|
|
||||||
const siteColumn: ProColumns<SiteProduct> = {
|
|
||||||
title: site.name,
|
|
||||||
key: `site_${site.id}`,
|
|
||||||
hideInSearch: true,
|
|
||||||
width: 220,
|
|
||||||
render: (_, record) => {
|
|
||||||
return (
|
|
||||||
<SiteProductCell
|
|
||||||
product={record}
|
|
||||||
site={site}
|
|
||||||
onSyncSuccess={() => {
|
|
||||||
// 同步成功后刷新表格
|
|
||||||
actionRef.current?.reload();
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
columns.push(siteColumn);
|
|
||||||
});
|
|
||||||
|
|
||||||
return columns;
|
|
||||||
};
|
|
||||||
|
|
||||||
if (initialLoading) {
|
|
||||||
return (
|
|
||||||
<Card title="商品同步状态" className="product-sync-card">
|
|
||||||
<Spin
|
|
||||||
size="large"
|
|
||||||
style={{ display: 'flex', justifyContent: 'center', padding: 40 }}
|
|
||||||
/>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card title="商品同步状态" className="product-sync-card">
|
|
||||||
<ProTable<SiteProduct>
|
|
||||||
columns={generateColumns()}
|
|
||||||
actionRef={actionRef}
|
|
||||||
rowKey="id"
|
|
||||||
rowSelection={{
|
|
||||||
selectedRowKeys,
|
|
||||||
onChange: (keys, rows) => {
|
|
||||||
setSelectedRowKeys(keys);
|
|
||||||
setSelectedRows(rows);
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
toolBarRender={() => [
|
|
||||||
<Select
|
|
||||||
key="site-select"
|
|
||||||
style={{ width: 200 }}
|
|
||||||
placeholder="选择目标站点"
|
|
||||||
value={selectedSiteId}
|
|
||||||
onChange={setSelectedSiteId}
|
|
||||||
options={sites.map((site) => ({
|
|
||||||
label: site.name,
|
|
||||||
value: site.id,
|
|
||||||
}))}
|
|
||||||
/>,
|
|
||||||
<Button
|
|
||||||
key="batch-sync"
|
|
||||||
type="primary"
|
|
||||||
icon={<SyncOutlined />}
|
|
||||||
onClick={() => {
|
|
||||||
if (!selectedSiteId) {
|
|
||||||
message.warning('请先选择目标站点');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setBatchSyncModalVisible(true);
|
|
||||||
}}
|
|
||||||
disabled={!selectedSiteId || sites.length === 0}
|
|
||||||
>
|
|
||||||
批量同步
|
|
||||||
</Button>,
|
|
||||||
]}
|
|
||||||
request={async (params, sort, filter) => {
|
|
||||||
// 调用本地获取产品列表 API
|
|
||||||
const response = await productcontrollerGetproductlist({
|
|
||||||
...params,
|
|
||||||
current: params.current,
|
|
||||||
pageSize: params.pageSize,
|
|
||||||
// 传递搜索参数
|
|
||||||
// keyword: params.keyword, // 假设 ProTable 的 search 表单会传递 keyword 或其他字段
|
|
||||||
sku: (params as any).sku,
|
|
||||||
name: (params as any).name,
|
|
||||||
} as any);
|
|
||||||
console.log('result', response);
|
|
||||||
// 返回给 ProTable
|
|
||||||
return {
|
|
||||||
data: response.data?.items || [],
|
|
||||||
success: response.success,
|
|
||||||
total: response.data?.total || 0,
|
|
||||||
};
|
|
||||||
}}
|
|
||||||
pagination={{
|
|
||||||
pageSize: 10,
|
|
||||||
showSizeChanger: true,
|
|
||||||
showQuickJumper: true,
|
|
||||||
}}
|
|
||||||
scroll={{ x: 'max-content' }}
|
|
||||||
search={{
|
|
||||||
labelWidth: 'auto',
|
|
||||||
}}
|
|
||||||
options={{
|
|
||||||
density: true,
|
|
||||||
fullScreen: true,
|
|
||||||
}}
|
|
||||||
dateFormatter="string"
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* 批量同步模态框 */}
|
|
||||||
<Modal
|
|
||||||
title="批量同步产品"
|
|
||||||
open={batchSyncModalVisible}
|
|
||||||
onCancel={() => !syncing && setBatchSyncModalVisible(false)}
|
|
||||||
footer={null}
|
|
||||||
closable={!syncing}
|
|
||||||
maskClosable={!syncing}
|
|
||||||
>
|
|
||||||
<div style={{ marginBottom: 16 }}>
|
|
||||||
<p>
|
|
||||||
目标站点:
|
|
||||||
<strong>{sites.find((s) => s.id === selectedSiteId)?.name}</strong>
|
|
||||||
</p>
|
|
||||||
{selectedRows.length > 0 ? (
|
|
||||||
<p>
|
|
||||||
已选择 <strong>{selectedRows.length}</strong> 个产品进行同步
|
|
||||||
</p>
|
|
||||||
) : (
|
|
||||||
<p>此操作将同步所有库存产品到指定站点,请确认是否继续?</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{syncing && (
|
|
||||||
<div style={{ marginBottom: 16 }}>
|
|
||||||
<div style={{ marginBottom: 8 }}>同步进度:</div>
|
|
||||||
<Progress percent={syncProgress} status="active" />
|
|
||||||
<div style={{ marginTop: 8, fontSize: 12, color: '#666' }}>
|
|
||||||
成功:{syncResults.success} | 失败:{syncResults.failed}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{syncResults.errors.length > 0 && (
|
|
||||||
<div style={{ marginBottom: 16, maxHeight: 200, overflow: 'auto' }}>
|
|
||||||
<div style={{ marginBottom: 8, color: '#ff4d4f' }}>错误详情:</div>
|
|
||||||
{syncResults.errors.slice(0, 10).map((error, index) => (
|
|
||||||
<div
|
|
||||||
key={index}
|
|
||||||
style={{ fontSize: 12, color: '#666', marginBottom: 4 }}
|
|
||||||
>
|
|
||||||
{error}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
{syncResults.errors.length > 10 && (
|
|
||||||
<div style={{ fontSize: 12, color: '#999' }}>
|
|
||||||
...还有 {syncResults.errors.length - 10} 个错误
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div style={{ textAlign: 'right' }}>
|
|
||||||
<Button
|
|
||||||
onClick={() => setBatchSyncModalVisible(false)}
|
|
||||||
disabled={syncing}
|
|
||||||
style={{ marginRight: 8 }}
|
|
||||||
>
|
|
||||||
取消
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
type="primary"
|
|
||||||
onClick={() => batchSyncProducts()}
|
|
||||||
loading={syncing}
|
|
||||||
disabled={syncing}
|
|
||||||
>
|
|
||||||
{syncing ? '同步中...' : '开始同步'}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</Modal>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ProductSyncPage;
|
|
||||||
|
|
@ -0,0 +1,605 @@
|
||||||
|
import { PRODUCT_STATUS_ENUM, PRODUCT_STOCK_STATUS_ENUM } from '@/constants';
|
||||||
|
import {
|
||||||
|
productcontrollerProductbysku,
|
||||||
|
productcontrollerSearchproducts,
|
||||||
|
} from '@/servers/api/product';
|
||||||
|
import { sitecontrollerAll } from '@/servers/api/site';
|
||||||
|
import {
|
||||||
|
wpproductcontrollerGetwpproducts,
|
||||||
|
wpproductcontrollerSetconstitution,
|
||||||
|
wpproductcontrollerSyncproducts,
|
||||||
|
wpproductcontrollerUpdateproduct,
|
||||||
|
wpproductcontrollerUpdatevariation,
|
||||||
|
wpproductcontrollerUpdatewpproductstate,
|
||||||
|
} from '@/servers/api/wpProduct';
|
||||||
|
import { EditOutlined } from '@ant-design/icons';
|
||||||
|
import {
|
||||||
|
ActionType,
|
||||||
|
DrawerForm,
|
||||||
|
PageContainer,
|
||||||
|
ProColumns,
|
||||||
|
ProForm,
|
||||||
|
ProFormDigit,
|
||||||
|
ProFormList,
|
||||||
|
ProFormSelect,
|
||||||
|
ProFormText,
|
||||||
|
ProTable,
|
||||||
|
} from '@ant-design/pro-components';
|
||||||
|
import { App, Button, Divider, Form } from 'antd';
|
||||||
|
import { useRef } from 'react';
|
||||||
|
|
||||||
|
const List: React.FC = () => {
|
||||||
|
const actionRef = useRef<ActionType>();
|
||||||
|
const columns: ProColumns<API.WpProductDTO>[] = [
|
||||||
|
{
|
||||||
|
title: '名称',
|
||||||
|
dataIndex: 'name',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '站点',
|
||||||
|
dataIndex: 'siteId',
|
||||||
|
valueType: 'select',
|
||||||
|
request: async () => {
|
||||||
|
const { data = [] } = await sitecontrollerAll();
|
||||||
|
return data.map((item) => ({
|
||||||
|
label: item.siteName,
|
||||||
|
value: item.id,
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'sku',
|
||||||
|
dataIndex: 'sku',
|
||||||
|
hideInSearch: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '产品状态',
|
||||||
|
dataIndex: 'status',
|
||||||
|
valueType: 'select',
|
||||||
|
valueEnum: PRODUCT_STATUS_ENUM,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '常规价格',
|
||||||
|
dataIndex: 'regular_price',
|
||||||
|
hideInSearch: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '销售价格',
|
||||||
|
dataIndex: 'sale_price',
|
||||||
|
hideInSearch: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '更新时间',
|
||||||
|
dataIndex: 'updatedAt',
|
||||||
|
valueType: 'dateTime',
|
||||||
|
hideInSearch: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '创建时间',
|
||||||
|
dataIndex: 'createdAt',
|
||||||
|
valueType: 'dateTime',
|
||||||
|
hideInSearch: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
dataIndex: 'option',
|
||||||
|
valueType: 'option',
|
||||||
|
render: (_, record) => (
|
||||||
|
<>
|
||||||
|
<UpdateForm tableRef={actionRef} values={record} />
|
||||||
|
<UpdateStatus tableRef={actionRef} values={record} />
|
||||||
|
{record.type === 'simple' && record.sku ? (
|
||||||
|
<>
|
||||||
|
<Divider type="vertical" />
|
||||||
|
<SetComponent
|
||||||
|
tableRef={actionRef}
|
||||||
|
values={record}
|
||||||
|
isProduct={true}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<></>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const varColumns: ProColumns<API.VariationDTO>[] = [
|
||||||
|
{
|
||||||
|
title: '变体名',
|
||||||
|
dataIndex: 'name',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'sku',
|
||||||
|
dataIndex: 'sku',
|
||||||
|
hideInSearch: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '常规价格',
|
||||||
|
dataIndex: 'regular_price',
|
||||||
|
hideInSearch: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '销售价格',
|
||||||
|
dataIndex: 'sale_price',
|
||||||
|
hideInSearch: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
dataIndex: 'option',
|
||||||
|
valueType: 'option',
|
||||||
|
render: (_, record) => (
|
||||||
|
<>
|
||||||
|
<UpdateVaritation tableRef={actionRef} values={record} />
|
||||||
|
{record.sku ? (
|
||||||
|
<>
|
||||||
|
<Divider type="vertical" />
|
||||||
|
<SetComponent
|
||||||
|
tableRef={actionRef}
|
||||||
|
values={record}
|
||||||
|
isProduct={false}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<></>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageContainer header={{ title: 'WP产品列表' }}>
|
||||||
|
<ProTable<API.WpProductDTO>
|
||||||
|
headerTitle="查询表格"
|
||||||
|
actionRef={actionRef}
|
||||||
|
rowKey="id"
|
||||||
|
request={async (params) => {
|
||||||
|
const { data, success } = await wpproductcontrollerGetwpproducts(
|
||||||
|
params,
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
total: data?.total || 0,
|
||||||
|
data: data?.items || [],
|
||||||
|
success,
|
||||||
|
};
|
||||||
|
}}
|
||||||
|
columns={columns}
|
||||||
|
toolBarRender={() => [<SyncForm tableRef={actionRef} />]}
|
||||||
|
expandable={{
|
||||||
|
rowExpandable: (record) => record.type === 'variable',
|
||||||
|
expandedRowRender: (record) => (
|
||||||
|
<ProTable<API.VariationDTO>
|
||||||
|
rowKey="id"
|
||||||
|
dataSource={record.variations}
|
||||||
|
pagination={false}
|
||||||
|
search={false}
|
||||||
|
options={false}
|
||||||
|
columns={varColumns}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</PageContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const SyncForm: React.FC<{
|
||||||
|
tableRef: React.MutableRefObject<ActionType | undefined>;
|
||||||
|
}> = ({ tableRef }) => {
|
||||||
|
const { message } = App.useApp();
|
||||||
|
return (
|
||||||
|
<DrawerForm<API.wpproductcontrollerSyncproductsParams>
|
||||||
|
title="同步产品"
|
||||||
|
trigger={
|
||||||
|
<Button key="syncSite" type="primary">
|
||||||
|
同步产品
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
autoFocusFirstInput
|
||||||
|
drawerProps={{
|
||||||
|
destroyOnHidden: true,
|
||||||
|
}}
|
||||||
|
onFinish={async (values) => {
|
||||||
|
try {
|
||||||
|
const { success, message: errMsg } =
|
||||||
|
await wpproductcontrollerSyncproducts(values);
|
||||||
|
if (!success) {
|
||||||
|
throw new Error(errMsg);
|
||||||
|
}
|
||||||
|
message.success('同步成功');
|
||||||
|
tableRef.current?.reload();
|
||||||
|
return true;
|
||||||
|
} catch (error: any) {
|
||||||
|
message.error(error.message);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ProForm.Group>
|
||||||
|
<ProFormSelect
|
||||||
|
name="siteId"
|
||||||
|
width="lg"
|
||||||
|
label="站点"
|
||||||
|
placeholder="请选择站点"
|
||||||
|
request={async () => {
|
||||||
|
const { data = [] } = await sitecontrollerAll();
|
||||||
|
return data.map((item) => ({
|
||||||
|
label: item.siteName,
|
||||||
|
value: item.id,
|
||||||
|
}));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</ProForm.Group>
|
||||||
|
</DrawerForm>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const UpdateStatus: React.FC<{
|
||||||
|
tableRef: React.MutableRefObject<ActionType | undefined>;
|
||||||
|
values: API.WpProductDTO;
|
||||||
|
}> = ({ tableRef, values: initialValues }) => {
|
||||||
|
const { message } = App.useApp();
|
||||||
|
return (
|
||||||
|
<DrawerForm<API.UpdateProductDTO>
|
||||||
|
title="修改产品上下架状态"
|
||||||
|
initialValues={initialValues}
|
||||||
|
trigger={
|
||||||
|
<Button type="primary">
|
||||||
|
<EditOutlined />
|
||||||
|
上下架
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
autoFocusFirstInput
|
||||||
|
drawerProps={{
|
||||||
|
destroyOnHidden: true,
|
||||||
|
}}
|
||||||
|
onFinish={async (values) => {
|
||||||
|
console.log('values', values);
|
||||||
|
const { status, stock_status } = values;
|
||||||
|
try {
|
||||||
|
const { success, message: errMsg } =
|
||||||
|
await wpproductcontrollerUpdatewpproductstate(
|
||||||
|
{
|
||||||
|
id: initialValues.id,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
status,
|
||||||
|
stock_status
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (!success) {
|
||||||
|
throw new Error(errMsg);
|
||||||
|
}
|
||||||
|
message.success('提交成功');
|
||||||
|
tableRef.current?.reload();
|
||||||
|
return true;
|
||||||
|
} catch (error: any) {
|
||||||
|
message.error(error.message);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ProForm.Group>
|
||||||
|
<ProFormSelect
|
||||||
|
label="状态"
|
||||||
|
width="lg"
|
||||||
|
name="status"
|
||||||
|
valueEnum={PRODUCT_STATUS_ENUM}
|
||||||
|
/>
|
||||||
|
<ProFormSelect
|
||||||
|
label="上下架状态"
|
||||||
|
width="lg"
|
||||||
|
name="stock_status"
|
||||||
|
valueEnum={PRODUCT_STOCK_STATUS_ENUM}
|
||||||
|
/>
|
||||||
|
</ProForm.Group>
|
||||||
|
</DrawerForm>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const UpdateForm: React.FC<{
|
||||||
|
tableRef: React.MutableRefObject<ActionType | undefined>;
|
||||||
|
values: API.WpProductDTO;
|
||||||
|
}> = ({ tableRef, values: initialValues }) => {
|
||||||
|
const { message } = App.useApp();
|
||||||
|
return (
|
||||||
|
<DrawerForm<API.UpdateProductDTO>
|
||||||
|
title="编辑产品"
|
||||||
|
initialValues={initialValues}
|
||||||
|
trigger={
|
||||||
|
<Button type="primary">
|
||||||
|
<EditOutlined />
|
||||||
|
编辑
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
autoFocusFirstInput
|
||||||
|
drawerProps={{
|
||||||
|
destroyOnHidden: true,
|
||||||
|
}}
|
||||||
|
onFinish={async (values) => {
|
||||||
|
const { siteId, ...params } = values;
|
||||||
|
try {
|
||||||
|
const { success, message: errMsg } =
|
||||||
|
await wpproductcontrollerUpdateproduct(
|
||||||
|
{
|
||||||
|
productId: initialValues.externalProductId,
|
||||||
|
siteId,
|
||||||
|
},
|
||||||
|
params,
|
||||||
|
);
|
||||||
|
if (!success) {
|
||||||
|
throw new Error(errMsg);
|
||||||
|
}
|
||||||
|
message.success('提交成功');
|
||||||
|
tableRef.current?.reload();
|
||||||
|
return true;
|
||||||
|
} catch (error: any) {
|
||||||
|
message.error(error.message);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ProForm.Group>
|
||||||
|
<ProFormText label="名称" width="lg" name="name" />
|
||||||
|
<ProFormSelect
|
||||||
|
width="lg"
|
||||||
|
label="站点"
|
||||||
|
request={async () => {
|
||||||
|
const { data = [] } = await sitecontrollerAll();
|
||||||
|
return data.map((item) => ({
|
||||||
|
label: item.siteName,
|
||||||
|
value: item.id,
|
||||||
|
}));
|
||||||
|
}}
|
||||||
|
name="siteId"
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
<ProFormText
|
||||||
|
name="sku"
|
||||||
|
width="lg"
|
||||||
|
label="sku"
|
||||||
|
tooltip="Example: TO-ZY-06MG-WG-S-0001"
|
||||||
|
placeholder="请输入SKU"
|
||||||
|
/>
|
||||||
|
{initialValues.type === 'simple' ? (
|
||||||
|
<>
|
||||||
|
<ProFormDigit
|
||||||
|
name="regular_price"
|
||||||
|
width="lg"
|
||||||
|
label="常规价格"
|
||||||
|
fieldProps={{
|
||||||
|
precision: 2,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<ProFormDigit
|
||||||
|
name="sale_price"
|
||||||
|
width="lg"
|
||||||
|
label="促销价格"
|
||||||
|
fieldProps={{
|
||||||
|
precision: 2,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<></>
|
||||||
|
)}
|
||||||
|
</ProForm.Group>
|
||||||
|
</DrawerForm>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const UpdateVaritation: React.FC<{
|
||||||
|
tableRef: React.MutableRefObject<ActionType | undefined>;
|
||||||
|
values: API.VariationDTO;
|
||||||
|
}> = ({ tableRef, values: initialValues }) => {
|
||||||
|
const { message } = App.useApp();
|
||||||
|
return (
|
||||||
|
<DrawerForm<API.UpdateProductDTO>
|
||||||
|
title="编辑变体"
|
||||||
|
initialValues={initialValues}
|
||||||
|
trigger={
|
||||||
|
<Button type="primary">
|
||||||
|
<EditOutlined />
|
||||||
|
编辑
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
autoFocusFirstInput
|
||||||
|
drawerProps={{
|
||||||
|
destroyOnHidden: true,
|
||||||
|
}}
|
||||||
|
onFinish={async (values) => {
|
||||||
|
const { ...params } = values;
|
||||||
|
try {
|
||||||
|
const { success, message: errMsg } =
|
||||||
|
await wpproductcontrollerUpdatevariation(
|
||||||
|
{
|
||||||
|
siteId: initialValues.siteId,
|
||||||
|
productId: initialValues.externalProductId,
|
||||||
|
variationId: initialValues.externalVariationId,
|
||||||
|
},
|
||||||
|
params,
|
||||||
|
);
|
||||||
|
if (!success) {
|
||||||
|
throw new Error(errMsg);
|
||||||
|
}
|
||||||
|
message.success('提交成功');
|
||||||
|
tableRef.current?.reload();
|
||||||
|
return true;
|
||||||
|
} catch (error: any) {
|
||||||
|
message.error(error.message);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ProForm.Group>
|
||||||
|
<ProFormText label="变体名称" width="lg" name="name" />
|
||||||
|
<ProFormText
|
||||||
|
name="sku"
|
||||||
|
width="lg"
|
||||||
|
label="sku"
|
||||||
|
tooltip="Example: TO-ZY-06MG-WG-S-0001"
|
||||||
|
placeholder="请输入SKU"
|
||||||
|
/>
|
||||||
|
<ProFormDigit
|
||||||
|
name="regular_price"
|
||||||
|
width="lg"
|
||||||
|
label="常规价格"
|
||||||
|
fieldProps={{
|
||||||
|
precision: 2,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<ProFormDigit
|
||||||
|
name="sale_price"
|
||||||
|
width="lg"
|
||||||
|
label="促销价格"
|
||||||
|
fieldProps={{
|
||||||
|
precision: 2,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</ProForm.Group>
|
||||||
|
</DrawerForm>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const SetComponent: React.FC<{
|
||||||
|
tableRef: React.MutableRefObject<ActionType | undefined>;
|
||||||
|
values: API.VariationDTO | API.WpProductDTO;
|
||||||
|
isProduct: boolean;
|
||||||
|
}> = ({ tableRef, values: { id, constitution, name }, isProduct = false }) => {
|
||||||
|
const { message } = App.useApp();
|
||||||
|
const [form] = Form.useForm();
|
||||||
|
|
||||||
|
const fetchInitialValues = async () => {
|
||||||
|
const initData = await Promise.all(
|
||||||
|
constitution?.map?.(async (item) => {
|
||||||
|
const { data } = await productcontrollerProductbysku({
|
||||||
|
sku: item.sku as string,
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
quantity: item.quantity,
|
||||||
|
sku: {
|
||||||
|
label: data?.name,
|
||||||
|
value: item.sku,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}) || [],
|
||||||
|
);
|
||||||
|
form.setFieldsValue({
|
||||||
|
constitution: initData,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DrawerForm<API.SetConstitutionDTO>
|
||||||
|
title={name}
|
||||||
|
form={form}
|
||||||
|
trigger={
|
||||||
|
<Button type="primary" danger={constitution?.length === 0}>
|
||||||
|
<EditOutlined />
|
||||||
|
构成
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
autoFocusFirstInput
|
||||||
|
drawerProps={{
|
||||||
|
destroyOnHidden: true,
|
||||||
|
}}
|
||||||
|
onFinish={async ({ constitution }) => {
|
||||||
|
try {
|
||||||
|
const { success, message: errMsg } =
|
||||||
|
await wpproductcontrollerSetconstitution(
|
||||||
|
{
|
||||||
|
id,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
isProduct,
|
||||||
|
constitution,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (!success) {
|
||||||
|
throw new Error(errMsg);
|
||||||
|
}
|
||||||
|
message.success('提交成功');
|
||||||
|
tableRef.current?.reload();
|
||||||
|
return true;
|
||||||
|
} catch (error: any) {
|
||||||
|
message.error(error.message);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onOpenChange={(visiable) => {
|
||||||
|
if (visiable) fetchInitialValues();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ProForm.Group>
|
||||||
|
<ProFormList<{
|
||||||
|
sku: string;
|
||||||
|
quantity: number;
|
||||||
|
}>
|
||||||
|
name="constitution"
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: '至少需要一个商品',
|
||||||
|
validator: (_, value) =>
|
||||||
|
value && value.length > 0
|
||||||
|
? Promise.resolve()
|
||||||
|
: Promise.reject('至少需要一个商品'),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
creatorButtonProps={{ children: '新增' }}
|
||||||
|
>
|
||||||
|
{(fields, idx, { remove }) => (
|
||||||
|
<div key={idx}>
|
||||||
|
<ProFormSelect
|
||||||
|
request={async ({ keyWords }) => {
|
||||||
|
if (keyWords.length < 3) return [];
|
||||||
|
try {
|
||||||
|
const { data } = await productcontrollerSearchproducts({
|
||||||
|
name: keyWords,
|
||||||
|
});
|
||||||
|
const arr =
|
||||||
|
data?.map((item) => {
|
||||||
|
return {
|
||||||
|
label: item.name,
|
||||||
|
value: item.sku,
|
||||||
|
};
|
||||||
|
}) || [];
|
||||||
|
return arr;
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
name="sku"
|
||||||
|
label="产品"
|
||||||
|
width="lg"
|
||||||
|
placeholder="请选择产品"
|
||||||
|
tooltip="至少输入3个字符"
|
||||||
|
fieldProps={{
|
||||||
|
showSearch: true,
|
||||||
|
filterOption: false,
|
||||||
|
}}
|
||||||
|
transform={(value) => {
|
||||||
|
return value?.value || value;
|
||||||
|
}}
|
||||||
|
debounceTime={300} // 防抖,减少请求频率
|
||||||
|
rules={[{ required: true, message: '请选择产品' }]}
|
||||||
|
/>
|
||||||
|
<ProFormDigit
|
||||||
|
name="quantity"
|
||||||
|
label="数量"
|
||||||
|
placeholder="请输入数量"
|
||||||
|
rules={[{ required: true, message: '请输入数量' }]}
|
||||||
|
fieldProps={{
|
||||||
|
precision: 0,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Button type="link" danger onClick={() => remove(fields.key)}>
|
||||||
|
删除
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</ProFormList>
|
||||||
|
</ProForm.Group>
|
||||||
|
</DrawerForm>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default List;
|
||||||
|
|
@ -1,268 +1,77 @@
|
||||||
import { ordercontrollerSyncorders } from '@/servers/api/order';
|
|
||||||
import {
|
|
||||||
sitecontrollerCreate,
|
|
||||||
sitecontrollerDisable,
|
|
||||||
sitecontrollerList,
|
|
||||||
sitecontrollerUpdate,
|
|
||||||
} from '@/servers/api/site';
|
|
||||||
import { stockcontrollerGetallstockpoints } from '@/servers/api/stock';
|
|
||||||
import { subscriptioncontrollerSync } from '@/servers/api/subscription';
|
|
||||||
import {
|
|
||||||
ActionType,
|
|
||||||
DrawerForm,
|
|
||||||
ProColumns,
|
|
||||||
ProFormSelect,
|
|
||||||
ProFormSwitch,
|
|
||||||
ProTable,
|
|
||||||
} from '@ant-design/pro-components';
|
|
||||||
import {
|
|
||||||
Button,
|
|
||||||
Form,
|
|
||||||
message,
|
|
||||||
notification,
|
|
||||||
Popconfirm,
|
|
||||||
Space,
|
|
||||||
Tag,
|
|
||||||
} from 'antd';
|
|
||||||
import * as countries from 'i18n-iso-countries';
|
|
||||||
import zhCN from 'i18n-iso-countries/langs/zh';
|
|
||||||
import React, { useEffect, useRef, useState } from 'react';
|
import React, { useEffect, useRef, useState } from 'react';
|
||||||
import EditSiteForm from '../Shop/EditSiteForm'; // 引入重构后的表单组件
|
import { ActionType, ProColumns, ProTable, ProFormInstance } from '@ant-design/pro-components';
|
||||||
|
import { DrawerForm, ProFormText, ProFormSelect, ProFormSwitch } from '@ant-design/pro-components';
|
||||||
|
import { Button, message, Popconfirm, Space, Tag } from 'antd';
|
||||||
|
import { request } from '@umijs/max';
|
||||||
|
|
||||||
// 区域数据项类型
|
// 站点数据项类型(前端不包含密钥字段,后端列表不返回密钥)
|
||||||
interface AreaItem {
|
interface SiteItem {
|
||||||
code: string;
|
|
||||||
name: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 仓库数据项类型
|
|
||||||
interface StockPointItem {
|
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
siteName: string;
|
||||||
}
|
|
||||||
|
|
||||||
// 站点数据项类型(前端不包含密钥字段,后端列表不返回密钥)
|
|
||||||
export interface SiteItem {
|
|
||||||
id: number;
|
|
||||||
name: string;
|
|
||||||
description?: string;
|
|
||||||
apiUrl?: string;
|
apiUrl?: string;
|
||||||
websiteUrl?: string; // 网站地址
|
|
||||||
type?: 'woocommerce' | 'shopyy';
|
type?: 'woocommerce' | 'shopyy';
|
||||||
skuPrefix?: string;
|
skuPrefix?: string;
|
||||||
isDisabled: number;
|
isDisabled: number;
|
||||||
areas?: AreaItem[];
|
}
|
||||||
stockPoints?: StockPointItem[];
|
|
||||||
|
// 创建/更新表单的值类型,包含可选的密钥字段
|
||||||
|
interface SiteFormValues {
|
||||||
|
siteName: string;
|
||||||
|
apiUrl?: string;
|
||||||
|
type?: 'woocommerce' | 'shopyy';
|
||||||
|
isDisabled?: boolean;
|
||||||
|
consumerKey?: string; // WooCommerce REST API 的 consumer key
|
||||||
|
consumerSecret?: string; // WooCommerce REST API 的 consumer secret
|
||||||
|
skuPrefix?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const SiteList: React.FC = () => {
|
const SiteList: React.FC = () => {
|
||||||
const actionRef = useRef<ActionType>();
|
const actionRef = useRef<ActionType>();
|
||||||
|
const formRef = useRef<ProFormInstance>();
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const [editing, setEditing] = useState<SiteItem & { areas: string[] } | null>(null);
|
const [editing, setEditing] = useState<SiteItem | null>(null);
|
||||||
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
|
|
||||||
const [batchEditOpen, setBatchEditOpen] = useState(false);
|
|
||||||
const [batchEditForm] = Form.useForm();
|
|
||||||
countries.registerLocale(zhCN);
|
|
||||||
|
|
||||||
const handleSync = async (ids: number[]) => {
|
|
||||||
if (!ids.length) return;
|
|
||||||
const hide = message.loading('正在同步...', 0);
|
|
||||||
|
|
||||||
const stats = {
|
|
||||||
orders: { success: 0, fail: 0 },
|
|
||||||
subscriptions: { success: 0, fail: 0 },
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
|
||||||
for (const id of ids) {
|
|
||||||
// 同步订单
|
|
||||||
const orderRes = await ordercontrollerSyncorders({ siteId: id });
|
|
||||||
if (orderRes.success) {
|
|
||||||
stats.orders.success += 1;
|
|
||||||
} else {
|
|
||||||
stats.orders.fail += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 同步订阅
|
|
||||||
const subRes = await subscriptioncontrollerSync({ siteId: id });
|
|
||||||
if (subRes.success) {
|
|
||||||
stats.subscriptions.success += 1;
|
|
||||||
} else {
|
|
||||||
stats.subscriptions.fail += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
hide();
|
|
||||||
|
|
||||||
notification.success({
|
|
||||||
message: '同步完成',
|
|
||||||
description: (
|
|
||||||
<div>
|
|
||||||
<p>
|
|
||||||
订单: 成功 {stats.orders.success}, 失败 {stats.orders.fail}
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
订阅: 成功 {stats.subscriptions.success}, 失败{' '}
|
|
||||||
{stats.subscriptions.fail}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
duration: null, // 不自动关闭
|
|
||||||
});
|
|
||||||
|
|
||||||
setSelectedRowKeys([]);
|
|
||||||
actionRef.current?.reload();
|
|
||||||
} catch (error: any) {
|
|
||||||
hide();
|
|
||||||
message.error(error.message || '同步失败');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 获取所有国家/地区的选项
|
|
||||||
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(() => {
|
useEffect(() => {
|
||||||
if (batchEditOpen) {
|
if (!open) return;
|
||||||
batchEditForm.resetFields();
|
if (editing) {
|
||||||
|
formRef.current?.setFieldsValue({
|
||||||
|
siteName: editing.siteName,
|
||||||
|
apiUrl: editing.apiUrl,
|
||||||
|
type: editing.type,
|
||||||
|
skuPrefix: editing.skuPrefix,
|
||||||
|
isDisabled: !!editing.isDisabled,
|
||||||
|
consumerKey: undefined,
|
||||||
|
consumerSecret: undefined,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
formRef.current?.setFieldsValue({
|
||||||
|
siteName: undefined,
|
||||||
|
apiUrl: undefined,
|
||||||
|
type: 'woocommerce',
|
||||||
|
skuPrefix: undefined,
|
||||||
|
isDisabled: false,
|
||||||
|
consumerKey: undefined,
|
||||||
|
consumerSecret: undefined,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}, [batchEditOpen, batchEditForm]);
|
}, [open, editing]);
|
||||||
|
|
||||||
// 表格列定义
|
// 表格列定义
|
||||||
const columns: ProColumns<SiteItem>[] = [
|
const columns: ProColumns<SiteItem>[] = [
|
||||||
{
|
{ title: 'ID', dataIndex: 'id', width: 80, sorter: true, hideInSearch: true },
|
||||||
title: 'ID',
|
{ title: '站点名称', dataIndex: 'siteName', width: 220 },
|
||||||
dataIndex: 'id',
|
|
||||||
width: 80,
|
|
||||||
sorter: true,
|
|
||||||
hideInSearch: true,
|
|
||||||
},
|
|
||||||
{ title: '名称', dataIndex: 'name', width: 220 },
|
|
||||||
{ title: '描述', dataIndex: 'description', width: 220, hideInSearch: true },
|
|
||||||
{ title: 'API 地址', dataIndex: 'apiUrl', width: 280, hideInSearch: true },
|
{ title: 'API 地址', dataIndex: 'apiUrl', width: 280, hideInSearch: true },
|
||||||
{
|
{ title: 'SKU 前缀', dataIndex: 'skuPrefix', width: 160, hideInSearch: true },
|
||||||
title: '网站地址',
|
|
||||||
dataIndex: 'websiteUrl',
|
|
||||||
width: 280,
|
|
||||||
hideInSearch: true,
|
|
||||||
render: (text) => (
|
|
||||||
<a href={text as string} target="_blank" rel="noopener noreferrer">
|
|
||||||
{text}
|
|
||||||
</a>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'webhook地址',
|
|
||||||
dataIndex: 'webhookUrl',
|
|
||||||
hideInSearch: true,
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
title: 'SKU 前缀',
|
|
||||||
dataIndex: 'skuPrefix',
|
|
||||||
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: '关联仓库',
|
|
||||||
dataIndex: 'stockPoints',
|
|
||||||
hideInSearch: true,
|
|
||||||
render: (_, row) => {
|
|
||||||
if (!row.stockPoints || row.stockPoints.length === 0) {
|
|
||||||
return <Tag>无</Tag>;
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<Space wrap>
|
|
||||||
{row.stockPoints.map((sp) => (
|
|
||||||
<Tag color="blue" key={sp.id}>
|
|
||||||
{sp.name}
|
|
||||||
</Tag>
|
|
||||||
))}
|
|
||||||
</Space>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
title: '状态',
|
title: '状态',
|
||||||
dataIndex: 'isDisabled',
|
dataIndex: 'isDisabled',
|
||||||
|
|
@ -278,27 +87,13 @@ const SiteList: React.FC = () => {
|
||||||
title: '操作',
|
title: '操作',
|
||||||
dataIndex: 'actions',
|
dataIndex: 'actions',
|
||||||
width: 240,
|
width: 240,
|
||||||
fixed: 'right',
|
|
||||||
hideInSearch: true,
|
hideInSearch: true,
|
||||||
render: (_, row) => (
|
render: (_, row) => (
|
||||||
<Space>
|
<Space>
|
||||||
<Button
|
|
||||||
size="small"
|
|
||||||
type="primary"
|
|
||||||
onClick={() => handleSync([row.id])}
|
|
||||||
>
|
|
||||||
同步
|
|
||||||
</Button>
|
|
||||||
<Button
|
<Button
|
||||||
size="small"
|
size="small"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
function normalEditing(row:SiteItem){
|
setEditing(row);
|
||||||
return {
|
|
||||||
...row,
|
|
||||||
areas: row.areas?.map(area=>area.code) || [],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setEditing(normalEditing(row));
|
|
||||||
setOpen(true);
|
setOpen(true);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
@ -306,13 +101,13 @@ const SiteList: React.FC = () => {
|
||||||
</Button>
|
</Button>
|
||||||
<Popconfirm
|
<Popconfirm
|
||||||
title={row.isDisabled ? '启用站点' : '禁用站点'}
|
title={row.isDisabled ? '启用站点' : '禁用站点'}
|
||||||
description={row.isDisabled ? '确认启用该站点?' : '确认禁用该站点?'}
|
description={row.isDisabled ? '确认启用该站点?' : '确认禁用该站点?'}
|
||||||
onConfirm={async () => {
|
onConfirm={async () => {
|
||||||
try {
|
try {
|
||||||
await sitecontrollerDisable(
|
await request(`/site/disable/${row.id}`, {
|
||||||
{ id: String(row.id) },
|
method: 'PUT',
|
||||||
{ disabled: !row.isDisabled },
|
data: { disabled: !row.isDisabled },
|
||||||
);
|
});
|
||||||
message.success('更新成功');
|
message.success('更新成功');
|
||||||
actionRef.current?.reload();
|
actionRef.current?.reload();
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
|
|
@ -332,17 +127,21 @@ const SiteList: React.FC = () => {
|
||||||
// 表格数据请求
|
// 表格数据请求
|
||||||
const tableRequest = async (params: Record<string, any>) => {
|
const tableRequest = async (params: Record<string, any>) => {
|
||||||
try {
|
try {
|
||||||
const { current, pageSize, name, type } = params;
|
const { current = 1, pageSize = 10, siteName, type } = params;
|
||||||
const resp = await sitecontrollerList({
|
const resp = await request('/site/list', {
|
||||||
|
method: 'GET',
|
||||||
|
params: {
|
||||||
current,
|
current,
|
||||||
pageSize,
|
pageSize,
|
||||||
keyword: name || undefined,
|
keyword: siteName || undefined,
|
||||||
type: type || undefined,
|
type: type || undefined,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
// 假设 resp 直接就是后端返回的结构,包含 items 和 total
|
const { success, data, message: errMsg } = resp as any;
|
||||||
|
if (!success) throw new Error(errMsg || '获取失败');
|
||||||
return {
|
return {
|
||||||
data: (resp?.data?.items ?? []) as SiteItem[],
|
data: (data?.items ?? []) as SiteItem[],
|
||||||
total: resp?.data?.total ?? 0,
|
total: data?.total ?? 0,
|
||||||
success: true,
|
success: true,
|
||||||
};
|
};
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
|
|
@ -351,20 +150,50 @@ const SiteList: React.FC = () => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFinish = async (values: any) => {
|
// 提交创建/更新逻辑;编辑时未填写密钥则不提交(保持原值)
|
||||||
|
const handleSubmit = async (values: SiteFormValues) => {
|
||||||
try {
|
try {
|
||||||
if (editing) {
|
if (editing) {
|
||||||
await sitecontrollerUpdate({ id: String(editing.id) }, values);
|
const payload: Record<string, any> = {
|
||||||
message.success('更新成功');
|
// 仅提交存在的字段,避免覆盖为 null/空
|
||||||
} else {
|
...(values.siteName ? { siteName: values.siteName } : {}),
|
||||||
await sitecontrollerCreate(values);
|
...(values.apiUrl ? { apiUrl: values.apiUrl } : {}),
|
||||||
message.success('创建成功');
|
...(values.type ? { type: values.type } : {}),
|
||||||
|
...(typeof values.isDisabled === 'boolean' ? { isDisabled: values.isDisabled } : {}),
|
||||||
|
...(values.skuPrefix ? { skuPrefix: values.skuPrefix } : {}),
|
||||||
|
};
|
||||||
|
// 仅当输入了新密钥时才提交,未输入则保持原本值
|
||||||
|
if (values.consumerKey && values.consumerKey.trim()) {
|
||||||
|
payload.consumerKey = values.consumerKey.trim();
|
||||||
}
|
}
|
||||||
|
if (values.consumerSecret && values.consumerSecret.trim()) {
|
||||||
|
payload.consumerSecret = values.consumerSecret.trim();
|
||||||
|
}
|
||||||
|
await request(`/site/update/${editing.id}`, { method: 'PUT', data: payload });
|
||||||
|
} else {
|
||||||
|
// 新增站点时要求填写 consumerKey 和 consumerSecret
|
||||||
|
if (!values.consumerKey || !values.consumerSecret) {
|
||||||
|
throw new Error('Consumer Key and Secret are required');
|
||||||
|
}
|
||||||
|
await request('/site/create', {
|
||||||
|
method: 'POST',
|
||||||
|
data: {
|
||||||
|
siteName: values.siteName,
|
||||||
|
apiUrl: values.apiUrl,
|
||||||
|
type: values.type || 'woocommerce',
|
||||||
|
consumerKey: values.consumerKey,
|
||||||
|
consumerSecret: values.consumerSecret,
|
||||||
|
skuPrefix: values.skuPrefix,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
message.success('提交成功');
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
|
setEditing(null);
|
||||||
actionRef.current?.reload();
|
actionRef.current?.reload();
|
||||||
return true;
|
return true;
|
||||||
} catch (error: any) {
|
} catch (e: any) {
|
||||||
message.error(error.message || '操作失败');
|
message.error(e?.message || '提交失败');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -372,101 +201,51 @@ const SiteList: React.FC = () => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ProTable<SiteItem>
|
<ProTable<SiteItem>
|
||||||
scroll={{ x: 'max-content' }}
|
|
||||||
actionRef={actionRef}
|
actionRef={actionRef}
|
||||||
rowKey="id"
|
rowKey="id"
|
||||||
columns={columns}
|
columns={columns}
|
||||||
request={tableRequest}
|
request={tableRequest}
|
||||||
rowSelection={{
|
|
||||||
selectedRowKeys,
|
|
||||||
onChange: setSelectedRowKeys,
|
|
||||||
}}
|
|
||||||
pagination={{
|
|
||||||
defaultPageSize: 20,
|
|
||||||
showSizeChanger: true,
|
|
||||||
showQuickJumper: true,
|
|
||||||
}}
|
|
||||||
toolBarRender={() => [
|
toolBarRender={() => [
|
||||||
<Button
|
<Button
|
||||||
|
key="new"
|
||||||
type="primary"
|
type="primary"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setEditing(null);
|
setEditing(null);
|
||||||
setOpen(true);
|
setOpen(true);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
新建站点
|
新增站点
|
||||||
</Button>,
|
|
||||||
<Button
|
|
||||||
disabled={!selectedRowKeys.length}
|
|
||||||
onClick={() => setBatchEditOpen(true)}
|
|
||||||
>
|
|
||||||
批量编辑
|
|
||||||
</Button>,
|
|
||||||
<Button
|
|
||||||
disabled={!selectedRowKeys.length}
|
|
||||||
onClick={() => handleSync(selectedRowKeys as number[])}
|
|
||||||
>
|
|
||||||
批量同步
|
|
||||||
</Button>,
|
</Button>,
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<EditSiteForm
|
<DrawerForm<SiteFormValues>
|
||||||
|
title={editing ? '编辑站点' : '新增站点'}
|
||||||
open={open}
|
open={open}
|
||||||
onOpenChange={(visible) => {
|
onOpenChange={setOpen}
|
||||||
setOpen(visible);
|
formRef={formRef}
|
||||||
if (!visible) {
|
onFinish={handleSubmit}
|
||||||
setEditing(null);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
initialValues={editing}
|
|
||||||
isEdit={!!editing}
|
|
||||||
onFinish={handleFinish}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* 批量编辑弹窗 */}
|
|
||||||
<DrawerForm
|
|
||||||
title={`批量编辑站点 (${selectedRowKeys.length} 个)`}
|
|
||||||
form={batchEditForm}
|
|
||||||
open={batchEditOpen}
|
|
||||||
onOpenChange={setBatchEditOpen}
|
|
||||||
onFinish={handleBatchEditFinish}
|
|
||||||
layout="vertical"
|
|
||||||
>
|
>
|
||||||
|
{/* 站点名称,必填 */}
|
||||||
|
<ProFormText name="siteName" label="站点名称" placeholder="例如:本地商店" rules={[{ required: true, message: '站点名称为必填项' }]} />
|
||||||
|
{/* API 地址,可选 */}
|
||||||
|
<ProFormText name="apiUrl" label="API 地址" placeholder="例如:https://shop.example.com" />
|
||||||
|
{/* 平台类型选择 */}
|
||||||
<ProFormSelect
|
<ProFormSelect
|
||||||
name="areas"
|
name="type"
|
||||||
label="区域"
|
label="平台"
|
||||||
mode="multiple"
|
options={[
|
||||||
placeholder="请选择区域(留空表示不修改)"
|
{ label: 'WooCommerce', value: 'woocommerce' },
|
||||||
showSearch
|
{ label: 'Shopyy', value: 'shopyy' },
|
||||||
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: '否',
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
|
{/* 是否禁用 */}
|
||||||
|
<ProFormSwitch name="isDisabled" label="禁用" />
|
||||||
|
<ProFormText name="skuPrefix" label="SKU 前缀" placeholder={editing ? '留空表示不修改' : '可选'} />
|
||||||
|
{/* WooCommerce REST consumer key;新增必填,编辑不填则保持原值 */}
|
||||||
|
<ProFormText name="consumerKey" label="Key" placeholder={editing ? '留空表示不修改' : '必填'} rules={editing ? [] : [{ required: true, message: 'Key 为必填项' }]} />
|
||||||
|
{/* WooCommerce REST consumer secret;新增必填,编辑不填则保持原值 */}
|
||||||
|
<ProFormText name="consumerSecret" label="Secret" placeholder={editing ? '留空表示不修改' : '必填'} rules={editing ? [] : [{ required: true, message: 'Secret 为必填项' }]} />
|
||||||
</DrawerForm>
|
</DrawerForm>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,538 +0,0 @@
|
||||||
import Address from '@/components/Address';
|
|
||||||
import {
|
|
||||||
DeleteFilled,
|
|
||||||
EditOutlined,
|
|
||||||
PlusOutlined,
|
|
||||||
UserOutlined,
|
|
||||||
} from '@ant-design/icons';
|
|
||||||
import {
|
|
||||||
ActionType,
|
|
||||||
DrawerForm,
|
|
||||||
ModalForm,
|
|
||||||
PageContainer,
|
|
||||||
ProColumns,
|
|
||||||
ProFormText,
|
|
||||||
ProFormTextArea,
|
|
||||||
ProTable,
|
|
||||||
} from '@ant-design/pro-components';
|
|
||||||
import { request, useParams } from '@umijs/max';
|
|
||||||
import { App, Avatar, Button, Modal, Popconfirm, Space, Tag } from 'antd';
|
|
||||||
import React, { useEffect, useRef, useState } from 'react';
|
|
||||||
|
|
||||||
const BatchEditCustomers: React.FC<{
|
|
||||||
tableRef: React.MutableRefObject<ActionType | undefined>;
|
|
||||||
selectedRowKeys: React.Key[];
|
|
||||||
setSelectedRowKeys: (keys: React.Key[]) => void;
|
|
||||||
siteId?: string;
|
|
||||||
}> = ({ tableRef, selectedRowKeys, setSelectedRowKeys, siteId }) => {
|
|
||||||
const { message } = App.useApp();
|
|
||||||
return (
|
|
||||||
<ModalForm
|
|
||||||
title="批量编辑客户"
|
|
||||||
trigger={
|
|
||||||
<Button
|
|
||||||
disabled={!selectedRowKeys.length}
|
|
||||||
type="primary"
|
|
||||||
icon={<EditOutlined />}
|
|
||||||
>
|
|
||||||
批量编辑
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
width={400}
|
|
||||||
modalProps={{ destroyOnHidden: true }}
|
|
||||||
onFinish={async (values) => {
|
|
||||||
if (!siteId) return false;
|
|
||||||
let ok = 0,
|
|
||||||
fail = 0;
|
|
||||||
for (const id of selectedRowKeys) {
|
|
||||||
try {
|
|
||||||
// Remove undefined values
|
|
||||||
const data = Object.fromEntries(
|
|
||||||
Object.entries(values).filter(
|
|
||||||
([_, v]) => v !== undefined && v !== '',
|
|
||||||
),
|
|
||||||
);
|
|
||||||
if (Object.keys(data).length === 0) continue;
|
|
||||||
|
|
||||||
const res = await request(`/site-api/${siteId}/customers/${id}`, {
|
|
||||||
method: 'PUT',
|
|
||||||
data: data,
|
|
||||||
});
|
|
||||||
if (res.success) ok++;
|
|
||||||
else fail++;
|
|
||||||
} catch (e) {
|
|
||||||
fail++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
message.success(`成功 ${ok}, 失败 ${fail}`);
|
|
||||||
tableRef.current?.reload();
|
|
||||||
setSelectedRowKeys([]);
|
|
||||||
return true;
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ProFormText
|
|
||||||
name="role"
|
|
||||||
label="角色"
|
|
||||||
placeholder="请输入角色,不修改请留空"
|
|
||||||
/>
|
|
||||||
<ProFormText
|
|
||||||
name="phone"
|
|
||||||
label="电话"
|
|
||||||
placeholder="请输入电话,不修改请留空"
|
|
||||||
/>
|
|
||||||
</ModalForm>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const CustomerPage: React.FC = () => {
|
|
||||||
const { message } = App.useApp();
|
|
||||||
const { siteId } = useParams<{ siteId: string }>();
|
|
||||||
const [editing, setEditing] = useState<any>(null);
|
|
||||||
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
|
|
||||||
const actionRef = useRef<ActionType>();
|
|
||||||
const [ordersVisible, setOrdersVisible] = useState<boolean>(false);
|
|
||||||
const [ordersCustomer, setOrdersCustomer] = useState<any>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// 当siteId变化时, 重新加载表格数据
|
|
||||||
if (siteId) {
|
|
||||||
actionRef.current?.reload();
|
|
||||||
}
|
|
||||||
}, [siteId]);
|
|
||||||
|
|
||||||
const handleDelete = async (id: number) => {
|
|
||||||
if (!siteId) return;
|
|
||||||
try {
|
|
||||||
const res = await request(`/site-api/${siteId}/customers/${id}`, {
|
|
||||||
method: 'DELETE',
|
|
||||||
});
|
|
||||||
if (res.success) {
|
|
||||||
message.success('删除成功');
|
|
||||||
actionRef.current?.reload();
|
|
||||||
} else {
|
|
||||||
message.error(res.message || '删除失败');
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
message.error('删除失败');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const columns: ProColumns<any>[] = [
|
|
||||||
{
|
|
||||||
title: '头像',
|
|
||||||
dataIndex: 'avatar_url',
|
|
||||||
hideInSearch: true,
|
|
||||||
width: 80,
|
|
||||||
render: (_, record) => {
|
|
||||||
// 从raw数据中获取头像URL,因为DTO中没有这个字段
|
|
||||||
const avatarUrl = record.raw?.avatar_url || record.avatar_url;
|
|
||||||
return <Avatar src={avatarUrl} icon={<UserOutlined />} 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';
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div>{username}</div>
|
|
||||||
<div style={{ fontSize: 12, color: '#888' }}>
|
|
||||||
{record.first_name} {record.last_name}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '邮箱',
|
|
||||||
dataIndex: 'email',
|
|
||||||
copyable: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '电话',
|
|
||||||
dataIndex: 'phone',
|
|
||||||
render: (_, record) =>
|
|
||||||
record.phone || record.billing?.phone || record.shipping?.phone || '-',
|
|
||||||
copyable: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '角色',
|
|
||||||
dataIndex: 'role',
|
|
||||||
render: (_, record) => {
|
|
||||||
// 角色信息可能从raw数据中获取,因为DTO中没有这个字段
|
|
||||||
const role = record.role || record.raw?.role || 'N/A';
|
|
||||||
return <Tag color="blue">{role}</Tag>;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '账单地址',
|
|
||||||
dataIndex: 'billing',
|
|
||||||
hideInSearch: true,
|
|
||||||
render: (_, record) => {
|
|
||||||
const { billing } = record;
|
|
||||||
return <Address address={billing} />;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '物流地址',
|
|
||||||
dataIndex: 'shipping',
|
|
||||||
hideInSearch: true,
|
|
||||||
render: (shipping) => {
|
|
||||||
return <Address address={shipping} />;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '创建时间',
|
|
||||||
dataIndex: 'date_created',
|
|
||||||
valueType: 'dateTime',
|
|
||||||
hideInSearch: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '更新时间',
|
|
||||||
dataIndex: 'date_modified',
|
|
||||||
valueType: 'dateTime',
|
|
||||||
hideInSearch: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '操作',
|
|
||||||
valueType: 'option',
|
|
||||||
width: 120,
|
|
||||||
fixed: 'right',
|
|
||||||
render: (_, record) => (
|
|
||||||
<Space>
|
|
||||||
<Button
|
|
||||||
type="link"
|
|
||||||
title="编辑"
|
|
||||||
icon={<EditOutlined />}
|
|
||||||
onClick={() => setEditing(record)}
|
|
||||||
/>
|
|
||||||
<Popconfirm
|
|
||||||
title="确定删除?"
|
|
||||||
onConfirm={() => handleDelete(record.id)}
|
|
||||||
>
|
|
||||||
<Button type="link" danger title="删除" icon={<DeleteFilled />} />
|
|
||||||
</Popconfirm>
|
|
||||||
<Button
|
|
||||||
type="link"
|
|
||||||
title="查询订单"
|
|
||||||
onClick={() => {
|
|
||||||
setOrdersCustomer(record);
|
|
||||||
setOrdersVisible(true);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
查询订单
|
|
||||||
</Button>
|
|
||||||
</Space>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<PageContainer
|
|
||||||
ghost
|
|
||||||
header={{
|
|
||||||
title: null,
|
|
||||||
breadcrumb: undefined,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ProTable
|
|
||||||
rowKey="id"
|
|
||||||
columns={columns}
|
|
||||||
search={{ labelWidth: 'auto' }}
|
|
||||||
options={{ reload: true }}
|
|
||||||
actionRef={actionRef}
|
|
||||||
scroll={{ x: 'max-content' }}
|
|
||||||
rowSelection={{
|
|
||||||
selectedRowKeys,
|
|
||||||
onChange: setSelectedRowKeys,
|
|
||||||
}}
|
|
||||||
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<string, 'asc' | 'desc'> | 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: current,
|
|
||||||
per_page: pageSize,
|
|
||||||
where,
|
|
||||||
...(orderObj ? { order: orderObj } : {}),
|
|
||||||
...(name || email ? { search: name || email } : {}),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.success) {
|
|
||||||
message.error(response.message || '获取客户列表失败');
|
|
||||||
return {
|
|
||||||
data: [],
|
|
||||||
total: 0,
|
|
||||||
success: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = response.data;
|
|
||||||
return {
|
|
||||||
total: data?.total || 0,
|
|
||||||
data: data?.items || [],
|
|
||||||
success: true,
|
|
||||||
};
|
|
||||||
}}
|
|
||||||
toolBarRender={() => [
|
|
||||||
<DrawerForm
|
|
||||||
title="新增客户"
|
|
||||||
trigger={
|
|
||||||
<Button type="primary" title="新增" icon={<PlusOutlined />} />
|
|
||||||
}
|
|
||||||
onFinish={async (values) => {
|
|
||||||
if (!siteId) return false;
|
|
||||||
const res = await request(`/site-api/${siteId}/customers`, {
|
|
||||||
method: 'POST',
|
|
||||||
data: values,
|
|
||||||
});
|
|
||||||
if (res.success) {
|
|
||||||
message.success('新增成功');
|
|
||||||
actionRef.current?.reload();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
message.error(res.message || '新增失败');
|
|
||||||
return false;
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ProFormText
|
|
||||||
name="email"
|
|
||||||
label="邮箱"
|
|
||||||
rules={[{ required: true }]}
|
|
||||||
/>
|
|
||||||
<ProFormText name="first_name" label="名" />
|
|
||||||
<ProFormText name="last_name" label="姓" />
|
|
||||||
<ProFormText name="username" label="用户名" />
|
|
||||||
<ProFormText name="phone" label="电话" />
|
|
||||||
</DrawerForm>,
|
|
||||||
<BatchEditCustomers
|
|
||||||
tableRef={actionRef}
|
|
||||||
selectedRowKeys={selectedRowKeys}
|
|
||||||
setSelectedRowKeys={setSelectedRowKeys}
|
|
||||||
siteId={siteId}
|
|
||||||
/>,
|
|
||||||
<Button
|
|
||||||
title="批量导出"
|
|
||||||
onClick={async () => {
|
|
||||||
if (!siteId) return;
|
|
||||||
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);
|
|
||||||
const a = document.createElement('a');
|
|
||||||
a.href = url;
|
|
||||||
a.download = 'customers.csv';
|
|
||||||
a.click();
|
|
||||||
URL.revokeObjectURL(url);
|
|
||||||
} else {
|
|
||||||
message.error(res.message || '导出失败');
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
批量导出
|
|
||||||
</Button>,
|
|
||||||
<ModalForm
|
|
||||||
title="批量导入客户"
|
|
||||||
trigger={
|
|
||||||
<Button type="primary" ghost>
|
|
||||||
批量导入
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
width={600}
|
|
||||||
modalProps={{ destroyOnHidden: true }}
|
|
||||||
onFinish={async (values) => {
|
|
||||||
if (!siteId) return false;
|
|
||||||
const csv = values.csv || '';
|
|
||||||
const items = values.items || [];
|
|
||||||
const res = await request(
|
|
||||||
`/site-api/${siteId}/customers/import`,
|
|
||||||
{ method: 'POST', data: { csv, items } },
|
|
||||||
);
|
|
||||||
if (res.success) {
|
|
||||||
message.success('导入完成');
|
|
||||||
actionRef.current?.reload();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
message.error(res.message || '导入失败');
|
|
||||||
return false;
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ProFormTextArea
|
|
||||||
name="csv"
|
|
||||||
label="CSV文本"
|
|
||||||
placeholder="粘贴CSV,首行为表头"
|
|
||||||
/>
|
|
||||||
</ModalForm>,
|
|
||||||
|
|
||||||
<Button
|
|
||||||
title="批量删除"
|
|
||||||
danger
|
|
||||||
icon={<DeleteFilled />}
|
|
||||||
onClick={async () => {
|
|
||||||
if (!siteId) return;
|
|
||||||
const res = await request(`/site-api/${siteId}/customers/batch`, {
|
|
||||||
method: 'POST',
|
|
||||||
data: { delete: selectedRowKeys },
|
|
||||||
});
|
|
||||||
actionRef.current?.reload();
|
|
||||||
setSelectedRowKeys([]);
|
|
||||||
if (res.success) {
|
|
||||||
message.success('批量删除成功');
|
|
||||||
} else {
|
|
||||||
message.warning(res.message || '部分删除失败');
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>,
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<DrawerForm
|
|
||||||
title="编辑客户"
|
|
||||||
open={!!editing}
|
|
||||||
onOpenChange={(visible) => !visible && setEditing(null)}
|
|
||||||
initialValues={editing || {}}
|
|
||||||
onFinish={async (values) => {
|
|
||||||
if (!siteId || !editing) return false;
|
|
||||||
const res = await request(
|
|
||||||
`/site-api/${siteId}/customers/${editing.id}`,
|
|
||||||
{ method: 'PUT', data: values },
|
|
||||||
);
|
|
||||||
if (res.success) {
|
|
||||||
message.success('更新成功');
|
|
||||||
actionRef.current?.reload();
|
|
||||||
setEditing(null);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
message.error(res.message || '更新失败');
|
|
||||||
return false;
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ProFormText name="email" label="邮箱" rules={[{ required: true }]} />
|
|
||||||
<ProFormText name="first_name" label="名" />
|
|
||||||
<ProFormText name="last_name" label="姓" />
|
|
||||||
<ProFormText name="username" label="用户名" />
|
|
||||||
<ProFormText name="phone" label="电话" />
|
|
||||||
</DrawerForm>
|
|
||||||
<Modal
|
|
||||||
open={ordersVisible}
|
|
||||||
onCancel={() => {
|
|
||||||
setOrdersVisible(false);
|
|
||||||
setOrdersCustomer(null);
|
|
||||||
}}
|
|
||||||
footer={null}
|
|
||||||
width={1000}
|
|
||||||
title="客户订单"
|
|
||||||
destroyOnClose
|
|
||||||
>
|
|
||||||
<ProTable
|
|
||||||
rowKey="id"
|
|
||||||
search={false}
|
|
||||||
pagination={{
|
|
||||||
pageSize: 20,
|
|
||||||
showSizeChanger: true,
|
|
||||||
showQuickJumper: true,
|
|
||||||
}}
|
|
||||||
columns={[
|
|
||||||
{ title: '订单号', dataIndex: 'number', copyable: true },
|
|
||||||
{
|
|
||||||
title: '客户邮箱',
|
|
||||||
dataIndex: 'email',
|
|
||||||
copyable: true,
|
|
||||||
render: () => {
|
|
||||||
return ordersCustomer?.email;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '支付时间',
|
|
||||||
dataIndex: 'date_paid',
|
|
||||||
valueType: 'dateTime',
|
|
||||||
hideInSearch: true,
|
|
||||||
},
|
|
||||||
{ title: '订单金额', dataIndex: 'total', hideInSearch: true },
|
|
||||||
{ title: '状态', dataIndex: 'status', hideInSearch: true },
|
|
||||||
{ title: '来源', dataIndex: 'created_via', hideInSearch: true },
|
|
||||||
{
|
|
||||||
title: '订单内容',
|
|
||||||
dataIndex: 'line_items',
|
|
||||||
hideInSearch: true,
|
|
||||||
render: (_, record) => {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
{record.line_items?.map((item: any) => (
|
|
||||||
<div key={item.id}>
|
|
||||||
{item.name} x {item.quantity}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
request={async (params) => {
|
|
||||||
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,
|
|
||||||
per_page: 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,
|
|
||||||
};
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Modal>
|
|
||||||
</PageContainer>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default CustomerPage;
|
|
||||||
|
|
@ -1,199 +0,0 @@
|
||||||
import { stockcontrollerGetallstockpoints } from '@/servers/api/stock';
|
|
||||||
import {
|
|
||||||
DrawerForm,
|
|
||||||
ProFormDependency,
|
|
||||||
ProFormSelect,
|
|
||||||
ProFormSwitch,
|
|
||||||
ProFormText,
|
|
||||||
ProFormTextArea,
|
|
||||||
} from '@ant-design/pro-components';
|
|
||||||
import { Form } from 'antd';
|
|
||||||
import * as countries from 'i18n-iso-countries';
|
|
||||||
import zhCN from 'i18n-iso-countries/langs/zh';
|
|
||||||
import React, { useEffect } from 'react';
|
|
||||||
|
|
||||||
// 定义组件的 props 类型
|
|
||||||
interface EditSiteFormProps {
|
|
||||||
open: boolean; // 控制抽屉表单的显示和隐藏
|
|
||||||
onOpenChange: (visible: boolean) => void; // 当抽屉表单显示状态改变时调用
|
|
||||||
onFinish: (values: any) => Promise<boolean | void>; // 表单提交成功时的回调
|
|
||||||
initialValues?: any; // 表单的初始值
|
|
||||||
isEdit: boolean; // 标记当前是编辑模式还是新建模式
|
|
||||||
}
|
|
||||||
|
|
||||||
const EditSiteForm: React.FC<EditSiteFormProps> = ({
|
|
||||||
open,
|
|
||||||
onOpenChange,
|
|
||||||
onFinish,
|
|
||||||
initialValues,
|
|
||||||
isEdit,
|
|
||||||
}) => {
|
|
||||||
const [form] = Form.useForm();
|
|
||||||
|
|
||||||
// 初始化中文语言包
|
|
||||||
countries.registerLocale(zhCN);
|
|
||||||
|
|
||||||
// 当 initialValues 或 open 状态变化时, 更新表单的值
|
|
||||||
useEffect(() => {
|
|
||||||
// 如果抽屉是打开的
|
|
||||||
if (open) {
|
|
||||||
// 如果是编辑模式并且有初始值
|
|
||||||
if (isEdit && initialValues) {
|
|
||||||
// 编辑模式下, 设置表单值为初始值
|
|
||||||
const { token, consumerKey, consumerSecret, ...safeInitialValues } =
|
|
||||||
initialValues;
|
|
||||||
// 清空敏感字段, 让用户输入最新的数据
|
|
||||||
form.setFieldsValue({
|
|
||||||
...safeInitialValues,
|
|
||||||
isDisabled: initialValues.isDisabled === 1, // 将后端的 1/0 转换成 true/false
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// 新建模式或抽屉关闭时, 重置表单
|
|
||||||
form.resetFields();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [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 (
|
|
||||||
<DrawerForm
|
|
||||||
title={isEdit ? '编辑站点' : '新建站点'}
|
|
||||||
form={form}
|
|
||||||
open={open}
|
|
||||||
onOpenChange={onOpenChange}
|
|
||||||
onFinish={async (values) => {
|
|
||||||
// 直接将表单值传递给 onFinish 回调
|
|
||||||
// 后端需要布尔值, 而 ProFormSwitch 已经提供了布尔值
|
|
||||||
return onFinish(values);
|
|
||||||
}}
|
|
||||||
layout="vertical"
|
|
||||||
>
|
|
||||||
{JSON.stringify(initialValues)}
|
|
||||||
<ProFormText
|
|
||||||
name="name"
|
|
||||||
label="名称"
|
|
||||||
rules={[{ required: true, message: '请输入名称' }]}
|
|
||||||
placeholder="请输入名称"
|
|
||||||
/>
|
|
||||||
<ProFormTextArea
|
|
||||||
name="description"
|
|
||||||
label="描述"
|
|
||||||
placeholder="请输入描述"
|
|
||||||
/>
|
|
||||||
<ProFormText
|
|
||||||
name="apiUrl"
|
|
||||||
label="API 地址"
|
|
||||||
rules={[{ required: true, message: '请输入 API 地址' }]}
|
|
||||||
placeholder="请输入 API 地址"
|
|
||||||
/>
|
|
||||||
<ProFormText
|
|
||||||
name="websiteUrl"
|
|
||||||
label="网站地址"
|
|
||||||
placeholder="请输入网站地址"
|
|
||||||
/>
|
|
||||||
<ProFormText
|
|
||||||
name="webhookUrl"
|
|
||||||
label="Webhook 地址"
|
|
||||||
placeholder="请输入 Webhook 地址"
|
|
||||||
/>
|
|
||||||
<ProFormSelect
|
|
||||||
name="type"
|
|
||||||
label="平台"
|
|
||||||
options={[
|
|
||||||
{ label: 'WooCommerce', value: 'woocommerce' },
|
|
||||||
{ label: 'Shopyy', value: 'shopyy' },
|
|
||||||
]}
|
|
||||||
rules={[{ required: true, message: '请选择平台' }]}
|
|
||||||
placeholder="请选择平台"
|
|
||||||
/>
|
|
||||||
{/* 根据选择的平台动态显示不同的认证字段 */}
|
|
||||||
<ProFormDependency name={['type']}>
|
|
||||||
{({ type }) => {
|
|
||||||
// 如果平台是 woocommerce
|
|
||||||
if (type === 'woocommerce') {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<ProFormText
|
|
||||||
name="consumerKey"
|
|
||||||
label="Consumer Key"
|
|
||||||
rules={[
|
|
||||||
{ required: !isEdit, message: '请输入 Consumer Key' },
|
|
||||||
]}
|
|
||||||
placeholder={
|
|
||||||
isEdit ? '留空表示不修改' : '请输入 Consumer Key'
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<ProFormText
|
|
||||||
name="consumerSecret"
|
|
||||||
label="Consumer Secret"
|
|
||||||
rules={[
|
|
||||||
{ required: !isEdit, message: '请输入 Consumer Secret' },
|
|
||||||
]}
|
|
||||||
placeholder={
|
|
||||||
isEdit ? '留空表示不修改' : '请输入 Consumer Secret'
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
// 如果平台是 shopyy
|
|
||||||
if (type === 'shopyy') {
|
|
||||||
return (
|
|
||||||
<ProFormText
|
|
||||||
name="token"
|
|
||||||
label="Token"
|
|
||||||
rules={[{ required: !isEdit, message: '请输入 Token' }]}
|
|
||||||
placeholder={isEdit ? '留空表示不修改' : '请输入 Token'}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}}
|
|
||||||
</ProFormDependency>
|
|
||||||
<ProFormText
|
|
||||||
name="skuPrefix"
|
|
||||||
label="SKU 前缀"
|
|
||||||
placeholder="请输入 SKU 前缀"
|
|
||||||
/>
|
|
||||||
<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="是否禁用" />
|
|
||||||
</DrawerForm>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default EditSiteForm;
|
|
||||||
|
|
@ -1,184 +0,0 @@
|
||||||
import { sitecontrollerAll } from '@/servers/api/site';
|
|
||||||
import { EditOutlined } from '@ant-design/icons';
|
|
||||||
import { PageContainer } from '@ant-design/pro-components';
|
|
||||||
import { Outlet, history, request, useLocation, useParams } from '@umijs/max';
|
|
||||||
import { Button, Col, Menu, Row, Select, Spin, message } from 'antd';
|
|
||||||
import Sider from 'antd/es/layout/Sider';
|
|
||||||
import React, { useEffect, useState } from 'react';
|
|
||||||
import type { SiteItem } from '../List/index';
|
|
||||||
import EditSiteForm from './EditSiteForm';
|
|
||||||
|
|
||||||
const ShopLayout: React.FC = () => {
|
|
||||||
const [sites, setSites] = useState<any[]>([]);
|
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
const { siteId } = useParams<{ siteId: string }>();
|
|
||||||
const location = useLocation();
|
|
||||||
|
|
||||||
const [editModalOpen, setEditModalOpen] = useState(false);
|
|
||||||
const [editingSite, setEditingSite] = useState<SiteItem & { areas: string[] } | null>(null);
|
|
||||||
|
|
||||||
const fetchSites = async () => {
|
|
||||||
try {
|
|
||||||
setLoading(true);
|
|
||||||
const { data = [] } = await sitecontrollerAll();
|
|
||||||
setSites(data);
|
|
||||||
|
|
||||||
if (!siteId && data.length > 0) {
|
|
||||||
history.replace(`/site/shop/${data[0].id}/products`);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to fetch sites', error);
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetchSites();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleSiteChange = (value: number) => {
|
|
||||||
const currentPath = location.pathname;
|
|
||||||
const parts = currentPath.split('/');
|
|
||||||
if (parts.length >= 5) {
|
|
||||||
parts[3] = String(value);
|
|
||||||
history.push(parts.join('/'));
|
|
||||||
} else {
|
|
||||||
history.push(`/site/shop/${value}/products`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleMenuClick = (e: { key: string }) => {
|
|
||||||
if (!siteId) return;
|
|
||||||
history.push(`/site/shop/${siteId}/${e.key}`);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getSelectedKey = () => {
|
|
||||||
const parts = location.pathname.split('/');
|
|
||||||
if (parts.length >= 5) {
|
|
||||||
return parts[4];
|
|
||||||
}
|
|
||||||
return 'products';
|
|
||||||
};
|
|
||||||
|
|
||||||
if (loading) {
|
|
||||||
return (
|
|
||||||
<Spin
|
|
||||||
size="large"
|
|
||||||
style={{ display: 'flex', justifyContent: 'center', marginTop: 100 }}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleFinish = async (values: any) => {
|
|
||||||
if (!editingSite) {
|
|
||||||
message.error('未找到要编辑的站点');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
await request(`/site/${editingSite.id}`, {
|
|
||||||
method: 'PUT',
|
|
||||||
data: values,
|
|
||||||
});
|
|
||||||
message.success('更新成功');
|
|
||||||
setEditModalOpen(false);
|
|
||||||
fetchSites(); // 重新获取站点列表以更新数据
|
|
||||||
return true;
|
|
||||||
} catch (error: any) {
|
|
||||||
message.error(error.message || '操作失败');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<PageContainer header={{ title: null, breadcrumb: undefined }}>
|
|
||||||
<Row gutter={16} style={{ height: 'calc(100vh - 100px)' }}>
|
|
||||||
<Col span={4} style={{ height: '100%' }}>
|
|
||||||
<Sider
|
|
||||||
style={{ background: 'white', height: '100%', overflow: 'hidden', zIndex: 1 }}
|
|
||||||
>
|
|
||||||
<div style={{ padding: '0 10px 16px' }}>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
flexWrap: 'wrap',
|
|
||||||
gap: '4px'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Select
|
|
||||||
style={{ flex: 1 }}
|
|
||||||
placeholder="请选择店铺"
|
|
||||||
options={sites?.map?.((site) => ({
|
|
||||||
label: site.name,
|
|
||||||
value: site.id,
|
|
||||||
}))}
|
|
||||||
value={siteId ? Number(siteId) : undefined}
|
|
||||||
onChange={handleSiteChange}
|
|
||||||
showSearch
|
|
||||||
optionFilterProp="label"
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
icon={<EditOutlined />}
|
|
||||||
onClick={() => {
|
|
||||||
const currentSite = sites.find(
|
|
||||||
(site) => site.id === Number(siteId),
|
|
||||||
);
|
|
||||||
if (currentSite) {
|
|
||||||
function normalizeEditing(site: SiteItem) {
|
|
||||||
return {
|
|
||||||
...site,
|
|
||||||
areas: site.areas?.map(area => area.code) || [],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setEditingSite(normalizeEditing(currentSite));
|
|
||||||
setEditModalOpen(true);
|
|
||||||
} else {
|
|
||||||
message.warning('请先选择一个店铺');
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div style={{ flex: 1, overflowY: 'auto' }}>
|
|
||||||
<Menu
|
|
||||||
mode="inline"
|
|
||||||
selectedKeys={[getSelectedKey()]}
|
|
||||||
onClick={handleMenuClick}
|
|
||||||
style={{ borderRight: 0 }}
|
|
||||||
items={[
|
|
||||||
{ key: 'products', label: '产品管理' },
|
|
||||||
{ key: 'orders', label: '订单管理' },
|
|
||||||
{ key: 'subscriptions', label: '订阅管理' },
|
|
||||||
{ key: 'media', label: '媒体管理' },
|
|
||||||
{ key: 'customers', label: '客户管理' },
|
|
||||||
{ key: 'reviews', label: '评论管理' },
|
|
||||||
{ key: 'webhooks', label: 'Webhooks管理' },
|
|
||||||
{ key: 'links', label: '链接管理' },
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Sider>
|
|
||||||
</Col>
|
|
||||||
<Col span={20} style={{ height: '100%', overflowY: 'auto' }}>
|
|
||||||
{siteId ? <Outlet /> : <div>请选择店铺</div>}
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
|
|
||||||
<EditSiteForm
|
|
||||||
open={editModalOpen}
|
|
||||||
onOpenChange={(visible: boolean) => {
|
|
||||||
setEditModalOpen(visible);
|
|
||||||
if (!visible) {
|
|
||||||
setEditingSite(null);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
initialValues={editingSite}
|
|
||||||
isEdit={!!editingSite}
|
|
||||||
onFinish={handleFinish}
|
|
||||||
/>
|
|
||||||
</PageContainer>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ShopLayout;
|
|
||||||
|
|
@ -1,99 +0,0 @@
|
||||||
import { LinkOutlined } from '@ant-design/icons';
|
|
||||||
import { PageHeader } from '@ant-design/pro-layout';
|
|
||||||
import { request, useParams } from '@umijs/max';
|
|
||||||
import { App, Button, Card, List } from 'antd';
|
|
||||||
import React, { useEffect, useState } from 'react';
|
|
||||||
|
|
||||||
// 定义链接项的类型
|
|
||||||
interface LinkItem {
|
|
||||||
title: string;
|
|
||||||
url: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const LinksPage: React.FC = () => {
|
|
||||||
const { siteId } = useParams<{ siteId: string }>();
|
|
||||||
const { message: antMessage } = App.useApp();
|
|
||||||
const [links, setLinks] = useState<LinkItem[]>([]);
|
|
||||||
const [loading, setLoading] = useState<boolean>(true);
|
|
||||||
|
|
||||||
// 获取链接列表的函数
|
|
||||||
const fetchLinks = async () => {
|
|
||||||
if (!siteId) return;
|
|
||||||
|
|
||||||
setLoading(true);
|
|
||||||
try {
|
|
||||||
const response = await request(`/site-api/${siteId}/links`);
|
|
||||||
if (response.success && response.data) {
|
|
||||||
setLinks(response.data);
|
|
||||||
} else {
|
|
||||||
antMessage.error(response.message || '获取链接列表失败');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
antMessage.error('获取链接列表失败');
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 页面加载时获取链接列表
|
|
||||||
useEffect(() => {
|
|
||||||
fetchLinks();
|
|
||||||
}, [siteId]);
|
|
||||||
|
|
||||||
// 处理链接点击事件,在新标签页打开
|
|
||||||
const handleLinkClick = (url: string) => {
|
|
||||||
window.open(url, '_blank', 'noopener,noreferrer');
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<PageHeader title="站点链接" breadcrumb={{ items: [] }} />
|
|
||||||
<Card
|
|
||||||
title="常用链接"
|
|
||||||
bordered={false}
|
|
||||||
extra={
|
|
||||||
<Button type="primary" onClick={fetchLinks} loading={loading}>
|
|
||||||
刷新列表
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<List
|
|
||||||
loading={loading}
|
|
||||||
dataSource={links}
|
|
||||||
renderItem={(item) => (
|
|
||||||
<List.Item
|
|
||||||
key={item.title}
|
|
||||||
actions={[
|
|
||||||
<Button
|
|
||||||
key={`visit-${item.title}`}
|
|
||||||
type="link"
|
|
||||||
icon={<LinkOutlined />}
|
|
||||||
onClick={() => handleLinkClick(item.url)}
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
访问
|
|
||||||
</Button>,
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<List.Item.Meta
|
|
||||||
title={item.title}
|
|
||||||
description={
|
|
||||||
<a
|
|
||||||
href={item.url}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
style={{ color: '#1890ff' }}
|
|
||||||
>
|
|
||||||
{item.url}
|
|
||||||
</a>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</List.Item>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default LinksPage;
|
|
||||||
|
|
@ -1,256 +0,0 @@
|
||||||
import {
|
|
||||||
logisticscontrollerDeleteshipment,
|
|
||||||
logisticscontrollerGetlist,
|
|
||||||
logisticscontrollerGetshipmentlabel,
|
|
||||||
logisticscontrollerUpdateshipmentstate,
|
|
||||||
} from '@/servers/api/logistics';
|
|
||||||
import { stockcontrollerGetallstockpoints } from '@/servers/api/stock';
|
|
||||||
import { formatUniuniShipmentState } from '@/utils/format';
|
|
||||||
import { printPDF } from '@/utils/util';
|
|
||||||
import {
|
|
||||||
CopyOutlined,
|
|
||||||
DeleteFilled,
|
|
||||||
FilePdfOutlined,
|
|
||||||
ReloadOutlined,
|
|
||||||
} from '@ant-design/icons';
|
|
||||||
import {
|
|
||||||
ActionType,
|
|
||||||
PageContainer,
|
|
||||||
ProColumns,
|
|
||||||
ProTable,
|
|
||||||
} from '@ant-design/pro-components';
|
|
||||||
import { useParams } from '@umijs/max';
|
|
||||||
import { App, Button, Divider, Popconfirm, Space } from 'antd';
|
|
||||||
import React, { useRef, useState } from 'react';
|
|
||||||
import { ToastContainer } from 'react-toastify';
|
|
||||||
|
|
||||||
const LogisticsPage: React.FC = () => {
|
|
||||||
const actionRef = useRef<ActionType>();
|
|
||||||
const { message } = App.useApp();
|
|
||||||
const [selectedRows, setSelectedRows] = useState<API.Service[]>([]);
|
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
|
||||||
const { siteId } = useParams<{ siteId: string }>();
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
actionRef.current?.reload();
|
|
||||||
}, [siteId]);
|
|
||||||
|
|
||||||
const columns: ProColumns<API.Service>[] = [
|
|
||||||
{
|
|
||||||
title: '服务商',
|
|
||||||
dataIndex: 'tracking_provider',
|
|
||||||
hideInSearch: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '仓库',
|
|
||||||
dataIndex: 'stockPointId',
|
|
||||||
// hideInTable: true,
|
|
||||||
valueType: 'select',
|
|
||||||
request: async () => {
|
|
||||||
const { data = [] } = await stockcontrollerGetallstockpoints();
|
|
||||||
return data.map((item) => ({
|
|
||||||
label: item.name,
|
|
||||||
value: item.id,
|
|
||||||
}));
|
|
||||||
},
|
|
||||||
},
|
|
||||||
// Site column removed
|
|
||||||
{
|
|
||||||
title: '订单号',
|
|
||||||
dataIndex: 'externalOrderId',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '快递单号',
|
|
||||||
dataIndex: 'return_tracking_number',
|
|
||||||
render(_, record) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{record.return_tracking_number}
|
|
||||||
<CopyOutlined
|
|
||||||
onClick={async () => {
|
|
||||||
try {
|
|
||||||
await navigator.clipboard.writeText(
|
|
||||||
record.return_tracking_number,
|
|
||||||
);
|
|
||||||
message.success('复制成功!');
|
|
||||||
} catch (err) {
|
|
||||||
message.error('复制失败!');
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '状态',
|
|
||||||
dataIndex: 'state',
|
|
||||||
hideInSearch: true,
|
|
||||||
render(_, record) {
|
|
||||||
return formatUniuniShipmentState(record.state);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '创建时间',
|
|
||||||
dataIndex: 'createdAt',
|
|
||||||
hideInSearch: true,
|
|
||||||
valueType: 'dateTime',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '操作',
|
|
||||||
dataIndex: 'operation',
|
|
||||||
hideInSearch: true,
|
|
||||||
render(_, record) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Button
|
|
||||||
type="primary"
|
|
||||||
title="打印标签"
|
|
||||||
icon={<FilePdfOutlined />}
|
|
||||||
disabled={isLoading}
|
|
||||||
onClick={async () => {
|
|
||||||
setIsLoading(true);
|
|
||||||
const { data } = await logisticscontrollerGetshipmentlabel({
|
|
||||||
shipmentId: record.id,
|
|
||||||
});
|
|
||||||
const content = data.content;
|
|
||||||
printPDF([content]);
|
|
||||||
setIsLoading(false);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Divider type="vertical" />
|
|
||||||
<Button
|
|
||||||
type="primary"
|
|
||||||
title="刷新状态"
|
|
||||||
icon={<ReloadOutlined />}
|
|
||||||
disabled={isLoading}
|
|
||||||
onClick={async () => {
|
|
||||||
setIsLoading(true);
|
|
||||||
const res = await logisticscontrollerUpdateshipmentstate({
|
|
||||||
shipmentId: record.id,
|
|
||||||
});
|
|
||||||
console.log('res', res);
|
|
||||||
|
|
||||||
setIsLoading(false);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Divider type="vertical" />
|
|
||||||
<Popconfirm
|
|
||||||
disabled={isLoading}
|
|
||||||
title="删除"
|
|
||||||
description="确认删除?"
|
|
||||||
onConfirm={async () => {
|
|
||||||
try {
|
|
||||||
setIsLoading(true);
|
|
||||||
const { success, message: errMsg } =
|
|
||||||
await logisticscontrollerDeleteshipment({ id: record.id });
|
|
||||||
if (!success) {
|
|
||||||
throw new Error(errMsg);
|
|
||||||
}
|
|
||||||
setIsLoading(false);
|
|
||||||
actionRef.current?.reload();
|
|
||||||
} catch (error: any) {
|
|
||||||
setIsLoading(false);
|
|
||||||
message.error(error.message);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
type="primary"
|
|
||||||
danger
|
|
||||||
title="删除"
|
|
||||||
icon={<DeleteFilled />}
|
|
||||||
/>
|
|
||||||
</Popconfirm>
|
|
||||||
<ToastContainer />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const handleBatchPrint = async () => {
|
|
||||||
if (selectedRows.length === 0) {
|
|
||||||
message.warning('请选择要打印的项');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// @ts-ignore
|
|
||||||
await printPDF(
|
|
||||||
selectedRows.map((row) => row.labels[row.labels.length - 1].url),
|
|
||||||
);
|
|
||||||
|
|
||||||
setSelectedRows([]);
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
<PageContainer ghost header={{ title: null, breadcrumb: undefined }}>
|
|
||||||
<ProTable
|
|
||||||
headerTitle="查询表格"
|
|
||||||
actionRef={actionRef}
|
|
||||||
rowKey="id"
|
|
||||||
request={async (values) => {
|
|
||||||
console.log(values);
|
|
||||||
const params = { ...values };
|
|
||||||
if (siteId) {
|
|
||||||
params.siteId = Number(siteId);
|
|
||||||
}
|
|
||||||
const {
|
|
||||||
data,
|
|
||||||
success,
|
|
||||||
message: errMsg,
|
|
||||||
} = await logisticscontrollerGetlist({
|
|
||||||
params,
|
|
||||||
});
|
|
||||||
if (success) {
|
|
||||||
return {
|
|
||||||
total: data?.total || 0,
|
|
||||||
data: data?.items || [],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
message.error(errMsg || '获取物流列表失败');
|
|
||||||
return {
|
|
||||||
data: [],
|
|
||||||
success: false,
|
|
||||||
};
|
|
||||||
}}
|
|
||||||
rowSelection={{
|
|
||||||
selectedRowKeys: selectedRows.map((row) => row.id),
|
|
||||||
onChange: (_, selectedRows) => setSelectedRows(selectedRows),
|
|
||||||
}}
|
|
||||||
columns={columns}
|
|
||||||
tableAlertOptionRender={() => {
|
|
||||||
return (
|
|
||||||
<Space>
|
|
||||||
<Button onClick={handleBatchPrint} type="primary">
|
|
||||||
批量打印
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
danger
|
|
||||||
type="primary"
|
|
||||||
onClick={async () => {
|
|
||||||
try {
|
|
||||||
setIsLoading(true);
|
|
||||||
let ok = 0;
|
|
||||||
for (const row of selectedRows) {
|
|
||||||
const { success } =
|
|
||||||
await logisticscontrollerDeleteshipment({ id: row.id });
|
|
||||||
if (success) ok++;
|
|
||||||
}
|
|
||||||
message.success(`成功删除 ${ok} 条`);
|
|
||||||
setIsLoading(false);
|
|
||||||
actionRef.current?.reload();
|
|
||||||
setSelectedRows([]);
|
|
||||||
} catch (e) {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
批量删除
|
|
||||||
</Button>
|
|
||||||
</Space>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</PageContainer>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
export default LogisticsPage;
|
|
||||||
|
|
@ -1,416 +0,0 @@
|
||||||
import { siteapicontrollerCreatemedia } from '@/servers/api/siteApi';
|
|
||||||
import { DeleteOutlined, EditOutlined, PlusOutlined } from '@ant-design/icons';
|
|
||||||
import {
|
|
||||||
ModalForm,
|
|
||||||
PageContainer,
|
|
||||||
ProColumns,
|
|
||||||
ProFormText,
|
|
||||||
ProFormUploadButton,
|
|
||||||
ProTable,
|
|
||||||
} from '@ant-design/pro-components';
|
|
||||||
import { request, useParams } from '@umijs/max';
|
|
||||||
import { App, Button, Image, Popconfirm, Space } from 'antd';
|
|
||||||
import React, { useState } from 'react';
|
|
||||||
|
|
||||||
const MediaPage: React.FC = () => {
|
|
||||||
const { message } = App.useApp();
|
|
||||||
const { siteId } = useParams<{ siteId: string }>();
|
|
||||||
const [editing, setEditing] = useState<any>(null);
|
|
||||||
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
|
|
||||||
const actionRef = React.useRef<any>(null);
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
actionRef.current?.reload();
|
|
||||||
}, [siteId]);
|
|
||||||
|
|
||||||
const handleDelete = async (id: number) => {
|
|
||||||
if (!siteId) return;
|
|
||||||
try {
|
|
||||||
const res = await request(`/site-api/${siteId}/media/${id}`, {
|
|
||||||
method: 'DELETE',
|
|
||||||
});
|
|
||||||
if (res.success) {
|
|
||||||
message.success('删除成功');
|
|
||||||
actionRef.current?.reload();
|
|
||||||
} else {
|
|
||||||
message.error(res.message || '删除失败');
|
|
||||||
}
|
|
||||||
} catch (error: any) {
|
|
||||||
message.error(error.message || '删除失败');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleUpdate = async (id: number, data: any) => {
|
|
||||||
if (!siteId) return false;
|
|
||||||
try {
|
|
||||||
const res = await request(`/site-api/${siteId}/media/${id}`, {
|
|
||||||
method: 'PUT',
|
|
||||||
data,
|
|
||||||
});
|
|
||||||
if (res.success) {
|
|
||||||
message.success('更新成功');
|
|
||||||
actionRef.current?.reload();
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
message.error(res.message || '更新失败');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
} catch (error: any) {
|
|
||||||
message.error(error.message || '更新失败');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const columns: ProColumns<any>[] = [
|
|
||||||
{
|
|
||||||
title: 'ID',
|
|
||||||
dataIndex: 'id',
|
|
||||||
hideInSearch: true,
|
|
||||||
width: 120,
|
|
||||||
copyable: true,
|
|
||||||
render: (_, record) => {
|
|
||||||
return record?.id ?? '-';
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '展示',
|
|
||||||
dataIndex: 'source_url',
|
|
||||||
hideInSearch: true,
|
|
||||||
render: (_, record) => (
|
|
||||||
<Image
|
|
||||||
src={record.source_url}
|
|
||||||
style={{
|
|
||||||
width: 60,
|
|
||||||
height: 60,
|
|
||||||
objectFit: 'contain',
|
|
||||||
background: '#f0f0f0',
|
|
||||||
}}
|
|
||||||
fallback="https://via.placeholder.com/60?text=No+Img"
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '名称',
|
|
||||||
dataIndex: 'title',
|
|
||||||
copyable: true,
|
|
||||||
ellipsis: true,
|
|
||||||
width: 200,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '地址',
|
|
||||||
dataIndex: 'source_url',
|
|
||||||
copyable: true,
|
|
||||||
ellipsis: true,
|
|
||||||
hideInSearch: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '媒体类型',
|
|
||||||
dataIndex: 'media_type',
|
|
||||||
width: 120,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'MIME类型',
|
|
||||||
dataIndex: 'mime_type',
|
|
||||||
width: 120,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// 文件大小列
|
|
||||||
title: '文件大小',
|
|
||||||
dataIndex: 'file_size',
|
|
||||||
hideInSearch: true,
|
|
||||||
width: 120,
|
|
||||||
render: (_: any, record: any) => {
|
|
||||||
// 获取文件大小
|
|
||||||
const fileSize = record.file_size;
|
|
||||||
// 如果文件大小不存在,则直接返回-
|
|
||||||
if (!fileSize) {
|
|
||||||
return '-';
|
|
||||||
}
|
|
||||||
// 如果文件大小小于1024,则单位为B
|
|
||||||
if (fileSize < 1024) {
|
|
||||||
return `${fileSize} B`;
|
|
||||||
// 如果文件大小小于1024*1024,则单位为KB
|
|
||||||
} else if (fileSize < 1024 * 1024) {
|
|
||||||
return `${(fileSize / 1024).toFixed(2)} KB`;
|
|
||||||
// 否则单位为MB
|
|
||||||
} else {
|
|
||||||
return `${(fileSize / (1024 * 1024)).toFixed(2)} MB`;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '创建时间',
|
|
||||||
dataIndex: 'date_created',
|
|
||||||
valueType: 'dateTime',
|
|
||||||
hideInSearch: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '操作',
|
|
||||||
valueType: 'option',
|
|
||||||
width: 160,
|
|
||||||
fixed: 'right',
|
|
||||||
render: (_, record) => (
|
|
||||||
<Space>
|
|
||||||
<Button
|
|
||||||
type="link"
|
|
||||||
title="编辑"
|
|
||||||
icon={<EditOutlined />}
|
|
||||||
onClick={() => {
|
|
||||||
setEditing(record);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
编辑
|
|
||||||
</Button>
|
|
||||||
<Popconfirm
|
|
||||||
title="确定删除吗?"
|
|
||||||
onConfirm={() => handleDelete(record.id)}
|
|
||||||
okText="确定"
|
|
||||||
cancelText="取消"
|
|
||||||
>
|
|
||||||
<Button type="link" danger title="删除" icon={<DeleteOutlined />}>
|
|
||||||
删除
|
|
||||||
</Button>
|
|
||||||
</Popconfirm>
|
|
||||||
</Space>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<PageContainer
|
|
||||||
ghost
|
|
||||||
header={{
|
|
||||||
title: null,
|
|
||||||
breadcrumb: undefined,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ProTable
|
|
||||||
rowKey="id"
|
|
||||||
actionRef={actionRef}
|
|
||||||
columns={columns}
|
|
||||||
rowSelection={{ selectedRowKeys, onChange: setSelectedRowKeys }}
|
|
||||||
scroll={{ x: 'max-content' }}
|
|
||||||
request={async (params, sort) => {
|
|
||||||
if (!siteId) return { data: [], total: 0 };
|
|
||||||
const { current, pageSize } = params || {};
|
|
||||||
let orderObj: Record<string, 'asc' | 'desc'> | 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: current,
|
|
||||||
per_page: pageSize,
|
|
||||||
...(orderObj ? { order: orderObj } : {}),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.success) {
|
|
||||||
message.error(response.message || '获取媒体列表失败');
|
|
||||||
return {
|
|
||||||
data: [],
|
|
||||||
total: 0,
|
|
||||||
success: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// 从API响应中正确获取数据,API响应结构为 { success, message, data, code }
|
|
||||||
const data = response.data;
|
|
||||||
return {
|
|
||||||
total: data?.total || 0,
|
|
||||||
data: data?.items || [],
|
|
||||||
success: true,
|
|
||||||
};
|
|
||||||
}}
|
|
||||||
search={false}
|
|
||||||
options={{ reload: true }}
|
|
||||||
toolBarRender={() => [
|
|
||||||
<ModalForm
|
|
||||||
title="上传媒体"
|
|
||||||
trigger={
|
|
||||||
<Button type="primary" title="上传媒体" icon={<PlusOutlined />}>
|
|
||||||
上传媒体
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
width={500}
|
|
||||||
onFinish={async (values) => {
|
|
||||||
if (!siteId) return false;
|
|
||||||
try {
|
|
||||||
const formData = new FormData();
|
|
||||||
formData.append('siteId', siteId);
|
|
||||||
if (values.file && values.file.length > 0) {
|
|
||||||
values.file.forEach((f: any) => {
|
|
||||||
formData.append('file', f.originFileObj);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
message.warning('请选择文件');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const res = await siteapicontrollerCreatemedia({
|
|
||||||
body: formData,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (res.success) {
|
|
||||||
message.success('上传成功');
|
|
||||||
actionRef.current?.reload();
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
message.error(res.message || '上传失败');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
} catch (error: any) {
|
|
||||||
message.error(error.message || '上传失败');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ProFormUploadButton
|
|
||||||
name="file"
|
|
||||||
label="文件"
|
|
||||||
fieldProps={{
|
|
||||||
name: 'file',
|
|
||||||
listType: 'picture-card',
|
|
||||||
}}
|
|
||||||
rules={[{ required: true, message: '请选择文件' }]}
|
|
||||||
/>
|
|
||||||
</ModalForm>,
|
|
||||||
<Button
|
|
||||||
title="批量导出"
|
|
||||||
onClick={async () => {
|
|
||||||
if (!siteId) return;
|
|
||||||
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);
|
|
||||||
const a = document.createElement('a');
|
|
||||||
a.href = url;
|
|
||||||
a.download = 'media.csv';
|
|
||||||
a.click();
|
|
||||||
URL.revokeObjectURL(url);
|
|
||||||
} else {
|
|
||||||
message.error(res.message || '导出失败');
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
批量导出
|
|
||||||
</Button>,
|
|
||||||
<Popconfirm
|
|
||||||
title="确定批量删除选中项吗?"
|
|
||||||
okText="确定"
|
|
||||||
cancelText="取消"
|
|
||||||
disabled={!selectedRowKeys.length}
|
|
||||||
onConfirm={async () => {
|
|
||||||
// 条件判断 如果站点編號不存在則直接返回
|
|
||||||
if (!siteId) return;
|
|
||||||
// 发起批量删除请求
|
|
||||||
const response = await request(
|
|
||||||
`/site-api/${siteId}/media/batch`,
|
|
||||||
{ method: 'POST', data: { delete: selectedRowKeys } },
|
|
||||||
);
|
|
||||||
// 条件判断 根据接口返回结果进行提示
|
|
||||||
if (response.success) {
|
|
||||||
message.success('批量删除成功');
|
|
||||||
} else {
|
|
||||||
message.warning(response.message || '部分删除失败');
|
|
||||||
}
|
|
||||||
// 清空已选择的行鍵值
|
|
||||||
setSelectedRowKeys([]);
|
|
||||||
// 刷新列表数据
|
|
||||||
actionRef.current?.reload();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
title="批量删除"
|
|
||||||
danger
|
|
||||||
icon={<DeleteOutlined />}
|
|
||||||
disabled={!selectedRowKeys.length}
|
|
||||||
>
|
|
||||||
批量删除
|
|
||||||
</Button>
|
|
||||||
</Popconfirm>,
|
|
||||||
|
|
||||||
<Button
|
|
||||||
title="批量转换为WebP"
|
|
||||||
disabled={!selectedRowKeys.length}
|
|
||||||
onClick={async () => {
|
|
||||||
// 条件判断 如果站点編號不存在則直接返回
|
|
||||||
if (!siteId) return;
|
|
||||||
try {
|
|
||||||
// 发起后端批量转换请求
|
|
||||||
const response = await request(
|
|
||||||
`/site-api/${siteId}/media/convert-webp`,
|
|
||||||
{
|
|
||||||
method: 'POST',
|
|
||||||
data: { ids: selectedRowKeys },
|
|
||||||
},
|
|
||||||
);
|
|
||||||
// 条件判断 根据接口返回结果进行提示
|
|
||||||
if (response.success) {
|
|
||||||
const convertedCount = response?.data?.converted?.length || 0;
|
|
||||||
const failedCount = response?.data?.failed?.length || 0;
|
|
||||||
if (failedCount > 0) {
|
|
||||||
message.warning(
|
|
||||||
`部分转换失败 已转换 ${convertedCount} 失败 ${failedCount}`,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
message.success(`转换成功 已转换 ${convertedCount}`);
|
|
||||||
}
|
|
||||||
// 刷新列表数据
|
|
||||||
actionRef.current?.reload();
|
|
||||||
} else {
|
|
||||||
message.error(response.message || '转换失败');
|
|
||||||
}
|
|
||||||
} catch (error: any) {
|
|
||||||
message.error(error.message || '转换失败');
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
批量转换为WebP
|
|
||||||
</Button>,
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ModalForm
|
|
||||||
title="编辑媒体信息"
|
|
||||||
open={!!editing}
|
|
||||||
onOpenChange={(visible) => {
|
|
||||||
if (!visible) setEditing(null);
|
|
||||||
}}
|
|
||||||
initialValues={{
|
|
||||||
title: editing?.title,
|
|
||||||
}}
|
|
||||||
modalProps={{
|
|
||||||
destroyOnClose: true,
|
|
||||||
}}
|
|
||||||
onFinish={async (values) => {
|
|
||||||
if (!editing) return false;
|
|
||||||
const success = await handleUpdate(editing.id, values);
|
|
||||||
if (success) {
|
|
||||||
setEditing(null);
|
|
||||||
}
|
|
||||||
return success;
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ProFormText
|
|
||||||
name="title"
|
|
||||||
label="标题"
|
|
||||||
placeholder="请输入标题"
|
|
||||||
rules={[{ required: true, message: '请输入标题' }]}
|
|
||||||
/>
|
|
||||||
</ModalForm>
|
|
||||||
</PageContainer>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default MediaPage;
|
|
||||||
|
|
@ -1,587 +0,0 @@
|
||||||
import { ORDER_STATUS_ENUM } from '@/constants';
|
|
||||||
import { HistoryOrder } from '@/pages/Statistics/Order';
|
|
||||||
import styles from '@/style/order-list.css';
|
|
||||||
import { DeleteFilled, EllipsisOutlined } from '@ant-design/icons';
|
|
||||||
import {
|
|
||||||
ActionType,
|
|
||||||
ModalForm,
|
|
||||||
PageContainer,
|
|
||||||
ProColumns,
|
|
||||||
ProFormTextArea,
|
|
||||||
ProTable,
|
|
||||||
} from '@ant-design/pro-components';
|
|
||||||
import { request, useParams } from '@umijs/max';
|
|
||||||
import { App, Button, Dropdown, Popconfirm, Tabs, TabsProps } from 'antd';
|
|
||||||
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
|
||||||
import {
|
|
||||||
BatchEditOrders,
|
|
||||||
CreateOrder,
|
|
||||||
EditOrder,
|
|
||||||
OrderNote,
|
|
||||||
ShipOrderForm,
|
|
||||||
} from '../components/Order/Forms';
|
|
||||||
|
|
||||||
const OrdersPage: React.FC = () => {
|
|
||||||
const actionRef = useRef<ActionType>();
|
|
||||||
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
|
|
||||||
const [activeKey, setActiveKey] = useState<string>('all');
|
|
||||||
const [count, setCount] = useState<any[]>([]);
|
|
||||||
const [activeLine, setActiveLine] = useState<number>(-1);
|
|
||||||
const { siteId } = useParams<{ siteId: string }>();
|
|
||||||
const { message } = App.useApp();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
actionRef.current?.reload();
|
|
||||||
}, [siteId]);
|
|
||||||
|
|
||||||
const tabs: TabsProps['items'] = useMemo(() => {
|
|
||||||
// 统计全部数量,依赖状态统计数组
|
|
||||||
const total = count.reduce((acc, cur) => acc + Number(cur.count), 0);
|
|
||||||
const tabs = [
|
|
||||||
{ key: 'pending', label: '待确认' },
|
|
||||||
{ key: 'processing', label: '待发货' },
|
|
||||||
{ key: 'completed', label: '已完成' },
|
|
||||||
{ key: 'cancelled', label: '已取消' },
|
|
||||||
{ key: 'refunded', label: '已退款' },
|
|
||||||
{ key: 'failed', label: '失败' },
|
|
||||||
{ key: 'after_sale_pending', label: '售后处理中' },
|
|
||||||
{ key: 'pending_reshipment', label: '待补发' },
|
|
||||||
// 退款相关状态
|
|
||||||
{ key: 'refund_requested', label: '已申请退款' },
|
|
||||||
{ key: 'refund_approved', label: '退款申请已通过' },
|
|
||||||
{ key: 'refund_cancelled', label: '已取消退款' },
|
|
||||||
].map((v) => {
|
|
||||||
// 根据状态键匹配统计数量
|
|
||||||
const number = count.find((el) => el.status === v.key)?.count || '0';
|
|
||||||
return {
|
|
||||||
label: `${v.label}(${number})`,
|
|
||||||
key: v.key,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
return [{ key: 'all', label: `全部(${total})` }, ...tabs];
|
|
||||||
}, [count]);
|
|
||||||
|
|
||||||
const columns: ProColumns<API.UnifiedOrderDTO>[] = [
|
|
||||||
{
|
|
||||||
title: '订单ID',
|
|
||||||
dataIndex: 'id',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '订单号',
|
|
||||||
dataIndex: 'number',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '状态',
|
|
||||||
dataIndex: 'status',
|
|
||||||
valueType: 'select',
|
|
||||||
valueEnum: ORDER_STATUS_ENUM,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '金额',
|
|
||||||
dataIndex: 'total',
|
|
||||||
hideInSearch: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '币种',
|
|
||||||
dataIndex: 'currency',
|
|
||||||
hideInSearch: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '财务状态',
|
|
||||||
dataIndex: 'financial_status',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '支付方式',
|
|
||||||
dataIndex: 'payment_method',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '支付时间',
|
|
||||||
dataIndex: 'date_paid',
|
|
||||||
hideInSearch: true,
|
|
||||||
valueType: 'dateTime',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '创建时间',
|
|
||||||
dataIndex: 'date_created',
|
|
||||||
hideInSearch: true,
|
|
||||||
valueType: 'dateTime',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '更新时间',
|
|
||||||
dataIndex: 'date_modified',
|
|
||||||
hideInSearch: true,
|
|
||||||
valueType: 'dateTime',
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
title: '客户ID',
|
|
||||||
dataIndex: 'customer_id',
|
|
||||||
hideInSearch: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '客户邮箱',
|
|
||||||
dataIndex: 'email',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '客户姓名',
|
|
||||||
dataIndex: 'customer_name',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '客户IP',
|
|
||||||
dataIndex: 'customer_ip_address',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '联系电话',
|
|
||||||
render: (_, record) => record.shipping?.phone || record.billing?.phone,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '设备类型',
|
|
||||||
dataIndex: 'device_type',
|
|
||||||
hideInSearch: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '来源类型',
|
|
||||||
dataIndex: 'source_type',
|
|
||||||
hideInSearch: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'UTM来源',
|
|
||||||
dataIndex: 'utm_source',
|
|
||||||
hideInSearch: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '商品',
|
|
||||||
dataIndex: 'line_items',
|
|
||||||
hideInSearch: true,
|
|
||||||
width: 200,
|
|
||||||
ellipsis: true,
|
|
||||||
render: (_, record) => {
|
|
||||||
// 检查 record.line_items 是否是数组并且有内容
|
|
||||||
if (Array.isArray(record.line_items) && record.line_items.length > 0) {
|
|
||||||
// 遍历 line_items 数组, 显示每个商品的名称和数量
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
{record.line_items.map((item: any) => (
|
|
||||||
<div
|
|
||||||
key={item.id}
|
|
||||||
>{`${item.name}(${item.sku}) x ${item.quantity}`}</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
// 如果 line_items 不存在或不是数组, 则显示占位符
|
|
||||||
return '-';
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '账单地址',
|
|
||||||
dataIndex: 'billing_full_address',
|
|
||||||
hideInSearch: true,
|
|
||||||
width: 200,
|
|
||||||
ellipsis: true,
|
|
||||||
copyable: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '收货地址',
|
|
||||||
dataIndex: 'shipping_full_address',
|
|
||||||
hideInSearch: true,
|
|
||||||
width: 200,
|
|
||||||
ellipsis: true,
|
|
||||||
copyable: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '发货状态',
|
|
||||||
dataIndex: 'fulfillment_status',
|
|
||||||
// hideInSearch: true,
|
|
||||||
// render: (_, record) => {
|
|
||||||
// const fulfillmentStatus = record.fulfillment_status;
|
|
||||||
// const fulfillmentStatusMap: Record<string, string> = {
|
|
||||||
// '0': '未发货',
|
|
||||||
// '1': '部分发货',
|
|
||||||
// '2': '已发货',
|
|
||||||
// '3': '已取消',
|
|
||||||
// '4': '确认发货',
|
|
||||||
// };
|
|
||||||
// if (fulfillmentStatus === undefined || fulfillmentStatus === null) {
|
|
||||||
// return '-';
|
|
||||||
// }
|
|
||||||
// return (
|
|
||||||
// fulfillmentStatusMap[String(fulfillmentStatus)] ||
|
|
||||||
// String(fulfillmentStatus)
|
|
||||||
// );
|
|
||||||
// },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '物流',
|
|
||||||
dataIndex: 'fulfillments',
|
|
||||||
hideInSearch: true,
|
|
||||||
render: (_, record) => {
|
|
||||||
// 检查是否有物流信息
|
|
||||||
if (
|
|
||||||
!record.fulfillments ||
|
|
||||||
!Array.isArray(record.fulfillments) ||
|
|
||||||
record.fulfillments.length === 0
|
|
||||||
) {
|
|
||||||
return '-';
|
|
||||||
}
|
|
||||||
// 遍历物流信息数组, 显示每个物流的提供商和单号
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
{record.fulfillments.map((item, index: number) => (
|
|
||||||
<div
|
|
||||||
key={index}
|
|
||||||
style={{ display: 'flex', flexDirection: 'column' }}
|
|
||||||
>
|
|
||||||
<span>
|
|
||||||
{item.shipping_provider
|
|
||||||
? `快递方式: ${item.shipping_provider}`
|
|
||||||
: ''}
|
|
||||||
</span>
|
|
||||||
{
|
|
||||||
item.shipping_method
|
|
||||||
? `发货方式: ${item.shipping_method}`
|
|
||||||
: ''
|
|
||||||
}
|
|
||||||
<span>
|
|
||||||
{item.tracking_number
|
|
||||||
? `物流单号: ${item.tracking_number}`
|
|
||||||
: ''}
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
{item.date_created ? `发货日期: ${item.date_created}` : ''}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '操作',
|
|
||||||
dataIndex: 'option',
|
|
||||||
valueType: 'option',
|
|
||||||
fixed: 'right',
|
|
||||||
width: '200',
|
|
||||||
render: (_, record) => {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<EditOrder
|
|
||||||
key={record.id}
|
|
||||||
record={record}
|
|
||||||
tableRef={actionRef}
|
|
||||||
orderId={record.id as number}
|
|
||||||
setActiveLine={setActiveLine}
|
|
||||||
siteId={siteId}
|
|
||||||
/>
|
|
||||||
<Dropdown
|
|
||||||
menu={{
|
|
||||||
items: [
|
|
||||||
// Sync button removed
|
|
||||||
{
|
|
||||||
key: 'history',
|
|
||||||
label: (
|
|
||||||
<HistoryOrder
|
|
||||||
email={(record as any).email}
|
|
||||||
tableRef={actionRef}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'note',
|
|
||||||
label: (
|
|
||||||
<OrderNote id={record.id as number} siteId={siteId} />
|
|
||||||
),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button type="text" icon={<EllipsisOutlined />} />
|
|
||||||
</Dropdown>
|
|
||||||
<ShipOrderForm
|
|
||||||
orderId={record.id as number}
|
|
||||||
tableRef={actionRef}
|
|
||||||
siteId={siteId}
|
|
||||||
orderItems={(record as any).line_items?.map((item: any) => ({
|
|
||||||
id: item.id,
|
|
||||||
name: item.name,
|
|
||||||
quantity: item.quantity,
|
|
||||||
sku: item.sku,
|
|
||||||
}))}
|
|
||||||
/>
|
|
||||||
{record.status === 'completed' && (
|
|
||||||
<Popconfirm
|
|
||||||
title="确定取消发货?"
|
|
||||||
description="取消发货后订单状态将恢复为处理中"
|
|
||||||
onConfirm={async () => {
|
|
||||||
try {
|
|
||||||
const res = await request(
|
|
||||||
`/site-api/${siteId}/orders/${record.id}/cancel-ship`,
|
|
||||||
{ method: 'POST' },
|
|
||||||
);
|
|
||||||
if (res.success) {
|
|
||||||
message.success('取消发货成功');
|
|
||||||
actionRef.current?.reload();
|
|
||||||
} else {
|
|
||||||
message.error(res.message || '取消发货失败');
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
message.error('取消发货失败');
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button type="link" danger title="取消发货">
|
|
||||||
取消发货
|
|
||||||
</Button>
|
|
||||||
</Popconfirm>
|
|
||||||
)}
|
|
||||||
<Popconfirm
|
|
||||||
title="确定删除订单?"
|
|
||||||
onConfirm={async () => {
|
|
||||||
try {
|
|
||||||
const res = await request(
|
|
||||||
`/site-api/${siteId}/orders/${record.id}`,
|
|
||||||
{ method: 'DELETE' },
|
|
||||||
);
|
|
||||||
if (res.success) {
|
|
||||||
message.success('删除成功');
|
|
||||||
actionRef.current?.reload();
|
|
||||||
} else {
|
|
||||||
message.error(res.message || '删除失败');
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
message.error('删除失败');
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button type="link" danger title="删除" icon={<DeleteFilled />} />
|
|
||||||
</Popconfirm>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
return (
|
|
||||||
<PageContainer ghost header={{ title: null, breadcrumb: undefined }}>
|
|
||||||
<Tabs items={tabs} activeKey={activeKey} onChange={setActiveKey} />
|
|
||||||
<ProTable
|
|
||||||
columns={columns}
|
|
||||||
params={{ status: activeKey }}
|
|
||||||
headerTitle="查询表格"
|
|
||||||
scroll={{ x: 'max-content' }}
|
|
||||||
actionRef={actionRef}
|
|
||||||
rowKey="id"
|
|
||||||
rowSelection={{ selectedRowKeys, onChange: setSelectedRowKeys }}
|
|
||||||
rowClassName={(record) => {
|
|
||||||
return record.id === activeLine
|
|
||||||
? styles['selected-line-order-protable']
|
|
||||||
: '';
|
|
||||||
}}
|
|
||||||
pagination={{
|
|
||||||
pageSizeOptions: ['10', '20', '50', '100', '1000'],
|
|
||||||
showSizeChanger: true,
|
|
||||||
showQuickJumper: true,
|
|
||||||
defaultPageSize: 10,
|
|
||||||
}}
|
|
||||||
toolBarRender={() => [
|
|
||||||
<CreateOrder tableRef={actionRef} siteId={siteId} />,
|
|
||||||
<BatchEditOrders
|
|
||||||
tableRef={actionRef}
|
|
||||||
selectedRowKeys={selectedRowKeys}
|
|
||||||
setSelectedRowKeys={setSelectedRowKeys}
|
|
||||||
siteId={siteId}
|
|
||||||
/>,
|
|
||||||
<Button disabled>批量发货</Button>,
|
|
||||||
<Button
|
|
||||||
title="批量删除"
|
|
||||||
danger
|
|
||||||
icon={<DeleteFilled />}
|
|
||||||
disabled={!selectedRowKeys.length}
|
|
||||||
onClick={async () => {
|
|
||||||
if (!siteId) return;
|
|
||||||
const res = await request(`/site-api/${siteId}/orders/batch`, {
|
|
||||||
method: 'POST',
|
|
||||||
data: { delete: selectedRowKeys },
|
|
||||||
});
|
|
||||||
setSelectedRowKeys([]);
|
|
||||||
actionRef.current?.reload();
|
|
||||||
if (res.success) {
|
|
||||||
message.success('批量删除成功');
|
|
||||||
} else {
|
|
||||||
message.warning(res.message || '部分删除失败');
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>,
|
|
||||||
<Button
|
|
||||||
onClick={async () => {
|
|
||||||
if (!siteId) return;
|
|
||||||
const idsParam = selectedRowKeys.length
|
|
||||||
? (selectedRowKeys as any[]).join(',')
|
|
||||||
: undefined;
|
|
||||||
const res = await request(`/site-api/${siteId}/orders/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);
|
|
||||||
const a = document.createElement('a');
|
|
||||||
a.href = url;
|
|
||||||
a.download = 'orders.csv';
|
|
||||||
a.click();
|
|
||||||
URL.revokeObjectURL(url);
|
|
||||||
} else {
|
|
||||||
message.error(res.message || '导出失败');
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
批量导出
|
|
||||||
</Button>,
|
|
||||||
<ModalForm
|
|
||||||
title="批量导入订单"
|
|
||||||
trigger={
|
|
||||||
<Button type="primary" ghost>
|
|
||||||
批量导入
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
width={600}
|
|
||||||
modalProps={{ destroyOnHidden: true }}
|
|
||||||
onFinish={async (values) => {
|
|
||||||
if (!siteId) return false;
|
|
||||||
const csv = values.csv || '';
|
|
||||||
const items = values.items || [];
|
|
||||||
const res = await request(`/site-api/${siteId}/orders/import`, {
|
|
||||||
method: 'POST',
|
|
||||||
data: { csv, items },
|
|
||||||
});
|
|
||||||
if (res.success) {
|
|
||||||
message.success('导入完成');
|
|
||||||
actionRef.current?.reload();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
message.error(res.message || '导入失败');
|
|
||||||
return false;
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ProFormTextArea
|
|
||||||
name="csv"
|
|
||||||
label="CSV文本"
|
|
||||||
placeholder="粘贴CSV,首行为表头"
|
|
||||||
/>
|
|
||||||
</ModalForm>,
|
|
||||||
]}
|
|
||||||
request={async (params, sort, filter) => {
|
|
||||||
const { current, pageSize, date, status, ...rest } = params;
|
|
||||||
const where: Record<string, any> = { ...(filter || {}), ...rest };
|
|
||||||
if (status && status !== 'all') {
|
|
||||||
where.status = status;
|
|
||||||
}
|
|
||||||
if (date) {
|
|
||||||
const [startDate, endDate] = date;
|
|
||||||
// 将日期范围转为后端筛选参数
|
|
||||||
where.startDate = `${startDate} 00:00:00`;
|
|
||||||
where.endDate = `${endDate} 23:59:59`;
|
|
||||||
}
|
|
||||||
let orderObj: Record<string, 'asc' | 'desc'> | 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}/orders`, {
|
|
||||||
params: {
|
|
||||||
page: current,
|
|
||||||
per_page: pageSize,
|
|
||||||
where,
|
|
||||||
...(orderObj ? { orderBy: orderObj } : {}),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.success) {
|
|
||||||
message.error(response.message || '获取订单列表失败');
|
|
||||||
return {
|
|
||||||
data: [],
|
|
||||||
success: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const { data } = response;
|
|
||||||
// 计算顶部状态数量,通过按状态并发查询站点接口
|
|
||||||
if (siteId) {
|
|
||||||
try {
|
|
||||||
// 定义需要统计的状态键集合
|
|
||||||
const statusKeys: string[] = [
|
|
||||||
'pending',
|
|
||||||
'processing',
|
|
||||||
'completed',
|
|
||||||
'cancelled',
|
|
||||||
'refunded',
|
|
||||||
'failed',
|
|
||||||
// 站点接口不支持的扩展状态,默认统计为0
|
|
||||||
'after_sale_pending',
|
|
||||||
'pending_reshipment',
|
|
||||||
'refund_requested',
|
|
||||||
'refund_approved',
|
|
||||||
'refund_cancelled',
|
|
||||||
];
|
|
||||||
// 构造基础筛选参数,移除当前状态避免重复过滤
|
|
||||||
const { status: _status, ...baseWhere } = where;
|
|
||||||
// 并发请求各状态的总数,对站点接口不支持的状态使用0
|
|
||||||
const results = await Promise.all(
|
|
||||||
statusKeys.map(async (key) => {
|
|
||||||
// 将前端退款状态映射为站点接口可能识别的原始状态
|
|
||||||
const mapToRawStatus: Record<string, string> = {
|
|
||||||
refund_requested: 'return-requested',
|
|
||||||
refund_approved: 'return-approved',
|
|
||||||
refund_cancelled: 'return-cancelled',
|
|
||||||
};
|
|
||||||
const rawStatus = mapToRawStatus[key] || key;
|
|
||||||
// 对扩展状态直接返回0,减少不必要的请求
|
|
||||||
const unsupported = [
|
|
||||||
'after_sale_pending',
|
|
||||||
'pending_reshipment',
|
|
||||||
];
|
|
||||||
if (unsupported.includes(key)) {
|
|
||||||
return { status: key, count: 0 };
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const res = await request(
|
|
||||||
`/site-api/${siteId}/orders/count`,
|
|
||||||
{
|
|
||||||
params: { ...baseWhere, status: rawStatus },
|
|
||||||
},
|
|
||||||
);
|
|
||||||
const totalCount = Number(res?.data?.total || 0);
|
|
||||||
return { status: key, count: totalCount };
|
|
||||||
} catch (err) {
|
|
||||||
// 请求失败时该状态数量记为0
|
|
||||||
return { status: key, count: 0 };
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
setCount(results);
|
|
||||||
} catch (e) {
|
|
||||||
// 统计失败时不影响列表展示
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data) {
|
|
||||||
return {
|
|
||||||
total: data?.total || 0,
|
|
||||||
data: data?.items || [],
|
|
||||||
success: true,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
data: [],
|
|
||||||
success: false,
|
|
||||||
};
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</PageContainer>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default OrdersPage;
|
|
||||||
|
|
@ -1,594 +0,0 @@
|
||||||
import { PRODUCT_STATUS_ENUM, PRODUCT_STOCK_STATUS_ENUM } from '@/constants';
|
|
||||||
import { DeleteFilled, LinkOutlined } from '@ant-design/icons';
|
|
||||||
import {
|
|
||||||
ActionType,
|
|
||||||
PageContainer,
|
|
||||||
ProColumns,
|
|
||||||
ProTable,
|
|
||||||
} from '@ant-design/pro-components';
|
|
||||||
import { request, useParams } from '@umijs/max';
|
|
||||||
import { App, Button, Divider, Popconfirm, Tag } from 'antd';
|
|
||||||
import React, { useEffect, useRef, useState } from 'react';
|
|
||||||
import { ErpProductBindModal } from '../components/Product/ErpProductBindModal';
|
|
||||||
import {
|
|
||||||
BatchDeleteProducts,
|
|
||||||
BatchEditProducts,
|
|
||||||
CreateProduct,
|
|
||||||
ImportCsv,
|
|
||||||
SetComponent,
|
|
||||||
UpdateForm,
|
|
||||||
UpdateStatus,
|
|
||||||
UpdateVaritation,
|
|
||||||
} from '../components/Product/Forms';
|
|
||||||
import { TagConfig } from '../components/Product/utils';
|
|
||||||
const ProductsPage: React.FC = () => {
|
|
||||||
const { message } = App.useApp();
|
|
||||||
const actionRef = useRef<ActionType>();
|
|
||||||
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
|
|
||||||
const [selectedRows, setSelectedRows] = useState<any[]>([]); // Use any or unified DTO type
|
|
||||||
const { siteId } = useParams<{ siteId: string }>();
|
|
||||||
const [siteInfo, setSiteInfo] = useState<any>();
|
|
||||||
const [config, setConfig] = useState<TagConfig>({
|
|
||||||
brands: [],
|
|
||||||
fruits: [],
|
|
||||||
mints: [],
|
|
||||||
flavors: [],
|
|
||||||
strengths: [],
|
|
||||||
sizes: [],
|
|
||||||
humidities: [],
|
|
||||||
categories: [],
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
actionRef.current?.reload();
|
|
||||||
}, [siteId]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const loadSiteInfo = async () => {
|
|
||||||
try {
|
|
||||||
const res = await request(`/site/get/${siteId}`);
|
|
||||||
if (res?.success && res?.data) {
|
|
||||||
setSiteInfo(res.data);
|
|
||||||
}
|
|
||||||
} catch (e) {}
|
|
||||||
};
|
|
||||||
if (siteId) {
|
|
||||||
loadSiteInfo();
|
|
||||||
}
|
|
||||||
}, [siteId]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const fetchAllConfigs = async () => {
|
|
||||||
try {
|
|
||||||
const dictList = await request('/dict/list');
|
|
||||||
|
|
||||||
const getItems = async (dictName: string) => {
|
|
||||||
const dict = dictList.find((d: any) => d.name === dictName);
|
|
||||||
if (!dict) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
const res = await request('/dict/items', {
|
|
||||||
params: { dictId: dict.id },
|
|
||||||
});
|
|
||||||
return res.map((item: any) => item.name);
|
|
||||||
};
|
|
||||||
|
|
||||||
const [
|
|
||||||
brands,
|
|
||||||
fruits,
|
|
||||||
mints,
|
|
||||||
flavors,
|
|
||||||
strengths,
|
|
||||||
sizes,
|
|
||||||
humidities,
|
|
||||||
categories,
|
|
||||||
] = await Promise.all([
|
|
||||||
getItems('brand'),
|
|
||||||
getItems('fruit'),
|
|
||||||
getItems('mint'),
|
|
||||||
getItems('flavor'),
|
|
||||||
getItems('strength'),
|
|
||||||
getItems('size'),
|
|
||||||
getItems('humidity'),
|
|
||||||
getItems('category'),
|
|
||||||
]);
|
|
||||||
|
|
||||||
setConfig({
|
|
||||||
brands,
|
|
||||||
fruits,
|
|
||||||
mints,
|
|
||||||
flavors,
|
|
||||||
strengths,
|
|
||||||
sizes,
|
|
||||||
humidities,
|
|
||||||
categories,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to fetch configs:', error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
fetchAllConfigs();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const columns: ProColumns<any>[] = [
|
|
||||||
{
|
|
||||||
// ID
|
|
||||||
title: 'ID',
|
|
||||||
dataIndex: 'id',
|
|
||||||
width: 120,
|
|
||||||
copyable: true,
|
|
||||||
render: (_, record) => {
|
|
||||||
return record?.id ?? '-';
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// sku
|
|
||||||
title: 'sku',
|
|
||||||
dataIndex: 'sku',
|
|
||||||
fixed: 'left',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// 名称
|
|
||||||
title: '名称',
|
|
||||||
dataIndex: 'name',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// 产品类型
|
|
||||||
title: '产品类型',
|
|
||||||
dataIndex: 'type',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// 产品状态
|
|
||||||
title: '产品状态',
|
|
||||||
dataIndex: 'status',
|
|
||||||
valueType: 'select',
|
|
||||||
valueEnum: PRODUCT_STATUS_ENUM,
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
// 库存状态
|
|
||||||
title: '库存状态',
|
|
||||||
dataIndex: 'stock_status',
|
|
||||||
valueType: 'select',
|
|
||||||
valueEnum: PRODUCT_STOCK_STATUS_ENUM,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// 库存
|
|
||||||
title: '库存数量',
|
|
||||||
dataIndex: 'stock_quantity',
|
|
||||||
hideInSearch: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// ERP产品信息
|
|
||||||
title: 'ERP产品',
|
|
||||||
dataIndex: 'erpProduct',
|
|
||||||
hideInSearch: true,
|
|
||||||
width: 200,
|
|
||||||
render: (_, record) => {
|
|
||||||
if (record.erpProduct) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
style={{ display: 'flex', flexDirection: 'column', gap: '4px' }}
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<strong>SKU:</strong> {record.erpProduct.sku}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<strong>名称:</strong> {record.erpProduct.name}
|
|
||||||
</div>
|
|
||||||
{record.erpProduct.nameCn && (
|
|
||||||
<div>
|
|
||||||
<strong>中文名:</strong> {record.erpProduct.nameCn}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{record.erpProduct.category && (
|
|
||||||
<div>
|
|
||||||
<strong>分类:</strong> {record.erpProduct.category.name}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div>
|
|
||||||
<strong>库存:</strong> {record.erpProduct.stock_quantity ?? '-'}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return <Tag color="orange">未绑定</Tag>;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// 图片
|
|
||||||
title: '图片',
|
|
||||||
dataIndex: 'images',
|
|
||||||
hideInSearch: true,
|
|
||||||
render: (_, record) => {
|
|
||||||
if (record.images && record.images.length > 0) {
|
|
||||||
return <img src={record.images[0].src} width="50" />;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// 常规价格
|
|
||||||
title: '常规价格',
|
|
||||||
dataIndex: 'regular_price',
|
|
||||||
hideInSearch: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// 销售价格
|
|
||||||
title: '销售价格',
|
|
||||||
dataIndex: 'sale_price',
|
|
||||||
hideInSearch: true,
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
// 分类
|
|
||||||
title: '分类',
|
|
||||||
dataIndex: 'categories',
|
|
||||||
hideInSearch: true,
|
|
||||||
width: 250,
|
|
||||||
render: (_, record) => {
|
|
||||||
// 检查 record.categories 是否存在并且是一个数组
|
|
||||||
if (record.categories && Array.isArray(record.categories)) {
|
|
||||||
// 遍历 categories 数组并为每个 category 对象渲染一个 Tag 组件
|
|
||||||
return (
|
|
||||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '4px' }}>
|
|
||||||
{record.categories.map((cat: any) => (
|
|
||||||
// 使用 cat.name 作为 key
|
|
||||||
<Tag key={cat.name}>{cat.name}</Tag>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
// 如果 record.categories 不是一个有效的数组,则不渲染任何内容
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// 属性
|
|
||||||
title: '属性',
|
|
||||||
dataIndex: 'attributes',
|
|
||||||
hideInSearch: true,
|
|
||||||
width: 250,
|
|
||||||
render: (_, record) => {
|
|
||||||
// 检查 record.attributes 是否存在并且是一个数组
|
|
||||||
if (record.attributes && Array.isArray(record.attributes)) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
style={{ display: 'flex', flexDirection: 'column', gap: '4px' }}
|
|
||||||
>
|
|
||||||
{(record.attributes as any[]).map((attr: any) => (
|
|
||||||
<div key={attr.name}>
|
|
||||||
<strong>{attr.name}:</strong>{' '}
|
|
||||||
{Array.isArray(attr.options) ? attr.options.join(', ') : ''}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// 标签
|
|
||||||
title: '标签',
|
|
||||||
dataIndex: 'tags',
|
|
||||||
hideInSearch: true,
|
|
||||||
width: 250,
|
|
||||||
render: (_, record) => {
|
|
||||||
// 检查 record.tags 是否存在并且是一个数组
|
|
||||||
if (record.tags && Array.isArray(record.tags)) {
|
|
||||||
// 遍历 tags 数组并为每个 tag 对象渲染一个 Tag 组件
|
|
||||||
return (
|
|
||||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '4px' }}>
|
|
||||||
{record.tags.map((tag: any) => (
|
|
||||||
// 使用 tag.name 作为 key, 因为 tag.id 可能是对象, 会导致 React key 错误
|
|
||||||
<Tag key={tag.name}>{tag.name}</Tag>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
// 如果 record.tags 不是一个有效的数组,则不渲染任何内容
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// 创建时间
|
|
||||||
title: '创建时间',
|
|
||||||
dataIndex: 'date_created',
|
|
||||||
valueType: 'dateTime',
|
|
||||||
hideInSearch: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// 修改时间
|
|
||||||
title: '修改时间',
|
|
||||||
dataIndex: 'date_modified',
|
|
||||||
valueType: 'dateTime',
|
|
||||||
hideInSearch: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// 操作
|
|
||||||
title: '操作',
|
|
||||||
dataIndex: 'option',
|
|
||||||
valueType: 'option',
|
|
||||||
fixed: 'right',
|
|
||||||
width: '200',
|
|
||||||
render: (_, record) => (
|
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '4px' }}>
|
|
||||||
<UpdateForm
|
|
||||||
tableRef={actionRef}
|
|
||||||
values={record}
|
|
||||||
config={config}
|
|
||||||
siteId={siteId}
|
|
||||||
/>
|
|
||||||
<UpdateStatus tableRef={actionRef} values={record} siteId={siteId} />
|
|
||||||
{siteId && (
|
|
||||||
<ErpProductBindModal
|
|
||||||
trigger={
|
|
||||||
<Button
|
|
||||||
type="link"
|
|
||||||
title={record.erpProduct ? '换绑ERP产品' : '绑定ERP产品'}
|
|
||||||
>
|
|
||||||
{record.erpProduct ? '换绑' : '绑定'}
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
siteProduct={record}
|
|
||||||
siteId={siteId}
|
|
||||||
onBindSuccess={() => {
|
|
||||||
actionRef.current?.reload();
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<Button
|
|
||||||
type="link"
|
|
||||||
title="店铺链接"
|
|
||||||
icon={<LinkOutlined />}
|
|
||||||
disabled={!record.permalink}
|
|
||||||
onClick={() => {
|
|
||||||
if (record.permalink) {
|
|
||||||
window.open(record.permalink, '_blank', 'noopener,noreferrer');
|
|
||||||
} else {
|
|
||||||
message.warning('未能生成店铺链接');
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Popconfirm
|
|
||||||
key="delete"
|
|
||||||
title="删除"
|
|
||||||
description="确认删除?"
|
|
||||||
onConfirm={async () => {
|
|
||||||
try {
|
|
||||||
await request(`/site-api/${siteId}/products/${record.id}`, {
|
|
||||||
method: 'DELETE',
|
|
||||||
});
|
|
||||||
message.success('删除成功');
|
|
||||||
actionRef.current?.reload();
|
|
||||||
} catch (e: any) {
|
|
||||||
message.error(e.message || '删除失败');
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button type="link" danger title="删除" icon={<DeleteFilled />} />
|
|
||||||
</Popconfirm>
|
|
||||||
{record.type === 'simple' && record.sku ? (
|
|
||||||
<SetComponent
|
|
||||||
tableRef={actionRef}
|
|
||||||
values={record}
|
|
||||||
isProduct={true}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<></>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const varColumns: ProColumns<any>[] = [];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<PageContainer header={{ title: null, breadcrumb: undefined }}>
|
|
||||||
<ProTable<API.UnifiedProductDTO>
|
|
||||||
scroll={{ x: 'max-content' }}
|
|
||||||
pagination={{
|
|
||||||
pageSizeOptions: ['10', '20', '50', '100', '1000', '2000'],
|
|
||||||
showSizeChanger: true,
|
|
||||||
showQuickJumper: true,
|
|
||||||
defaultPageSize: 10,
|
|
||||||
}}
|
|
||||||
actionRef={actionRef}
|
|
||||||
rowKey="id"
|
|
||||||
rowSelection={{
|
|
||||||
selectedRowKeys,
|
|
||||||
onChange: (keys, rows) => {
|
|
||||||
setSelectedRowKeys(keys);
|
|
||||||
setSelectedRows(rows);
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
request={async (params, sort, filter) => {
|
|
||||||
// 从参数中解构分页和筛选条件, ProTable 使用 current 作为页码, 但后端需要 page, 所以在这里进行重命名
|
|
||||||
const { current: page, pageSize, ...rest } = params || {};
|
|
||||||
const where = { ...rest, ...(filter || {}) };
|
|
||||||
let orderObj: Record<string, 'asc' | 'desc'> | 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}/products`, {
|
|
||||||
params: {
|
|
||||||
page,
|
|
||||||
per_page: pageSize,
|
|
||||||
where,
|
|
||||||
...(orderObj
|
|
||||||
? {
|
|
||||||
sortField: Object.keys(orderObj)[0],
|
|
||||||
sortOrder: Object.values(orderObj)[0],
|
|
||||||
}
|
|
||||||
: {}),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.success) {
|
|
||||||
message.error(response.message || '获取列表失败');
|
|
||||||
return {
|
|
||||||
data: [],
|
|
||||||
total: 0,
|
|
||||||
success: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// 从API响应中正确获取数据,API响应结构为 { success, message, data, code }
|
|
||||||
const data = response.data;
|
|
||||||
return {
|
|
||||||
total: data?.total || 0,
|
|
||||||
data: data?.items || [],
|
|
||||||
success: true,
|
|
||||||
};
|
|
||||||
}}
|
|
||||||
columns={columns}
|
|
||||||
toolBarRender={() => [
|
|
||||||
<CreateProduct tableRef={actionRef} siteId={siteId} />,
|
|
||||||
// SyncForm removed
|
|
||||||
<BatchEditProducts
|
|
||||||
tableRef={actionRef}
|
|
||||||
selectedRowKeys={selectedRowKeys}
|
|
||||||
setSelectedRowKeys={setSelectedRowKeys}
|
|
||||||
selectedRows={selectedRows}
|
|
||||||
siteId={siteId}
|
|
||||||
/>,
|
|
||||||
<BatchDeleteProducts
|
|
||||||
tableRef={actionRef}
|
|
||||||
selectedRowKeys={selectedRowKeys}
|
|
||||||
setSelectedRowKeys={setSelectedRowKeys}
|
|
||||||
siteId={siteId}
|
|
||||||
/>,
|
|
||||||
<ImportCsv tableRef={actionRef} siteId={siteId} />,
|
|
||||||
<Button
|
|
||||||
onClick={async () => {
|
|
||||||
const idsParam = selectedRowKeys.length
|
|
||||||
? (selectedRowKeys as any[]).join(',')
|
|
||||||
: undefined;
|
|
||||||
const res = await request(`/site-api/${siteId}/products/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);
|
|
||||||
const a = document.createElement('a');
|
|
||||||
a.href = url;
|
|
||||||
a.download = 'products.csv';
|
|
||||||
a.click();
|
|
||||||
URL.revokeObjectURL(url);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
批量导出
|
|
||||||
</Button>,
|
|
||||||
]}
|
|
||||||
expandable={{
|
|
||||||
rowExpandable: (record) => record.type === 'variable',
|
|
||||||
expandedRowRender: (record) => {
|
|
||||||
const productExternalId =
|
|
||||||
(record as any).externalProductId ||
|
|
||||||
(record as any).external_product_id ||
|
|
||||||
record.id;
|
|
||||||
const innerColumns: ProColumns<any>[] = [
|
|
||||||
{
|
|
||||||
title: 'ID',
|
|
||||||
dataIndex: 'id',
|
|
||||||
hideInSearch: true,
|
|
||||||
width: 120,
|
|
||||||
render: (_, row) => {
|
|
||||||
return row?.id ?? '-';
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{ title: '变体名', dataIndex: 'name' },
|
|
||||||
{ title: 'sku', dataIndex: 'sku' },
|
|
||||||
{
|
|
||||||
title: '常规价格',
|
|
||||||
dataIndex: 'regular_price',
|
|
||||||
hideInSearch: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '销售价格',
|
|
||||||
dataIndex: 'sale_price',
|
|
||||||
hideInSearch: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Attributes',
|
|
||||||
dataIndex: 'attributes',
|
|
||||||
hideInSearch: true,
|
|
||||||
render: (_, row) => {
|
|
||||||
// 检查 row.attributes 是否存在并且是一个数组
|
|
||||||
if (row.attributes && Array.isArray(row.attributes)) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
gap: '4px',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{(row.attributes as any[]).map((attr: any) => (
|
|
||||||
<div key={attr.name}>
|
|
||||||
<strong>{attr.name}:</strong> {attr.option}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '操作',
|
|
||||||
dataIndex: 'option',
|
|
||||||
valueType: 'option',
|
|
||||||
render: (_, row) => (
|
|
||||||
<>
|
|
||||||
<UpdateVaritation
|
|
||||||
tableRef={actionRef}
|
|
||||||
values={row}
|
|
||||||
siteId={siteId}
|
|
||||||
productId={productExternalId}
|
|
||||||
/>
|
|
||||||
{row.sku ? (
|
|
||||||
<>
|
|
||||||
<Divider type="vertical" />
|
|
||||||
<SetComponent
|
|
||||||
tableRef={actionRef}
|
|
||||||
values={row}
|
|
||||||
isProduct={false}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<></>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
return (
|
|
||||||
<ProTable<any>
|
|
||||||
rowKey="id"
|
|
||||||
dataSource={record.variations}
|
|
||||||
pagination={false}
|
|
||||||
search={false}
|
|
||||||
options={false}
|
|
||||||
columns={innerColumns}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</PageContainer>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ProductsPage;
|
|
||||||
|
|
@ -1,184 +0,0 @@
|
||||||
import {
|
|
||||||
siteapicontrollerCreatereview,
|
|
||||||
siteapicontrollerUpdatereview,
|
|
||||||
} from '@/servers/api/siteApi';
|
|
||||||
import { Form, Input, InputNumber, Modal, Select, message } from 'antd';
|
|
||||||
import React, { useEffect } from 'react';
|
|
||||||
|
|
||||||
const { TextArea } = Input;
|
|
||||||
const { Option } = Select;
|
|
||||||
|
|
||||||
interface ReviewFormProps {
|
|
||||||
open: boolean;
|
|
||||||
editing: any;
|
|
||||||
siteId: number;
|
|
||||||
onClose: () => void;
|
|
||||||
onSuccess: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ReviewForm: React.FC<ReviewFormProps> = ({
|
|
||||||
open,
|
|
||||||
editing,
|
|
||||||
siteId,
|
|
||||||
onClose,
|
|
||||||
onSuccess,
|
|
||||||
}) => {
|
|
||||||
const [form] = Form.useForm();
|
|
||||||
|
|
||||||
// 当编辑状态改变时,重置表单数据
|
|
||||||
useEffect(() => {
|
|
||||||
if (editing) {
|
|
||||||
form.setFieldsValue({
|
|
||||||
product_id: editing.product_id,
|
|
||||||
author: editing.author,
|
|
||||||
email: editing.email,
|
|
||||||
content: editing.content,
|
|
||||||
rating: editing.rating,
|
|
||||||
status: editing.status,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
form.resetFields();
|
|
||||||
}
|
|
||||||
}, [editing, form]);
|
|
||||||
|
|
||||||
// 处理表单提交
|
|
||||||
const handleSubmit = async (values: any) => {
|
|
||||||
try {
|
|
||||||
let response;
|
|
||||||
|
|
||||||
if (editing) {
|
|
||||||
// 更新评论
|
|
||||||
response = await siteapicontrollerUpdatereview(
|
|
||||||
{
|
|
||||||
siteId,
|
|
||||||
id: editing.id,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
review: values.content,
|
|
||||||
rating: values.rating,
|
|
||||||
status: values.status,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
// 创建新评论
|
|
||||||
response = await siteapicontrollerCreatereview(
|
|
||||||
{
|
|
||||||
siteId,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
product_id: values.product_id,
|
|
||||||
review: values.content,
|
|
||||||
rating: values.rating,
|
|
||||||
author: values.author,
|
|
||||||
author_email: values.email,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (response.success) {
|
|
||||||
message.success(editing ? '更新成功' : '创建成功');
|
|
||||||
onSuccess();
|
|
||||||
onClose();
|
|
||||||
form.resetFields();
|
|
||||||
} else {
|
|
||||||
message.error(response.message || '操作失败');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('提交评论表单失败:', error);
|
|
||||||
message.error('提交失败,请重试');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Modal
|
|
||||||
title={editing ? '编辑评论' : '新建评论'}
|
|
||||||
open={open}
|
|
||||||
onCancel={onClose}
|
|
||||||
onOk={() => form.submit()}
|
|
||||||
okText="保存"
|
|
||||||
cancelText="取消"
|
|
||||||
width={600}
|
|
||||||
>
|
|
||||||
<Form
|
|
||||||
form={form}
|
|
||||||
layout="vertical"
|
|
||||||
onFinish={handleSubmit}
|
|
||||||
initialValues={{
|
|
||||||
status: 'approved',
|
|
||||||
rating: 5,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{!editing && (
|
|
||||||
<>
|
|
||||||
<Form.Item
|
|
||||||
name="product_id"
|
|
||||||
label="产品ID"
|
|
||||||
rules={[{ required: true, message: '请输入产品ID' }]}
|
|
||||||
>
|
|
||||||
<Input placeholder="请输入产品ID" />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
name="author"
|
|
||||||
label="评论者"
|
|
||||||
rules={[{ required: true, message: '请输入评论者姓名' }]}
|
|
||||||
>
|
|
||||||
<Input placeholder="请输入评论者姓名" />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
name="email"
|
|
||||||
label="邮箱"
|
|
||||||
rules={[
|
|
||||||
{ required: true, message: '请输入邮箱' },
|
|
||||||
{ type: 'email', message: '请输入有效的邮箱地址' },
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<Input placeholder="请输入邮箱" />
|
|
||||||
</Form.Item>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Form.Item
|
|
||||||
name="content"
|
|
||||||
label="评论内容"
|
|
||||||
rules={[{ required: true, message: '请输入评论内容' }]}
|
|
||||||
>
|
|
||||||
<TextArea
|
|
||||||
rows={4}
|
|
||||||
placeholder="请输入评论内容"
|
|
||||||
maxLength={1000}
|
|
||||||
showCount
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
<Form.Item
|
|
||||||
name="rating"
|
|
||||||
label="评分"
|
|
||||||
rules={[{ required: true, message: '请选择评分' }]}
|
|
||||||
>
|
|
||||||
<InputNumber
|
|
||||||
min={1}
|
|
||||||
max={5}
|
|
||||||
precision={0}
|
|
||||||
placeholder="评分 (1-5)"
|
|
||||||
style={{ width: '100%' }}
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
<Form.Item
|
|
||||||
name="status"
|
|
||||||
label="状态"
|
|
||||||
rules={[{ required: true, message: '请选择状态' }]}
|
|
||||||
>
|
|
||||||
<Select placeholder="请选择状态">
|
|
||||||
<Option value="approved">已批准</Option>
|
|
||||||
<Option value="pending">待审核</Option>
|
|
||||||
<Option value="spam">垃圾评论</Option>
|
|
||||||
<Option value="trash">回收站</Option>
|
|
||||||
</Select>
|
|
||||||
</Form.Item>
|
|
||||||
</Form>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ReviewForm;
|
|
||||||
|
|
@ -1,166 +0,0 @@
|
||||||
import {
|
|
||||||
siteapicontrollerDeletereview,
|
|
||||||
siteapicontrollerGetreviews,
|
|
||||||
} from '@/servers/api/siteApi';
|
|
||||||
import {
|
|
||||||
ActionType,
|
|
||||||
ProCard,
|
|
||||||
ProColumns,
|
|
||||||
ProTable,
|
|
||||||
} from '@ant-design/pro-components';
|
|
||||||
import { useParams } from '@umijs/max';
|
|
||||||
import { Button, message, Popconfirm, Space } from 'antd';
|
|
||||||
import React, { useRef, useState } from 'react';
|
|
||||||
import ReviewForm from './ReviewForm';
|
|
||||||
|
|
||||||
const ReviewsPage: React.FC = () => {
|
|
||||||
const params = useParams();
|
|
||||||
const siteId = Number(params.siteId);
|
|
||||||
const actionRef = useRef<ActionType>();
|
|
||||||
const [open, setOpen] = useState(false);
|
|
||||||
const [editing, setEditing] = useState<any>(null);
|
|
||||||
|
|
||||||
const columns: ProColumns<API.UnifiedReviewDTO>[] = [
|
|
||||||
{ title: 'ID', dataIndex: 'id', key: 'id', width: 50 },
|
|
||||||
{ title: '产品ID', dataIndex: 'product_id', key: 'product_id', width: 80 },
|
|
||||||
{ title: '作者', dataIndex: 'author', key: 'author' },
|
|
||||||
{ title: '评分', dataIndex: 'rating', key: 'rating', width: 80 },
|
|
||||||
{ title: '状态', dataIndex: 'status', key: 'status', width: 100 },
|
|
||||||
{
|
|
||||||
title: '创建时间',
|
|
||||||
dataIndex: 'date_created',
|
|
||||||
key: 'date_created',
|
|
||||||
valueType: 'dateTime',
|
|
||||||
width: 150,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '操作',
|
|
||||||
key: 'action',
|
|
||||||
width: 150,
|
|
||||||
render: (_, record) => (
|
|
||||||
<Space>
|
|
||||||
<Button
|
|
||||||
type="link"
|
|
||||||
style={{ padding: 0 }}
|
|
||||||
onClick={() => {
|
|
||||||
setEditing(record);
|
|
||||||
setOpen(true);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
编辑
|
|
||||||
</Button>
|
|
||||||
<Popconfirm
|
|
||||||
title="确定删除吗?"
|
|
||||||
onConfirm={async () => {
|
|
||||||
if (record.id) {
|
|
||||||
try {
|
|
||||||
const response = await siteapicontrollerDeletereview({
|
|
||||||
siteId,
|
|
||||||
id: String(record.id),
|
|
||||||
});
|
|
||||||
if (response.success) {
|
|
||||||
message.success('删除成功');
|
|
||||||
actionRef.current?.reload();
|
|
||||||
} else {
|
|
||||||
message.error('删除失败');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
message.error('删除失败');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button type="link" danger>
|
|
||||||
删除
|
|
||||||
</Button>
|
|
||||||
</Popconfirm>
|
|
||||||
</Space>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ProCard>
|
|
||||||
<ProTable<API.UnifiedReviewDTO>
|
|
||||||
columns={columns}
|
|
||||||
actionRef={actionRef}
|
|
||||||
request={async (params) => {
|
|
||||||
try {
|
|
||||||
const response = await siteapicontrollerGetreviews({
|
|
||||||
...params,
|
|
||||||
siteId,
|
|
||||||
page: params.current,
|
|
||||||
per_page: params.pageSize,
|
|
||||||
});
|
|
||||||
// 确保 response.data 存在
|
|
||||||
if (!response || !response.data) {
|
|
||||||
return {
|
|
||||||
data: [],
|
|
||||||
success: true,
|
|
||||||
total: 0,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
// 确保 response.data.items 是数组
|
|
||||||
const items = Array.isArray(response.data.items)
|
|
||||||
? response.data.items
|
|
||||||
: [];
|
|
||||||
// 确保每个 item 有有效的 id
|
|
||||||
const processedItems = items.map((item, index) => ({
|
|
||||||
...item,
|
|
||||||
// 如果 id 是对象,转换为字符串,否则使用索引作为后备
|
|
||||||
id:
|
|
||||||
typeof item.id === 'object'
|
|
||||||
? JSON.stringify(item.id)
|
|
||||||
: item.id || index,
|
|
||||||
// 如果 product_id 是对象,转换为字符串
|
|
||||||
product_id:
|
|
||||||
typeof item.product_id === 'object'
|
|
||||||
? JSON.stringify(item.product_id)
|
|
||||||
: item.product_id,
|
|
||||||
}));
|
|
||||||
return {
|
|
||||||
data: processedItems,
|
|
||||||
success: true,
|
|
||||||
total: Number(response.data.total) || 0,
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
console.error('获取评论失败:', error);
|
|
||||||
return {
|
|
||||||
data: [],
|
|
||||||
success: true,
|
|
||||||
total: 0,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
rowKey="id"
|
|
||||||
search={{
|
|
||||||
labelWidth: 'auto',
|
|
||||||
}}
|
|
||||||
headerTitle="评论列表"
|
|
||||||
toolBarRender={() => [
|
|
||||||
<Button
|
|
||||||
type="primary"
|
|
||||||
onClick={() => {
|
|
||||||
setEditing(null);
|
|
||||||
setOpen(true);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
新建评论
|
|
||||||
</Button>,
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
<ReviewForm
|
|
||||||
open={open}
|
|
||||||
editing={editing}
|
|
||||||
siteId={siteId}
|
|
||||||
onClose={() => setOpen(false)}
|
|
||||||
onSuccess={() => {
|
|
||||||
setOpen(false);
|
|
||||||
actionRef.current?.reload();
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</ProCard>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ReviewsPage;
|
|
||||||
|
|
@ -1,259 +0,0 @@
|
||||||
import {} from '@/servers/api/subscription';
|
|
||||||
import { DeleteFilled, EditOutlined, PlusOutlined } from '@ant-design/icons';
|
|
||||||
import {
|
|
||||||
ActionType,
|
|
||||||
PageContainer,
|
|
||||||
ProColumns,
|
|
||||||
ProTable,
|
|
||||||
} from '@ant-design/pro-components';
|
|
||||||
import { useParams } from '@umijs/max';
|
|
||||||
import { App, Button, Drawer, List, Popconfirm, Space, Tag } from 'antd';
|
|
||||||
import dayjs from 'dayjs';
|
|
||||||
import React, { useRef, useState } from 'react';
|
|
||||||
import { request } from 'umi';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 订阅状态枚举(用于筛选与展示)
|
|
||||||
* 保持与后端同步的原始状态值
|
|
||||||
*/
|
|
||||||
const SUBSCRIPTION_STATUS_ENUM: Record<string, { text: string }> = {
|
|
||||||
active: { text: '激活' },
|
|
||||||
cancelled: { text: '已取消' },
|
|
||||||
expired: { text: '已过期' },
|
|
||||||
pending: { text: '待处理' },
|
|
||||||
'on-hold': { text: '暂停' },
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 订阅列表页:展示,筛选,触发订阅同步
|
|
||||||
*/
|
|
||||||
const SubscriptionsPage: React.FC = () => {
|
|
||||||
// 表格操作引用:用于在同步后触发表格刷新
|
|
||||||
const actionRef = useRef<ActionType>();
|
|
||||||
const { message } = App.useApp();
|
|
||||||
const { siteId } = useParams<{ siteId: string }>();
|
|
||||||
|
|
||||||
// 监听 siteId 变化并重新加载表格
|
|
||||||
React.useEffect(() => {
|
|
||||||
actionRef.current?.reload();
|
|
||||||
}, [siteId]);
|
|
||||||
|
|
||||||
// 关联订单抽屉状态
|
|
||||||
const [drawerOpen, setDrawerOpen] = useState(false);
|
|
||||||
const [drawerTitle, setDrawerTitle] = useState('详情');
|
|
||||||
const [relatedOrders, setRelatedOrders] = useState<any[]>([]);
|
|
||||||
|
|
||||||
// 表格列定义(尽量与项目风格保持一致)
|
|
||||||
const [editing, setEditing] = useState<any>(null);
|
|
||||||
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
|
|
||||||
|
|
||||||
const columns: ProColumns<any>[] = [
|
|
||||||
// Site column removed
|
|
||||||
{
|
|
||||||
title: '订阅ID',
|
|
||||||
dataIndex: 'id',
|
|
||||||
hideInSearch: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '状态',
|
|
||||||
dataIndex: 'status',
|
|
||||||
valueType: 'select',
|
|
||||||
valueEnum: SUBSCRIPTION_STATUS_ENUM,
|
|
||||||
// 以 Tag 形式展示,更易辨识
|
|
||||||
render: (_, row) =>
|
|
||||||
row?.status ? (
|
|
||||||
<Tag>{SUBSCRIPTION_STATUS_ENUM[row.status]?.text || row.status}</Tag>
|
|
||||||
) : (
|
|
||||||
'-'
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '客户ID',
|
|
||||||
dataIndex: 'customer_id',
|
|
||||||
hideInSearch: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '计费周期',
|
|
||||||
dataIndex: 'billing_period',
|
|
||||||
hideInSearch: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '计费间隔',
|
|
||||||
dataIndex: 'billing_interval',
|
|
||||||
hideInSearch: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '开始时间',
|
|
||||||
dataIndex: 'start_date',
|
|
||||||
hideInSearch: true,
|
|
||||||
width: 160,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '下次支付',
|
|
||||||
dataIndex: 'next_payment_date',
|
|
||||||
hideInSearch: true,
|
|
||||||
width: 160,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// 创建时间
|
|
||||||
title: '创建时间',
|
|
||||||
dataIndex: 'date_created',
|
|
||||||
valueType: 'dateTime',
|
|
||||||
hideInSearch: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// 修改时间
|
|
||||||
title: '修改时间',
|
|
||||||
dataIndex: 'date_modified',
|
|
||||||
valueType: 'dateTime',
|
|
||||||
hideInSearch: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '操作',
|
|
||||||
valueType: 'option',
|
|
||||||
render: (_, row) => (
|
|
||||||
<Space>
|
|
||||||
<Button
|
|
||||||
type="link"
|
|
||||||
title="编辑"
|
|
||||||
icon={<EditOutlined />}
|
|
||||||
onClick={() => setEditing(row)}
|
|
||||||
/>
|
|
||||||
<Popconfirm
|
|
||||||
title="确定删除?"
|
|
||||||
onConfirm={() => message.info('订阅删除未实现')}
|
|
||||||
>
|
|
||||||
<Button type="link" danger title="删除" icon={<DeleteFilled />} />
|
|
||||||
</Popconfirm>
|
|
||||||
</Space>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<PageContainer ghost header={{ title: null, breadcrumb: undefined }}>
|
|
||||||
<ProTable<API.Subscription>
|
|
||||||
headerTitle="查询表格"
|
|
||||||
rowKey="id"
|
|
||||||
actionRef={actionRef}
|
|
||||||
/**
|
|
||||||
* 列表数据请求;保持与后端分页参数一致
|
|
||||||
* 兼容后端 data.items 或 data.list 返回字段
|
|
||||||
*/
|
|
||||||
request={async (params) => {
|
|
||||||
if (!siteId) return { data: [], success: true };
|
|
||||||
const response = await request(`/site-api/${siteId}/subscriptions`, {
|
|
||||||
params: {
|
|
||||||
...params,
|
|
||||||
page: params.current,
|
|
||||||
per_page: params.pageSize,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.success) {
|
|
||||||
message.error(response.message || '获取订阅列表失败');
|
|
||||||
return {
|
|
||||||
data: [],
|
|
||||||
total: 0,
|
|
||||||
success: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const { data } = response;
|
|
||||||
return {
|
|
||||||
total: data?.total || 0,
|
|
||||||
data: data?.items || [],
|
|
||||||
success: true,
|
|
||||||
};
|
|
||||||
}}
|
|
||||||
columns={columns}
|
|
||||||
// 工具栏:订阅同步入口
|
|
||||||
rowSelection={{ selectedRowKeys, onChange: setSelectedRowKeys }}
|
|
||||||
toolBarRender={() => [
|
|
||||||
<Button
|
|
||||||
type="primary"
|
|
||||||
title="新增"
|
|
||||||
icon={<PlusOutlined />}
|
|
||||||
onClick={() => message.info('订阅新增未实现')}
|
|
||||||
/>,
|
|
||||||
<Button
|
|
||||||
title="批量编辑"
|
|
||||||
icon={<EditOutlined />}
|
|
||||||
onClick={() => message.info('批量编辑未实现')}
|
|
||||||
/>,
|
|
||||||
<Button
|
|
||||||
title="批量导出"
|
|
||||||
onClick={async () => {
|
|
||||||
if (!siteId) return;
|
|
||||||
const res = await request(
|
|
||||||
`/site-api/${siteId}/subscriptions/export`,
|
|
||||||
{ params: {} },
|
|
||||||
);
|
|
||||||
if (res?.success && res?.data?.csv) {
|
|
||||||
const blob = new Blob([res.data.csv], {
|
|
||||||
type: 'text/csv;charset=utf-8;',
|
|
||||||
});
|
|
||||||
const url = URL.createObjectURL(blob);
|
|
||||||
const a = document.createElement('a');
|
|
||||||
a.href = url;
|
|
||||||
a.download = 'subscriptions.csv';
|
|
||||||
a.click();
|
|
||||||
URL.revokeObjectURL(url);
|
|
||||||
} else {
|
|
||||||
message.error(res.message || '导出失败');
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>,
|
|
||||||
<Button
|
|
||||||
title="批量删除"
|
|
||||||
danger
|
|
||||||
icon={<DeleteFilled />}
|
|
||||||
onClick={() => message.info('订阅删除未实现')}
|
|
||||||
/>,
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
<Drawer
|
|
||||||
open={drawerOpen}
|
|
||||||
title={drawerTitle}
|
|
||||||
width={720}
|
|
||||||
onClose={() => setDrawerOpen(false)}
|
|
||||||
>
|
|
||||||
<List
|
|
||||||
header={<div>关联订单</div>}
|
|
||||||
dataSource={relatedOrders}
|
|
||||||
renderItem={(item: any) => (
|
|
||||||
<List.Item>
|
|
||||||
<List.Item.Meta
|
|
||||||
title={`#${item?.externalOrderId || '-'}`}
|
|
||||||
description={`关系:${item?.relationship || '-'},站点:${
|
|
||||||
item?.name || '-'
|
|
||||||
}`}
|
|
||||||
/>
|
|
||||||
<div style={{ display: 'flex', gap: 12, alignItems: 'center' }}>
|
|
||||||
<span>
|
|
||||||
{item?.date_created
|
|
||||||
? dayjs(item.date_created).format('YYYY-MM-DD HH:mm')
|
|
||||||
: '-'}
|
|
||||||
</span>
|
|
||||||
<Tag>{item?.status || '-'}</Tag>
|
|
||||||
<span>
|
|
||||||
{item?.currency_symbol || ''}
|
|
||||||
{typeof item?.total === 'number'
|
|
||||||
? item.total.toFixed(2)
|
|
||||||
: item?.total ?? '-'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</List.Item>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</Drawer>
|
|
||||||
</PageContainer>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 同步订阅抽屉表单:选择站点后触发同步
|
|
||||||
*/
|
|
||||||
// 已移除订阅同步入口,改为直接从站点实时获取
|
|
||||||
|
|
||||||
export default SubscriptionsPage;
|
|
||||||
|
|
@ -1,332 +0,0 @@
|
||||||
import {
|
|
||||||
siteapicontrollerCreatewebhook,
|
|
||||||
siteapicontrollerDeletewebhook,
|
|
||||||
siteapicontrollerGetwebhooks,
|
|
||||||
siteapicontrollerUpdatewebhook,
|
|
||||||
} from '@/servers/api/siteApi';
|
|
||||||
import {
|
|
||||||
ActionType,
|
|
||||||
ProCard,
|
|
||||||
ProColumns,
|
|
||||||
ProTable,
|
|
||||||
} from '@ant-design/pro-components';
|
|
||||||
import { useParams } from '@umijs/max';
|
|
||||||
import {
|
|
||||||
Button,
|
|
||||||
Form,
|
|
||||||
Input,
|
|
||||||
message,
|
|
||||||
Modal,
|
|
||||||
Popconfirm,
|
|
||||||
Select,
|
|
||||||
Space,
|
|
||||||
} from 'antd';
|
|
||||||
import React, { useRef, useState } from 'react';
|
|
||||||
|
|
||||||
const WebhooksPage: React.FC = () => {
|
|
||||||
const params = useParams();
|
|
||||||
const siteId = Number(params.siteId);
|
|
||||||
const actionRef = useRef<ActionType>();
|
|
||||||
|
|
||||||
// 模态框状态
|
|
||||||
const [isModalVisible, setIsModalVisible] = useState(false);
|
|
||||||
const [isEditMode, setIsEditMode] = useState(false);
|
|
||||||
const [currentWebhook, setCurrentWebhook] =
|
|
||||||
useState<API.UnifiedWebhookDTO | null>(null);
|
|
||||||
|
|
||||||
// 表单实例
|
|
||||||
const [form] = Form.useForm();
|
|
||||||
|
|
||||||
// webhook主题选项
|
|
||||||
const webhookTopics = [
|
|
||||||
{ label: '订单创建', value: 'order.created' },
|
|
||||||
{ label: '订单更新', value: 'order.updated' },
|
|
||||||
{ label: '订单删除', value: 'order.deleted' },
|
|
||||||
{ label: '产品创建', value: 'product.created' },
|
|
||||||
{ label: '产品更新', value: 'product.updated' },
|
|
||||||
{ label: '产品删除', value: 'product.deleted' },
|
|
||||||
{ label: '客户创建', value: 'customer.created' },
|
|
||||||
{ label: '客户更新', value: 'customer.updated' },
|
|
||||||
{ label: '客户删除', value: 'customer.deleted' },
|
|
||||||
];
|
|
||||||
|
|
||||||
// webhook状态选项
|
|
||||||
const webhookStatuses = [
|
|
||||||
{ label: '活跃', value: 'active' },
|
|
||||||
{ label: '非活跃', value: 'inactive' },
|
|
||||||
];
|
|
||||||
|
|
||||||
// 打开新建模态框
|
|
||||||
const showCreateModal = () => {
|
|
||||||
setIsEditMode(false);
|
|
||||||
setCurrentWebhook(null);
|
|
||||||
form.resetFields();
|
|
||||||
setIsModalVisible(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 打开编辑模态框
|
|
||||||
const showEditModal = async (record: API.UnifiedWebhookDTO) => {
|
|
||||||
setIsEditMode(true);
|
|
||||||
setCurrentWebhook(record);
|
|
||||||
try {
|
|
||||||
// 如果需要获取最新的webhook数据,可以取消下面的注释
|
|
||||||
// const response = await siteapicontrollerGetwebhook({ siteId, id: String(record.id) });
|
|
||||||
// if (response.success && response.data) {
|
|
||||||
// form.setFieldsValue(response.data);
|
|
||||||
// } else {
|
|
||||||
// form.setFieldsValue(record);
|
|
||||||
// }
|
|
||||||
form.setFieldsValue(record);
|
|
||||||
setIsModalVisible(true);
|
|
||||||
} catch (error) {
|
|
||||||
message.error('加载webhook数据失败');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 关闭模态框
|
|
||||||
const handleCancel = () => {
|
|
||||||
setIsModalVisible(false);
|
|
||||||
form.resetFields();
|
|
||||||
};
|
|
||||||
|
|
||||||
// 提交表单
|
|
||||||
const handleSubmit = async () => {
|
|
||||||
try {
|
|
||||||
const values = await form.validateFields();
|
|
||||||
|
|
||||||
// 准备提交数据
|
|
||||||
const webhookData = {
|
|
||||||
...values,
|
|
||||||
siteId,
|
|
||||||
};
|
|
||||||
|
|
||||||
let response;
|
|
||||||
if (isEditMode && currentWebhook?.id) {
|
|
||||||
// 更新webhook
|
|
||||||
response = await siteapicontrollerUpdatewebhook({
|
|
||||||
...webhookData,
|
|
||||||
id: String(currentWebhook.id),
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// 创建新webhook
|
|
||||||
response = await siteapicontrollerCreatewebhook(webhookData);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (response.success) {
|
|
||||||
message.success(isEditMode ? '更新成功' : '创建成功');
|
|
||||||
setIsModalVisible(false);
|
|
||||||
form.resetFields();
|
|
||||||
actionRef.current?.reload();
|
|
||||||
} else {
|
|
||||||
message.error(isEditMode ? '更新失败' : '创建失败');
|
|
||||||
}
|
|
||||||
} catch (error: any) {
|
|
||||||
message.error('表单验证失败:' + error.message);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const columns: ProColumns<API.UnifiedWebhookDTO>[] = [
|
|
||||||
{ title: 'ID', dataIndex: 'id', key: 'id', width: 50 },
|
|
||||||
{ title: '名称', dataIndex: 'name', key: 'name' },
|
|
||||||
{ title: '主题', dataIndex: 'topic', key: 'topic' },
|
|
||||||
{
|
|
||||||
title: '回调URL',
|
|
||||||
dataIndex: 'delivery_url',
|
|
||||||
key: 'delivery_url',
|
|
||||||
ellipsis: true,
|
|
||||||
},
|
|
||||||
{ title: '状态', dataIndex: 'status', key: 'status', width: 80 },
|
|
||||||
{
|
|
||||||
title: '创建时间',
|
|
||||||
dataIndex: 'date_created',
|
|
||||||
key: 'date_created',
|
|
||||||
valueType: 'dateTime',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '更新时间',
|
|
||||||
dataIndex: 'date_modified',
|
|
||||||
key: 'date_modified',
|
|
||||||
valueType: 'dateTime',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '操作',
|
|
||||||
key: 'action',
|
|
||||||
width: 150,
|
|
||||||
render: (_, record) => (
|
|
||||||
<Space>
|
|
||||||
<Button
|
|
||||||
type="link"
|
|
||||||
style={{ padding: 0 }}
|
|
||||||
onClick={() => showEditModal(record)}
|
|
||||||
>
|
|
||||||
编辑
|
|
||||||
</Button>
|
|
||||||
<Popconfirm
|
|
||||||
title="确定删除吗?"
|
|
||||||
onConfirm={async () => {
|
|
||||||
if (record.id) {
|
|
||||||
try {
|
|
||||||
const response = await siteapicontrollerDeletewebhook({
|
|
||||||
siteId,
|
|
||||||
id: String(record.id),
|
|
||||||
});
|
|
||||||
if (response.success) {
|
|
||||||
message.success('删除成功');
|
|
||||||
actionRef.current?.reload();
|
|
||||||
} else {
|
|
||||||
message.error('删除失败');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
message.error('删除失败');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button type="link" danger>
|
|
||||||
删除
|
|
||||||
</Button>
|
|
||||||
</Popconfirm>
|
|
||||||
</Space>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<ProCard>
|
|
||||||
<ProTable<API.UnifiedWebhookDTO>
|
|
||||||
columns={columns}
|
|
||||||
actionRef={actionRef}
|
|
||||||
request={async (params) => {
|
|
||||||
try {
|
|
||||||
const response = await siteapicontrollerGetwebhooks({
|
|
||||||
...params,
|
|
||||||
siteId,
|
|
||||||
page: params.current,
|
|
||||||
per_page: params.pageSize,
|
|
||||||
});
|
|
||||||
// 确保 response.data 存在
|
|
||||||
if (!response || !response.data) {
|
|
||||||
return {
|
|
||||||
data: [],
|
|
||||||
success: true,
|
|
||||||
total: 0,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
// 确保 response.data.items 是数组
|
|
||||||
const items = Array.isArray(response.data.items)
|
|
||||||
? response.data.items
|
|
||||||
: [];
|
|
||||||
// 确保每个 item 有有效的 id
|
|
||||||
const processedItems = items.map((item, index) => ({
|
|
||||||
...item,
|
|
||||||
// 如果 id 是对象,转换为字符串,否则使用索引作为后备
|
|
||||||
id:
|
|
||||||
typeof item.id === 'object'
|
|
||||||
? JSON.stringify(item.id)
|
|
||||||
: item.id || index,
|
|
||||||
}));
|
|
||||||
return {
|
|
||||||
data: processedItems,
|
|
||||||
success: true,
|
|
||||||
total: Number(response.data.total) || 0,
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
console.error('获取webhooks失败:', error);
|
|
||||||
return {
|
|
||||||
data: [],
|
|
||||||
success: true,
|
|
||||||
total: 0,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
rowKey="id"
|
|
||||||
search={{
|
|
||||||
labelWidth: 'auto',
|
|
||||||
}}
|
|
||||||
headerTitle="Webhooks列表"
|
|
||||||
toolBarRender={() => [
|
|
||||||
<Button type="primary" onClick={showCreateModal}>
|
|
||||||
新建Webhook
|
|
||||||
</Button>,
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</ProCard>
|
|
||||||
|
|
||||||
{/* Webhook编辑/新建模态框 */}
|
|
||||||
<Modal
|
|
||||||
title={isEditMode ? '编辑Webhook' : '新建Webhook'}
|
|
||||||
open={isModalVisible}
|
|
||||||
onCancel={handleCancel}
|
|
||||||
footer={[
|
|
||||||
<Button key="back" onClick={handleCancel}>
|
|
||||||
取消
|
|
||||||
</Button>,
|
|
||||||
<Button key="submit" type="primary" onClick={handleSubmit}>
|
|
||||||
{isEditMode ? '更新' : '创建'}
|
|
||||||
</Button>,
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<Form
|
|
||||||
form={form}
|
|
||||||
layout="vertical"
|
|
||||||
initialValues={{
|
|
||||||
status: 'active',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Form.Item
|
|
||||||
name="name"
|
|
||||||
label="名称"
|
|
||||||
rules={[
|
|
||||||
{ required: true, message: '请输入webhook名称' },
|
|
||||||
{ max: 100, message: '名称不能超过100个字符' },
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<Input placeholder="请输入webhook名称" />
|
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
<Form.Item
|
|
||||||
name="topic"
|
|
||||||
label="主题"
|
|
||||||
rules={[{ required: true, message: '请选择webhook主题' }]}
|
|
||||||
>
|
|
||||||
<Select
|
|
||||||
placeholder="请选择webhook主题"
|
|
||||||
options={webhookTopics}
|
|
||||||
allowClear
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
<Form.Item
|
|
||||||
name="delivery_url"
|
|
||||||
label="回调URL"
|
|
||||||
rules={[
|
|
||||||
{ required: true, message: '请输入回调URL' },
|
|
||||||
{ type: 'url', message: '请输入有效的URL' },
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<Input placeholder="请输入回调URL,如:https://example.com/webhook" />
|
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
<Form.Item
|
|
||||||
name="secret"
|
|
||||||
label="密钥(可选)"
|
|
||||||
rules={[{ max: 255, message: '密钥不能超过255个字符' }]}
|
|
||||||
>
|
|
||||||
<Input placeholder="请输入密钥,用于验证webhook请求" />
|
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
<Form.Item
|
|
||||||
name="status"
|
|
||||||
label="状态"
|
|
||||||
rules={[{ required: true, message: '请选择webhook状态' }]}
|
|
||||||
>
|
|
||||||
<Select placeholder="请选择webhook状态" options={webhookStatuses} />
|
|
||||||
</Form.Item>
|
|
||||||
</Form>
|
|
||||||
</Modal>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default WebhooksPage;
|
|
||||||
|
|
@ -1,941 +0,0 @@
|
||||||
import { ORDER_STATUS_ENUM } from '@/constants';
|
|
||||||
import {
|
|
||||||
logisticscontrollerCreateshipment,
|
|
||||||
logisticscontrollerGetshippingaddresslist,
|
|
||||||
} from '@/servers/api/logistics';
|
|
||||||
import { productcontrollerSearchproducts } from '@/servers/api/product';
|
|
||||||
import { stockcontrollerGetallstockpoints } from '@/servers/api/stock';
|
|
||||||
import {
|
|
||||||
CodeSandboxOutlined,
|
|
||||||
EditOutlined,
|
|
||||||
PlusOutlined,
|
|
||||||
TagsOutlined,
|
|
||||||
} from '@ant-design/icons';
|
|
||||||
import {
|
|
||||||
ActionType,
|
|
||||||
DrawerForm,
|
|
||||||
ModalForm,
|
|
||||||
ProColumns,
|
|
||||||
ProForm,
|
|
||||||
ProFormDatePicker,
|
|
||||||
ProFormDigit,
|
|
||||||
ProFormInstance,
|
|
||||||
ProFormList,
|
|
||||||
ProFormSelect,
|
|
||||||
ProFormText,
|
|
||||||
ProFormTextArea,
|
|
||||||
ProTable,
|
|
||||||
} from '@ant-design/pro-components';
|
|
||||||
import { request } from '@umijs/max';
|
|
||||||
import { App, Button, Col, Divider, Row } from 'antd';
|
|
||||||
import dayjs from 'dayjs';
|
|
||||||
import React, { useRef, useState } from 'react';
|
|
||||||
|
|
||||||
const region = {
|
|
||||||
AB: 'Alberta',
|
|
||||||
BC: 'British',
|
|
||||||
MB: 'Manitoba',
|
|
||||||
NB: 'New',
|
|
||||||
NL: 'Newfoundland',
|
|
||||||
NS: 'Nova',
|
|
||||||
ON: 'Ontario',
|
|
||||||
PE: 'Prince',
|
|
||||||
QC: 'Quebec',
|
|
||||||
SK: 'Saskatchewan',
|
|
||||||
NT: 'Northwest',
|
|
||||||
NU: 'Nunavut',
|
|
||||||
YT: 'Yukon',
|
|
||||||
};
|
|
||||||
|
|
||||||
// 定义发货订单表单的数据类型
|
|
||||||
export interface ShipOrderFormData {
|
|
||||||
tracking_number?: string;
|
|
||||||
shipping_provider?: string;
|
|
||||||
shipping_method?: string;
|
|
||||||
items?: Array<{
|
|
||||||
id?: string;
|
|
||||||
quantity?: number;
|
|
||||||
}>;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 发货订单表单组件
|
|
||||||
export const ShipOrderForm: React.FC<{
|
|
||||||
orderId: number;
|
|
||||||
tableRef?: React.MutableRefObject<ActionType | undefined>;
|
|
||||||
siteId?: string;
|
|
||||||
orderItems?: Array<{
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
quantity: number;
|
|
||||||
sku?: string;
|
|
||||||
}>;
|
|
||||||
}> = ({ orderId, tableRef, siteId, orderItems }) => {
|
|
||||||
const { message } = App.useApp();
|
|
||||||
const formRef = useRef<ProFormInstance>();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ModalForm
|
|
||||||
formRef={formRef}
|
|
||||||
title="发货订单"
|
|
||||||
width="600px"
|
|
||||||
modalProps={{ destroyOnHidden: true }}
|
|
||||||
trigger={
|
|
||||||
<Button type="link" title="发货">
|
|
||||||
发货
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
onFinish={async (values: ShipOrderFormData) => {
|
|
||||||
if (!siteId) {
|
|
||||||
message.error('缺少站点ID');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const { success, message: errMsg } = await request(
|
|
||||||
`/site-api/${siteId}/orders/${orderId}/ship`,
|
|
||||||
{
|
|
||||||
method: 'POST',
|
|
||||||
data: values,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
if (success === false) {
|
|
||||||
throw new Error(errMsg || '发货失败');
|
|
||||||
}
|
|
||||||
|
|
||||||
message.success('发货成功');
|
|
||||||
tableRef?.current?.reload();
|
|
||||||
return true;
|
|
||||||
} catch (error: any) {
|
|
||||||
message.error(error?.message || '发货失败');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onFinishFailed={() => {
|
|
||||||
const element = document.querySelector('.ant-form-item-explain-error');
|
|
||||||
if (element) {
|
|
||||||
element.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ProFormText
|
|
||||||
name="tracking_number"
|
|
||||||
label="物流单号"
|
|
||||||
placeholder="请输入物流单号"
|
|
||||||
rules={[{ required: true, message: '请输入物流单号' }]}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ProFormText
|
|
||||||
name="shipping_provider"
|
|
||||||
label="物流公司"
|
|
||||||
placeholder="请输入物流公司名称"
|
|
||||||
rules={[{ required: true, message: '请输入物流公司名称' }]}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ProFormText
|
|
||||||
name="shipping_method"
|
|
||||||
label="发货方式"
|
|
||||||
placeholder="请输入发货方式"
|
|
||||||
/>
|
|
||||||
|
|
||||||
{orderItems && orderItems.length > 0 && (
|
|
||||||
<ProFormList
|
|
||||||
label="发货商品项"
|
|
||||||
name="items"
|
|
||||||
tooltip="如果不选择,则默认发货所有商品"
|
|
||||||
>
|
|
||||||
<ProForm.Group>
|
|
||||||
<ProFormSelect
|
|
||||||
name="id"
|
|
||||||
label="商品"
|
|
||||||
placeholder="请选择商品"
|
|
||||||
options={orderItems.map((item) => ({
|
|
||||||
label: `${item.name} (SKU: ${item.sku || 'N/A'}) - 可发数量: ${
|
|
||||||
item.quantity
|
|
||||||
}`,
|
|
||||||
value: item.id,
|
|
||||||
}))}
|
|
||||||
rules={[{ required: true, message: '请选择商品' }]}
|
|
||||||
/>
|
|
||||||
<ProFormDigit
|
|
||||||
name="quantity"
|
|
||||||
label="发货数量"
|
|
||||||
placeholder="请输入发货数量"
|
|
||||||
rules={[{ required: true, message: '请输入发货数量' }]}
|
|
||||||
fieldProps={{
|
|
||||||
precision: 0,
|
|
||||||
min: 1,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</ProForm.Group>
|
|
||||||
</ProFormList>
|
|
||||||
)}
|
|
||||||
</ModalForm>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const OrderNote: React.FC<{
|
|
||||||
id: number;
|
|
||||||
descRef?: React.MutableRefObject<ActionType | undefined>;
|
|
||||||
siteId?: string;
|
|
||||||
}> = ({ id, descRef, siteId }) => {
|
|
||||||
const { message } = App.useApp();
|
|
||||||
return (
|
|
||||||
<ModalForm
|
|
||||||
title="添加备注"
|
|
||||||
trigger={
|
|
||||||
<Button type="primary" ghost size="small" icon={<TagsOutlined />}>
|
|
||||||
备注
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
onFinish={async (values: any) => {
|
|
||||||
if (!siteId) {
|
|
||||||
message.error('缺少站点ID');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
// Use new API for creating note
|
|
||||||
const { success, data } = await request(
|
|
||||||
`/site-api/${siteId}/orders/${id}/notes`,
|
|
||||||
{
|
|
||||||
method: 'POST',
|
|
||||||
data: {
|
|
||||||
...values,
|
|
||||||
orderId: id, // API might not need this in body if in URL, but keeping for compatibility if adapter needs it
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
// Check success based on response structure
|
|
||||||
if (success === false) {
|
|
||||||
// Assuming response.util returns success: boolean
|
|
||||||
throw new Error('提交失败');
|
|
||||||
}
|
|
||||||
|
|
||||||
descRef?.current?.reload();
|
|
||||||
message.success('提交成功');
|
|
||||||
return true;
|
|
||||||
} catch (error: any) {
|
|
||||||
message.error(error.message);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ProFormTextArea
|
|
||||||
name="content"
|
|
||||||
label="内容"
|
|
||||||
width="lg"
|
|
||||||
placeholder="请输入备注"
|
|
||||||
rules={[{ required: true, message: '请输入备注' }]}
|
|
||||||
/>
|
|
||||||
</ModalForm>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const AddressPicker: React.FC<{
|
|
||||||
value?: any;
|
|
||||||
onChange?: (value: any) => void;
|
|
||||||
}> = ({ onChange, value }) => {
|
|
||||||
const [selectedRow, setSelectedRow] = useState(null);
|
|
||||||
const { message } = App.useApp();
|
|
||||||
const columns: ProColumns<API.ShippingAddress>[] = [
|
|
||||||
{
|
|
||||||
title: '仓库点',
|
|
||||||
dataIndex: 'stockPointId',
|
|
||||||
hideInSearch: true,
|
|
||||||
valueType: 'select',
|
|
||||||
request: async () => {
|
|
||||||
const { data = [] } = await stockcontrollerGetallstockpoints();
|
|
||||||
return data.map((item) => ({
|
|
||||||
label: item.name,
|
|
||||||
value: item.id,
|
|
||||||
}));
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '地区',
|
|
||||||
dataIndex: ['address', 'region'],
|
|
||||||
hideInSearch: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '城市',
|
|
||||||
dataIndex: ['address', 'city'],
|
|
||||||
hideInSearch: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '邮编',
|
|
||||||
dataIndex: ['address', 'postal_code'],
|
|
||||||
hideInSearch: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '详细地址',
|
|
||||||
dataIndex: ['address', 'address_line_1'],
|
|
||||||
hideInSearch: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '联系电话',
|
|
||||||
render: (_, record) =>
|
|
||||||
`+${record.phone_number_extension} ${record.phone_number}`,
|
|
||||||
hideInSearch: true,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
return (
|
|
||||||
<ModalForm
|
|
||||||
title="选择地址"
|
|
||||||
trigger={<Button type="primary">选择地址</Button>}
|
|
||||||
modalProps={{ destroyOnHidden: true }}
|
|
||||||
onFinish={async () => {
|
|
||||||
if (!selectedRow) {
|
|
||||||
message.error('请选择地址');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (onChange) onChange(selectedRow);
|
|
||||||
return true;
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ProTable
|
|
||||||
rowKey="id"
|
|
||||||
request={async () => {
|
|
||||||
const { data, success } =
|
|
||||||
await logisticscontrollerGetshippingaddresslist();
|
|
||||||
if (success) {
|
|
||||||
return {
|
|
||||||
data: data,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
data: [],
|
|
||||||
};
|
|
||||||
}}
|
|
||||||
columns={columns}
|
|
||||||
search={false}
|
|
||||||
rowSelection={{
|
|
||||||
type: 'radio',
|
|
||||||
onChange: (_, selectedRows) => {
|
|
||||||
setSelectedRow(selectedRows[0]);
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</ModalForm>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Shipping: React.FC<{
|
|
||||||
id: number;
|
|
||||||
tableRef?: React.MutableRefObject<ActionType | undefined>;
|
|
||||||
descRef?: React.MutableRefObject<ActionType | undefined>;
|
|
||||||
reShipping?: boolean;
|
|
||||||
setActiveLine: Function;
|
|
||||||
siteId?: string;
|
|
||||||
}> = ({ id, tableRef, descRef, reShipping = false, setActiveLine, siteId }) => {
|
|
||||||
const [options, setOptions] = useState<any[]>([]);
|
|
||||||
const formRef = useRef<ProFormInstance>();
|
|
||||||
|
|
||||||
const [shipmentFee, setShipmentFee] = useState<number>(0);
|
|
||||||
const [ratesLoading, setRatesLoading] = useState(false);
|
|
||||||
const { message } = App.useApp();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ModalForm
|
|
||||||
formRef={formRef}
|
|
||||||
title="创建运单"
|
|
||||||
size="large"
|
|
||||||
width="80vw"
|
|
||||||
modalProps={{
|
|
||||||
destroyOnHidden: true,
|
|
||||||
styles: {
|
|
||||||
body: { maxHeight: '65vh', overflowY: 'auto', overflowX: 'hidden' },
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
trigger={
|
|
||||||
<Button
|
|
||||||
type="primary"
|
|
||||||
size="small"
|
|
||||||
icon={<CodeSandboxOutlined />}
|
|
||||||
onClick={() => {
|
|
||||||
setActiveLine(id);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
发货
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
request={async () => {
|
|
||||||
if (!siteId) return {};
|
|
||||||
// Use site-api to get order detail
|
|
||||||
const { data, success } = await request(
|
|
||||||
`/site-api/${siteId}/orders/${id}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!success || !data) return {};
|
|
||||||
|
|
||||||
// Use 'sales' which I added to DTO
|
|
||||||
const sales = data.sales || [];
|
|
||||||
|
|
||||||
// Logic for merging duplicate products
|
|
||||||
const mergedSales = sales.reduce((acc: any[], cur: any) => {
|
|
||||||
let idx = acc.findIndex((v: any) => v.productId === cur.productId);
|
|
||||||
if (idx === -1) {
|
|
||||||
acc.push({ ...cur }); // clone
|
|
||||||
} else {
|
|
||||||
acc[idx].quantity += cur.quantity;
|
|
||||||
}
|
|
||||||
return acc;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Update data.sales
|
|
||||||
data.sales = mergedSales;
|
|
||||||
|
|
||||||
setOptions(
|
|
||||||
data.sales?.map((item: any) => ({
|
|
||||||
label: item.name,
|
|
||||||
value: item.sku,
|
|
||||||
})) || [],
|
|
||||||
);
|
|
||||||
if (reShipping) data.sales = [{}];
|
|
||||||
let shipmentInfo = localStorage.getItem('shipmentInfo');
|
|
||||||
if (shipmentInfo) shipmentInfo = JSON.parse(shipmentInfo);
|
|
||||||
return {
|
|
||||||
...data,
|
|
||||||
stockPointId: shipmentInfo?.stockPointId,
|
|
||||||
details: {
|
|
||||||
destination: {
|
|
||||||
name: data?.shipping?.company || data?.billing?.company || ' ',
|
|
||||||
address: {
|
|
||||||
address_line_1:
|
|
||||||
data?.shipping?.address_1 || data?.billing?.address_1,
|
|
||||||
city: data?.shipping?.city || data?.billing?.city,
|
|
||||||
region: data?.shipping?.state || data?.billing?.state,
|
|
||||||
postal_code:
|
|
||||||
data?.shipping?.postcode || data?.billing?.postcode,
|
|
||||||
},
|
|
||||||
contact_name:
|
|
||||||
data?.shipping?.first_name || data?.shipping?.last_name
|
|
||||||
? `${data?.shipping?.first_name} ${data?.shipping?.last_name}`
|
|
||||||
: `${data?.billing?.first_name} ${data?.billing?.last_name}`,
|
|
||||||
phone_number: {
|
|
||||||
phone: data?.shipping?.phone || data?.billing?.phone,
|
|
||||||
},
|
|
||||||
email_addresses: data?.shipping?.email || data?.billing?.email,
|
|
||||||
signature_requirement: 'not-required',
|
|
||||||
},
|
|
||||||
origin: {
|
|
||||||
name: data?.name, // name? order name?
|
|
||||||
email_addresses: data?.email,
|
|
||||||
contact_name: data?.name,
|
|
||||||
phone_number: shipmentInfo?.phone_number,
|
|
||||||
address: {
|
|
||||||
region: shipmentInfo?.region,
|
|
||||||
city: shipmentInfo?.city,
|
|
||||||
postal_code: shipmentInfo?.postal_code,
|
|
||||||
address_line_1: shipmentInfo?.address_line_1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
packaging_type: 'package',
|
|
||||||
expected_ship_date: dayjs(),
|
|
||||||
packaging_properties: {
|
|
||||||
packages: [
|
|
||||||
{
|
|
||||||
measurements: {
|
|
||||||
weight: {
|
|
||||||
unit: 'LBS',
|
|
||||||
value: 1,
|
|
||||||
},
|
|
||||||
cuboid: {
|
|
||||||
unit: 'IN',
|
|
||||||
l: 6,
|
|
||||||
w: 4,
|
|
||||||
h: 4,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
description: 'food',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}}
|
|
||||||
onFinish={async ({
|
|
||||||
customer_note,
|
|
||||||
notes,
|
|
||||||
items,
|
|
||||||
details,
|
|
||||||
externalOrderId,
|
|
||||||
...data
|
|
||||||
}) => {
|
|
||||||
// Warning: This uses local logistics controller which might expect local ID.
|
|
||||||
// We are passing 'id' which is now External ID (if we fetch via site-api).
|
|
||||||
// If logistics module doesn't handle external ID, this will fail.
|
|
||||||
|
|
||||||
details.origin.email_addresses =
|
|
||||||
details.origin.email_addresses.split(',');
|
|
||||||
details.destination.email_addresses =
|
|
||||||
details.destination.email_addresses.split(',');
|
|
||||||
details.destination.phone_number.number =
|
|
||||||
details.destination.phone_number.phone;
|
|
||||||
details.origin.phone_number.number = details.origin.phone_number.phone;
|
|
||||||
try {
|
|
||||||
const {
|
|
||||||
success,
|
|
||||||
message: errMsg,
|
|
||||||
...resShipment
|
|
||||||
} = await logisticscontrollerCreateshipment(
|
|
||||||
{ orderId: id },
|
|
||||||
{
|
|
||||||
details,
|
|
||||||
...data,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
if (!success) throw new Error(errMsg);
|
|
||||||
message.success('创建成功');
|
|
||||||
tableRef?.current?.reload();
|
|
||||||
descRef?.current?.reload();
|
|
||||||
|
|
||||||
localStorage.setItem(
|
|
||||||
'shipmentInfo',
|
|
||||||
JSON.stringify({
|
|
||||||
stockPointId: data.stockPointId,
|
|
||||||
region: details.origin.address.region,
|
|
||||||
city: details.origin.address.city,
|
|
||||||
postal_code: details.origin.address.postal_code,
|
|
||||||
address_line_1: details.origin.address.address_line_1,
|
|
||||||
phone_number: details.origin.phone_number,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
return true;
|
|
||||||
} catch (error: any) {
|
|
||||||
message.error(error?.message || '创建失败');
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onFinishFailed={() => {
|
|
||||||
const element = document.querySelector('.ant-form-item-explain-error');
|
|
||||||
if (element) {
|
|
||||||
element.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ProFormText label="订单号" readonly name={'externalOrderId'} />
|
|
||||||
<ProFormText label="客户备注" readonly name="customer_note" />
|
|
||||||
<ProFormList
|
|
||||||
label="后台备注"
|
|
||||||
name="notes"
|
|
||||||
actionRender={() => []}
|
|
||||||
readonly
|
|
||||||
>
|
|
||||||
<ProFormText readonly name="content" />
|
|
||||||
</ProFormList>
|
|
||||||
<Row gutter={16}>
|
|
||||||
<Col span={12}>
|
|
||||||
<ProFormSelect
|
|
||||||
label="合并发货订单号"
|
|
||||||
name="orderIds"
|
|
||||||
showSearch
|
|
||||||
mode="multiple"
|
|
||||||
// request={...} // Removed or update to use site-api search?
|
|
||||||
// Existing logic uses ordercontrollerGetorderbynumber (local).
|
|
||||||
// If we use site-api, we should search site-api.
|
|
||||||
// But site-api doesn't have order search by number yet.
|
|
||||||
// I'll leave it empty/disabled for now.
|
|
||||||
options={[]}
|
|
||||||
disabled
|
|
||||||
placeholder="暂不支持合并外部订单发货"
|
|
||||||
/>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
<Row gutter={16}>
|
|
||||||
<Col span={12}>
|
|
||||||
<ProFormList
|
|
||||||
label="原始订单"
|
|
||||||
name="items"
|
|
||||||
readonly
|
|
||||||
actionRender={() => []}
|
|
||||||
>
|
|
||||||
<ProForm.Group>
|
|
||||||
<ProFormText name="name" readonly />
|
|
||||||
<ProFormDigit name="quantity" readonly />
|
|
||||||
</ProForm.Group>
|
|
||||||
</ProFormList>
|
|
||||||
</Col>
|
|
||||||
<Col span={12}>
|
|
||||||
<ProFormList label="发货产品" name="sales">
|
|
||||||
<ProForm.Group>
|
|
||||||
<ProFormSelect
|
|
||||||
params={{ options }}
|
|
||||||
request={async ({ keyWords, options }) => {
|
|
||||||
if (!keyWords || keyWords.length < 2) return options;
|
|
||||||
try {
|
|
||||||
const { data } = await productcontrollerSearchproducts({
|
|
||||||
name: keyWords,
|
|
||||||
});
|
|
||||||
return (
|
|
||||||
data?.map((item) => {
|
|
||||||
return {
|
|
||||||
label: `${item.name} - ${item.nameCn}`,
|
|
||||||
value: item?.sku,
|
|
||||||
};
|
|
||||||
}) || options
|
|
||||||
);
|
|
||||||
} catch (error: any) {
|
|
||||||
return options;
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
name="sku"
|
|
||||||
label="产品"
|
|
||||||
placeholder="请选择产品"
|
|
||||||
tooltip="至少输入3个字符"
|
|
||||||
fieldProps={{
|
|
||||||
showSearch: true,
|
|
||||||
filterOption: false,
|
|
||||||
}}
|
|
||||||
debounceTime={300} // 防抖,减少请求频率
|
|
||||||
rules={[{ required: true, message: '请选择产品' }]}
|
|
||||||
/>
|
|
||||||
<ProFormDigit
|
|
||||||
name="quantity"
|
|
||||||
colProps={{ span: 12 }}
|
|
||||||
label="数量"
|
|
||||||
placeholder="请输入数量"
|
|
||||||
rules={[{ required: true, message: '请输入数量' }]}
|
|
||||||
fieldProps={{
|
|
||||||
precision: 0,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</ProForm.Group>
|
|
||||||
</ProFormList>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
<Row gutter={16}>
|
|
||||||
<Col span={12}>
|
|
||||||
<ProForm.Group
|
|
||||||
title="发货信息"
|
|
||||||
extra={
|
|
||||||
<AddressPicker
|
|
||||||
onChange={({
|
|
||||||
address,
|
|
||||||
phone_number,
|
|
||||||
phone_number_extension,
|
|
||||||
stockPointId,
|
|
||||||
}) => {
|
|
||||||
formRef?.current?.setFieldsValue({
|
|
||||||
stockPointId,
|
|
||||||
details: {
|
|
||||||
origin: {
|
|
||||||
address,
|
|
||||||
phone_number: {
|
|
||||||
phone: phone_number,
|
|
||||||
extension: phone_number_extension,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<ProFormSelect
|
|
||||||
name="stockPointId"
|
|
||||||
width="md"
|
|
||||||
label="发货仓库点"
|
|
||||||
placeholder="请选择仓库点"
|
|
||||||
rules={[{ required: true, message: '请选择发货仓库点' }]}
|
|
||||||
request={async () => {
|
|
||||||
const { data = [] } = await stockcontrollerGetallstockpoints();
|
|
||||||
return data.map((item) => ({
|
|
||||||
label: item.name,
|
|
||||||
value: item.id,
|
|
||||||
}));
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{/* ... Address fields ... */}
|
|
||||||
<ProFormText
|
|
||||||
label="公司名称"
|
|
||||||
name={['details', 'origin', 'name']}
|
|
||||||
rules={[{ required: true, message: '请输入公司名称' }]}
|
|
||||||
/>
|
|
||||||
{/* Simplified for brevity - assume standard fields remain */}
|
|
||||||
</ProForm.Group>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
{/* ... Packaging fields ... */}
|
|
||||||
</ModalForm>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const SalesChange: React.FC<any> = () => null; // Disable for now
|
|
||||||
|
|
||||||
export const CreateOrder: React.FC<{
|
|
||||||
tableRef?: React.MutableRefObject<ActionType | undefined>;
|
|
||||||
siteId?: string;
|
|
||||||
}> = ({ tableRef, siteId }) => {
|
|
||||||
const formRef = useRef<ProFormInstance>();
|
|
||||||
const { message } = App.useApp();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ModalForm
|
|
||||||
formRef={formRef}
|
|
||||||
title="创建订单"
|
|
||||||
size="large"
|
|
||||||
width="80vw"
|
|
||||||
modalProps={{
|
|
||||||
destroyOnHidden: true,
|
|
||||||
styles: {
|
|
||||||
body: { maxHeight: '65vh', overflowY: 'auto', overflowX: 'hidden' },
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
trigger={
|
|
||||||
<Button type="primary" icon={<PlusOutlined />}>
|
|
||||||
创建订单
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
params={{
|
|
||||||
source_type: 'admin',
|
|
||||||
}}
|
|
||||||
onFinish={async ({ items, details, ...data }) => {
|
|
||||||
if (!siteId) {
|
|
||||||
message.error('缺少站点ID');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
// Use site-api to create order
|
|
||||||
const { success, message: errMsg } = await request(
|
|
||||||
`/site-api/${siteId}/orders`,
|
|
||||||
{
|
|
||||||
method: 'POST',
|
|
||||||
data: {
|
|
||||||
...data,
|
|
||||||
customer_email: data?.billing?.email,
|
|
||||||
billing_phone: data?.billing?.phone,
|
|
||||||
// map other fields if needed for Adapter
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
if (success === false) throw new Error(errMsg); // Check success
|
|
||||||
|
|
||||||
message.success('创建成功');
|
|
||||||
tableRef?.current?.reload();
|
|
||||||
|
|
||||||
return true;
|
|
||||||
} catch (error: any) {
|
|
||||||
message.error(error?.message || '创建失败');
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onFinishFailed={() => {
|
|
||||||
const element = document.querySelector('.ant-form-item-explain-error');
|
|
||||||
if (element) {
|
|
||||||
element.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{/* ... Form fields ... same as before */}
|
|
||||||
<ProFormDigit
|
|
||||||
label="金额"
|
|
||||||
name="total"
|
|
||||||
rules={[{ required: true, message: '请输入金额' }]}
|
|
||||||
/>
|
|
||||||
{/* ... */}
|
|
||||||
</ModalForm>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const BatchEditOrders: React.FC<{
|
|
||||||
tableRef: React.MutableRefObject<ActionType | undefined>;
|
|
||||||
selectedRowKeys: React.Key[];
|
|
||||||
setSelectedRowKeys: (keys: React.Key[]) => void;
|
|
||||||
siteId?: string;
|
|
||||||
}> = ({ tableRef, selectedRowKeys, setSelectedRowKeys, siteId }) => {
|
|
||||||
const { message } = App.useApp();
|
|
||||||
return (
|
|
||||||
<ModalForm
|
|
||||||
title="批量编辑订单"
|
|
||||||
trigger={
|
|
||||||
<Button
|
|
||||||
disabled={!selectedRowKeys.length}
|
|
||||||
type="primary"
|
|
||||||
icon={<EditOutlined />}
|
|
||||||
>
|
|
||||||
批量编辑
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
width={400}
|
|
||||||
modalProps={{ destroyOnHidden: true }}
|
|
||||||
onFinish={async (values) => {
|
|
||||||
if (!siteId) return false;
|
|
||||||
let ok = 0,
|
|
||||||
fail = 0;
|
|
||||||
for (const id of selectedRowKeys) {
|
|
||||||
try {
|
|
||||||
// Remove undefined values
|
|
||||||
const data = Object.fromEntries(
|
|
||||||
Object.entries(values).filter(
|
|
||||||
([_, v]) => v !== undefined && v !== '',
|
|
||||||
),
|
|
||||||
);
|
|
||||||
if (Object.keys(data).length === 0) continue;
|
|
||||||
|
|
||||||
const res = await request(`/site-api/${siteId}/orders/${id}`, {
|
|
||||||
method: 'PUT',
|
|
||||||
data: data,
|
|
||||||
});
|
|
||||||
if (res.success) ok++;
|
|
||||||
else fail++;
|
|
||||||
} catch (e) {
|
|
||||||
fail++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
message.success(`成功 ${ok}, 失败 ${fail}`);
|
|
||||||
tableRef.current?.reload();
|
|
||||||
setSelectedRowKeys([]);
|
|
||||||
return true;
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ProFormSelect
|
|
||||||
name="status"
|
|
||||||
label="状态"
|
|
||||||
valueEnum={ORDER_STATUS_ENUM}
|
|
||||||
placeholder="不修改请留空"
|
|
||||||
/>
|
|
||||||
</ModalForm>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const EditOrder: React.FC<{
|
|
||||||
tableRef: React.MutableRefObject<ActionType | undefined>;
|
|
||||||
orderId: number;
|
|
||||||
record: API.Order;
|
|
||||||
setActiveLine: Function;
|
|
||||||
siteId?: string;
|
|
||||||
}> = ({ tableRef, orderId, record, setActiveLine, siteId }) => {
|
|
||||||
const { message } = App.useApp();
|
|
||||||
const formRef = useRef<ProFormInstance>();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DrawerForm
|
|
||||||
formRef={formRef}
|
|
||||||
title="编辑订单"
|
|
||||||
trigger={
|
|
||||||
<Button
|
|
||||||
type="primary"
|
|
||||||
size="small"
|
|
||||||
icon={<EditOutlined />}
|
|
||||||
onClick={() => setActiveLine(record.id)}
|
|
||||||
>
|
|
||||||
编辑
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
drawerProps={{
|
|
||||||
destroyOnHidden: true,
|
|
||||||
width: '60vw',
|
|
||||||
}}
|
|
||||||
request={async () => {
|
|
||||||
if (!siteId) return {};
|
|
||||||
const { data, success } = await request(
|
|
||||||
`/site-api/${siteId}/orders/${orderId}`,
|
|
||||||
);
|
|
||||||
if (!success || !data) return {};
|
|
||||||
|
|
||||||
const sales = data.sales || [];
|
|
||||||
const mergedSales = sales.reduce((acc: any[], cur: any) => {
|
|
||||||
let idx = acc.findIndex((v: any) => v.productId === cur.productId);
|
|
||||||
if (idx === -1) {
|
|
||||||
acc.push(cur);
|
|
||||||
} else {
|
|
||||||
acc[idx].quantity += cur.quantity;
|
|
||||||
}
|
|
||||||
return acc;
|
|
||||||
}, []);
|
|
||||||
data.sales = mergedSales;
|
|
||||||
|
|
||||||
return data;
|
|
||||||
}}
|
|
||||||
onFinish={async (values) => {
|
|
||||||
if (!siteId) return false;
|
|
||||||
try {
|
|
||||||
const res = await request(`/site-api/${siteId}/orders/${orderId}`, {
|
|
||||||
method: 'PUT',
|
|
||||||
data: values,
|
|
||||||
});
|
|
||||||
if (res.success) {
|
|
||||||
message.success('更新成功');
|
|
||||||
tableRef.current?.reload();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
message.error(res.message || '更新失败');
|
|
||||||
return false;
|
|
||||||
} catch (e: any) {
|
|
||||||
message.error(e.message || '更新失败');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ProForm.Group title="基本信息">
|
|
||||||
<ProFormText name="number" label="订单号" readonly />
|
|
||||||
<ProFormSelect
|
|
||||||
name="status"
|
|
||||||
label="状态"
|
|
||||||
valueEnum={ORDER_STATUS_ENUM}
|
|
||||||
/>
|
|
||||||
<ProFormText name="currency" label="币种" readonly />
|
|
||||||
<ProFormText name="payment_method" label="支付方式" readonly />
|
|
||||||
<ProFormText name="transaction_id" label="交易ID" readonly />
|
|
||||||
<ProFormDatePicker
|
|
||||||
name="date_created"
|
|
||||||
label="创建时间"
|
|
||||||
readonly
|
|
||||||
fieldProps={{ style: { width: '100%' } }}
|
|
||||||
/>
|
|
||||||
</ProForm.Group>
|
|
||||||
|
|
||||||
<Divider />
|
|
||||||
|
|
||||||
<ProForm.Group title="账单地址">
|
|
||||||
<ProFormText name={['billing', 'first_name']} label="名" />
|
|
||||||
<ProFormText name={['billing', 'last_name']} label="姓" />
|
|
||||||
<ProFormText name={['billing', 'company']} label="公司" />
|
|
||||||
<ProFormText name={['billing', 'address_1']} label="地址1" />
|
|
||||||
<ProFormText name={['billing', 'address_2']} label="地址2" />
|
|
||||||
<ProFormText name={['billing', 'city']} label="城市" />
|
|
||||||
<ProFormText name={['billing', 'state']} label="省/州" />
|
|
||||||
<ProFormText name={['billing', 'postcode']} label="邮编" />
|
|
||||||
<ProFormText name={['billing', 'country']} label="国家" />
|
|
||||||
<ProFormText name={['billing', 'email']} label="邮箱" />
|
|
||||||
<ProFormText name={['billing', 'phone']} label="电话" />
|
|
||||||
</ProForm.Group>
|
|
||||||
|
|
||||||
<Divider />
|
|
||||||
|
|
||||||
<ProForm.Group title="收货地址">
|
|
||||||
<ProFormText name={['shipping', 'first_name']} label="名" />
|
|
||||||
<ProFormText name={['shipping', 'last_name']} label="姓" />
|
|
||||||
<ProFormText name={['shipping', 'company']} label="公司" />
|
|
||||||
<ProFormText name={['shipping', 'address_1']} label="地址1" />
|
|
||||||
<ProFormText name={['shipping', 'address_2']} label="地址2" />
|
|
||||||
<ProFormText name={['shipping', 'city']} label="城市" />
|
|
||||||
<ProFormText name={['shipping', 'state']} label="省/州" />
|
|
||||||
<ProFormText name={['shipping', 'postcode']} label="邮编" />
|
|
||||||
<ProFormText name={['shipping', 'country']} label="国家" />
|
|
||||||
<ProFormText name={['shipping', 'phone']} label="电话" />
|
|
||||||
</ProForm.Group>
|
|
||||||
|
|
||||||
<Divider />
|
|
||||||
|
|
||||||
<ProFormTextArea name="customer_note" label="客户备注" />
|
|
||||||
|
|
||||||
<Divider />
|
|
||||||
|
|
||||||
<ProFormList
|
|
||||||
name="sales"
|
|
||||||
label="商品列表"
|
|
||||||
readonly
|
|
||||||
actionRender={() => []}
|
|
||||||
>
|
|
||||||
<ProForm.Group>
|
|
||||||
<ProFormText name="name" label="商品名" />
|
|
||||||
<ProFormText name="sku" label="SKU" />
|
|
||||||
<ProFormDigit name="quantity" label="数量" />
|
|
||||||
<ProFormText name="total" label="总价" />
|
|
||||||
</ProForm.Group>
|
|
||||||
</ProFormList>
|
|
||||||
|
|
||||||
<ProFormText name="total" label="订单总额" readonly />
|
|
||||||
</DrawerForm>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -1,177 +0,0 @@
|
||||||
import { productcontrollerSearchproducts } from '@/servers/api/product';
|
|
||||||
import { ModalForm, ProTable } from '@ant-design/pro-components';
|
|
||||||
import { Form, message } from 'antd';
|
|
||||||
import React, { useState } from 'react';
|
|
||||||
|
|
||||||
interface ErpProductBindModalProps {
|
|
||||||
trigger: React.ReactNode;
|
|
||||||
siteProduct: any;
|
|
||||||
siteId: string;
|
|
||||||
onBindSuccess?: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ErpProductBindModal: React.FC<ErpProductBindModalProps> = ({
|
|
||||||
trigger,
|
|
||||||
siteProduct,
|
|
||||||
siteId,
|
|
||||||
onBindSuccess,
|
|
||||||
}) => {
|
|
||||||
const [form] = Form.useForm();
|
|
||||||
const [selectedProduct, setSelectedProduct] = useState<any>(null);
|
|
||||||
|
|
||||||
const handleBind = async (values: any) => {
|
|
||||||
if (!selectedProduct) {
|
|
||||||
message.error('请选择一个ERP产品');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 调用绑定API
|
|
||||||
const response = await fetch(
|
|
||||||
`/api/site-api/${siteId}/products/${siteProduct.id}/bind-erp`,
|
|
||||||
{
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
erpProductId: selectedProduct.id,
|
|
||||||
siteSku: siteProduct.sku,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const result = await response.json();
|
|
||||||
|
|
||||||
if (result.success) {
|
|
||||||
message.success('ERP产品绑定成功');
|
|
||||||
onBindSuccess?.();
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
message.error(result.message || '绑定失败');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
message.error('绑定请求失败');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ModalForm
|
|
||||||
title="绑定ERP产品"
|
|
||||||
trigger={trigger}
|
|
||||||
form={form}
|
|
||||||
modalProps={{
|
|
||||||
destroyOnClose: true,
|
|
||||||
width: 800,
|
|
||||||
}}
|
|
||||||
onFinish={handleBind}
|
|
||||||
>
|
|
||||||
<div style={{ marginBottom: 16 }}>
|
|
||||||
<strong>站点产品信息:</strong>
|
|
||||||
<div>SKU: {siteProduct.sku}</div>
|
|
||||||
<div>名称: {siteProduct.name}</div>
|
|
||||||
{siteProduct.erpProduct && (
|
|
||||||
<div style={{ color: '#ff4d4f' }}>
|
|
||||||
⚠️ 当前已绑定ERP产品:{siteProduct.erpProduct.sku} -{' '}
|
|
||||||
{siteProduct.erpProduct.name}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ProTable
|
|
||||||
rowKey="id"
|
|
||||||
search={{
|
|
||||||
labelWidth: 'auto',
|
|
||||||
}}
|
|
||||||
request={async (params) => {
|
|
||||||
const response = await productcontrollerSearchproducts({
|
|
||||||
keyword: params.keyword,
|
|
||||||
page: params.current,
|
|
||||||
per_page: params.pageSize,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.success) {
|
|
||||||
return {
|
|
||||||
data: response.data.items,
|
|
||||||
total: response.data.total,
|
|
||||||
success: true,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
data: [],
|
|
||||||
total: 0,
|
|
||||||
success: false,
|
|
||||||
};
|
|
||||||
}}
|
|
||||||
columns={[
|
|
||||||
{
|
|
||||||
title: 'ID',
|
|
||||||
dataIndex: 'id',
|
|
||||||
width: 80,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'SKU',
|
|
||||||
dataIndex: 'sku',
|
|
||||||
copyable: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '产品名称',
|
|
||||||
dataIndex: 'name',
|
|
||||||
ellipsis: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '中文名称',
|
|
||||||
dataIndex: 'nameCn',
|
|
||||||
ellipsis: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '分类',
|
|
||||||
dataIndex: ['category', 'name'],
|
|
||||||
ellipsis: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '价格',
|
|
||||||
dataIndex: 'price',
|
|
||||||
width: 100,
|
|
||||||
render: (text) => `¥${text}`,
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
rowSelection={{
|
|
||||||
type: 'radio',
|
|
||||||
onChange: (_, selectedRows) => {
|
|
||||||
setSelectedProduct(selectedRows[0]);
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
pagination={{
|
|
||||||
pageSize: 10,
|
|
||||||
showSizeChanger: true,
|
|
||||||
showQuickJumper: true,
|
|
||||||
}}
|
|
||||||
toolBarRender={false}
|
|
||||||
options={false}
|
|
||||||
scroll={{ y: 400 }}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{selectedProduct && (
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
marginTop: 16,
|
|
||||||
padding: 12,
|
|
||||||
backgroundColor: '#f6ffed',
|
|
||||||
border: '1px solid #b7eb8f',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<strong>已选择:</strong>
|
|
||||||
<div>SKU: {selectedProduct.sku}</div>
|
|
||||||
<div>名称: {selectedProduct.name}</div>
|
|
||||||
{selectedProduct.nameCn && (
|
|
||||||
<div>中文名: {selectedProduct.nameCn}</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</ModalForm>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -1,799 +0,0 @@
|
||||||
import { PRODUCT_STATUS_ENUM, PRODUCT_STOCK_STATUS_ENUM } from '@/constants';
|
|
||||||
import { DeleteFilled, EditOutlined, PlusOutlined } from '@ant-design/icons';
|
|
||||||
import {
|
|
||||||
ActionType,
|
|
||||||
DrawerForm,
|
|
||||||
ModalForm,
|
|
||||||
ProForm,
|
|
||||||
ProFormDigit,
|
|
||||||
ProFormList,
|
|
||||||
ProFormSelect,
|
|
||||||
ProFormText,
|
|
||||||
ProFormTextArea,
|
|
||||||
} from '@ant-design/pro-components';
|
|
||||||
import { request } from '@umijs/max';
|
|
||||||
import { App, Button, Divider, Form } from 'antd';
|
|
||||||
import React from 'react';
|
|
||||||
import { TagConfig, computeTags } from './utils';
|
|
||||||
|
|
||||||
export const CreateProduct: React.FC<{
|
|
||||||
tableRef: React.MutableRefObject<ActionType | undefined>;
|
|
||||||
siteId?: string;
|
|
||||||
}> = ({ tableRef, siteId }) => {
|
|
||||||
const { message } = App.useApp();
|
|
||||||
const [form] = Form.useForm();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DrawerForm
|
|
||||||
title="新增产品"
|
|
||||||
form={form}
|
|
||||||
trigger={
|
|
||||||
<Button type="primary" title="新增产品" icon={<PlusOutlined />}>
|
|
||||||
新增产品
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
autoFocusFirstInput
|
|
||||||
drawerProps={{
|
|
||||||
destroyOnHidden: true,
|
|
||||||
}}
|
|
||||||
onFinish={async (values) => {
|
|
||||||
if (!siteId) {
|
|
||||||
message.error('缺少站点ID');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
// 将数字字段转换为字符串以匹配DTO
|
|
||||||
const productData = {
|
|
||||||
...values,
|
|
||||||
type: values.type || 'simple',
|
|
||||||
regular_price: values.regular_price?.toString() || '',
|
|
||||||
sale_price: values.sale_price?.toString() || '',
|
|
||||||
price:
|
|
||||||
values.sale_price?.toString() ||
|
|
||||||
values.regular_price?.toString() ||
|
|
||||||
'',
|
|
||||||
};
|
|
||||||
|
|
||||||
await request(`/site-api/${siteId}/products`, {
|
|
||||||
method: 'POST',
|
|
||||||
data: productData,
|
|
||||||
});
|
|
||||||
|
|
||||||
message.success('创建成功');
|
|
||||||
tableRef.current?.reload();
|
|
||||||
return true;
|
|
||||||
} catch (error: any) {
|
|
||||||
message.error(error.message || '创建失败');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ProForm.Group>
|
|
||||||
<ProFormText
|
|
||||||
name="name"
|
|
||||||
label="产品名称"
|
|
||||||
width="lg"
|
|
||||||
rules={[{ required: true, message: '请输入产品名称' }]}
|
|
||||||
/>
|
|
||||||
<ProFormSelect
|
|
||||||
name="type"
|
|
||||||
label="产品类型"
|
|
||||||
width="md"
|
|
||||||
valueEnum={{ simple: '简单产品', variable: '可变产品' }}
|
|
||||||
initialValue="simple"
|
|
||||||
/>
|
|
||||||
<ProFormText
|
|
||||||
name="sku"
|
|
||||||
label="SKU"
|
|
||||||
width="lg"
|
|
||||||
rules={[{ required: true, message: '请输入SKU' }]}
|
|
||||||
/>
|
|
||||||
<ProFormTextArea name="description" label="描述" width="lg" />
|
|
||||||
<ProFormTextArea name="short_description" label="简短描述" width="lg" />
|
|
||||||
<ProFormDigit
|
|
||||||
name="regular_price"
|
|
||||||
label="常规价格"
|
|
||||||
width="md"
|
|
||||||
fieldProps={{ precision: 2 }}
|
|
||||||
/>
|
|
||||||
<ProFormDigit
|
|
||||||
name="sale_price"
|
|
||||||
label="促销价格"
|
|
||||||
width="md"
|
|
||||||
fieldProps={{ precision: 2 }}
|
|
||||||
/>
|
|
||||||
<ProFormDigit
|
|
||||||
name="stock_quantity"
|
|
||||||
label="库存数量"
|
|
||||||
width="md"
|
|
||||||
fieldProps={{ precision: 0 }}
|
|
||||||
/>
|
|
||||||
<ProFormSelect
|
|
||||||
name="status"
|
|
||||||
label="产品状态"
|
|
||||||
width="md"
|
|
||||||
valueEnum={PRODUCT_STATUS_ENUM}
|
|
||||||
initialValue="publish"
|
|
||||||
/>
|
|
||||||
<ProFormSelect
|
|
||||||
name="stock_status"
|
|
||||||
label="库存状态"
|
|
||||||
width="md"
|
|
||||||
valueEnum={PRODUCT_STOCK_STATUS_ENUM}
|
|
||||||
/>
|
|
||||||
</ProForm.Group>
|
|
||||||
<Divider />
|
|
||||||
<ProFormList
|
|
||||||
name="images"
|
|
||||||
label="产品图片"
|
|
||||||
initialValue={[{}]}
|
|
||||||
creatorButtonProps={{
|
|
||||||
creatorButtonText: '添加图片',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ProForm.Group>
|
|
||||||
<ProFormText name="src" label="图片URL" width="lg" />
|
|
||||||
<ProFormText name="alt" label="替代文本" width="md" />
|
|
||||||
</ProForm.Group>
|
|
||||||
</ProFormList>
|
|
||||||
<Divider />
|
|
||||||
<ProFormList
|
|
||||||
name="attributes"
|
|
||||||
label="产品属性"
|
|
||||||
initialValue={[]}
|
|
||||||
creatorButtonProps={{
|
|
||||||
creatorButtonText: '添加属性',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ProForm.Group>
|
|
||||||
<ProFormText name="name" label="属性名称" width="md" />
|
|
||||||
<ProFormSelect
|
|
||||||
name="options"
|
|
||||||
label="选项"
|
|
||||||
width="md"
|
|
||||||
mode="tags"
|
|
||||||
placeholder="输入选项并回车"
|
|
||||||
/>
|
|
||||||
<ProFormSelect
|
|
||||||
name="visible"
|
|
||||||
label="可见性"
|
|
||||||
width="xs"
|
|
||||||
options={[
|
|
||||||
{ label: '可见', value: true },
|
|
||||||
{ label: '隐藏', value: false },
|
|
||||||
]}
|
|
||||||
initialValue={true}
|
|
||||||
/>
|
|
||||||
<ProFormSelect
|
|
||||||
name="variation"
|
|
||||||
label="用于变体"
|
|
||||||
width="xs"
|
|
||||||
options={[
|
|
||||||
{ label: '是', value: true },
|
|
||||||
{ label: '否', value: false },
|
|
||||||
]}
|
|
||||||
initialValue={false}
|
|
||||||
/>
|
|
||||||
</ProForm.Group>
|
|
||||||
</ProFormList>
|
|
||||||
</DrawerForm>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const UpdateStatus: React.FC<{
|
|
||||||
tableRef: React.MutableRefObject<ActionType | undefined>;
|
|
||||||
values: any;
|
|
||||||
siteId?: string;
|
|
||||||
}> = ({ tableRef, values: initialValues, siteId }) => {
|
|
||||||
const { message } = App.useApp();
|
|
||||||
|
|
||||||
// 转换初始值,将字符串价格转换为数字以便编辑
|
|
||||||
const formValues = {
|
|
||||||
...initialValues,
|
|
||||||
stock_quantity: initialValues.stock_quantity
|
|
||||||
? parseInt(initialValues.stock_quantity)
|
|
||||||
: 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DrawerForm<{
|
|
||||||
status: any;
|
|
||||||
stock_status: any;
|
|
||||||
stock_quantity: number;
|
|
||||||
}>
|
|
||||||
title="修改产品状态"
|
|
||||||
initialValues={formValues}
|
|
||||||
trigger={
|
|
||||||
<Button type="link" title="修改状态" icon={<EditOutlined />}>
|
|
||||||
修改状态
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
autoFocusFirstInput
|
|
||||||
drawerProps={{
|
|
||||||
destroyOnHidden: true,
|
|
||||||
}}
|
|
||||||
onFinish={async (values) => {
|
|
||||||
if (!siteId) {
|
|
||||||
message.error('缺少站点ID');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
await request(`/site-api/${siteId}/products/${initialValues.id}`, {
|
|
||||||
method: 'PUT',
|
|
||||||
data: {
|
|
||||||
status: values.status,
|
|
||||||
stock_status: values.stock_status,
|
|
||||||
stock_quantity: values.stock_quantity,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
message.success('状态更新成功');
|
|
||||||
tableRef.current?.reload();
|
|
||||||
return true;
|
|
||||||
} catch (error: any) {
|
|
||||||
message.error(error.message || '状态更新失败');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ProForm.Group>
|
|
||||||
<ProFormSelect
|
|
||||||
label="产品状态"
|
|
||||||
width="lg"
|
|
||||||
name="status"
|
|
||||||
valueEnum={PRODUCT_STATUS_ENUM}
|
|
||||||
/>
|
|
||||||
<ProFormSelect
|
|
||||||
label="库存状态"
|
|
||||||
width="lg"
|
|
||||||
name="stock_status"
|
|
||||||
valueEnum={PRODUCT_STOCK_STATUS_ENUM}
|
|
||||||
/>
|
|
||||||
<ProFormDigit
|
|
||||||
name="stock_quantity"
|
|
||||||
label="库存数量"
|
|
||||||
width="lg"
|
|
||||||
fieldProps={{ precision: 0 }}
|
|
||||||
/>
|
|
||||||
</ProForm.Group>
|
|
||||||
</DrawerForm>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const UpdateForm: React.FC<{
|
|
||||||
tableRef: React.MutableRefObject<ActionType | undefined>;
|
|
||||||
values: any;
|
|
||||||
config?: TagConfig;
|
|
||||||
siteId?: string;
|
|
||||||
}> = ({ tableRef, values: initialValues, config, siteId }) => {
|
|
||||||
const { message } = App.useApp();
|
|
||||||
const [form] = Form.useForm();
|
|
||||||
|
|
||||||
// 转换初始值,将字符串价格转换为数字以便编辑
|
|
||||||
const formValues = {
|
|
||||||
...initialValues,
|
|
||||||
categories: initialValues.categories?.map((c: any) => c.name) || [],
|
|
||||||
tags: initialValues.tags?.map((t: any) => t.name) || [],
|
|
||||||
regular_price: initialValues.regular_price
|
|
||||||
? parseFloat(initialValues.regular_price)
|
|
||||||
: 0,
|
|
||||||
sale_price: initialValues.sale_price
|
|
||||||
? parseFloat(initialValues.sale_price)
|
|
||||||
: 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleAutoGenerateTags = () => {
|
|
||||||
if (!config) {
|
|
||||||
message.warning('正在获取标签配置,请稍后再试');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const sku = initialValues.sku || '';
|
|
||||||
const name = initialValues.name || '';
|
|
||||||
|
|
||||||
const generatedTagsString = computeTags(name, sku, config);
|
|
||||||
const generatedTags = generatedTagsString.split(', ').filter((t) => t);
|
|
||||||
|
|
||||||
if (generatedTags.length > 0) {
|
|
||||||
const currentTags = form.getFieldValue('tags') || [];
|
|
||||||
const newTags = [...new Set([...currentTags, ...generatedTags])];
|
|
||||||
form.setFieldsValue({ tags: newTags });
|
|
||||||
message.success(`已自动生成 ${generatedTags.length} 个标签`);
|
|
||||||
} else {
|
|
||||||
message.info('未能根据名称和SKU自动生成标签');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DrawerForm
|
|
||||||
title="编辑产品"
|
|
||||||
form={form}
|
|
||||||
initialValues={formValues}
|
|
||||||
trigger={
|
|
||||||
<Button type="link" title="编辑详情" icon={<EditOutlined />}>
|
|
||||||
编辑详情
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
autoFocusFirstInput
|
|
||||||
drawerProps={{
|
|
||||||
destroyOnHidden: true,
|
|
||||||
}}
|
|
||||||
onFinish={async (values) => {
|
|
||||||
if (!siteId) {
|
|
||||||
message.error('缺少站点ID');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
// 将数字字段转换为字符串以匹配DTO
|
|
||||||
const updateData = {
|
|
||||||
...values,
|
|
||||||
regular_price: values.regular_price?.toString() || '',
|
|
||||||
sale_price: values.sale_price?.toString() || '',
|
|
||||||
price:
|
|
||||||
values.sale_price?.toString() ||
|
|
||||||
values.regular_price?.toString() ||
|
|
||||||
'',
|
|
||||||
};
|
|
||||||
|
|
||||||
await request(`/site-api/${siteId}/products/${initialValues.id}`, {
|
|
||||||
method: 'PUT',
|
|
||||||
data: updateData,
|
|
||||||
});
|
|
||||||
message.success('提交成功');
|
|
||||||
tableRef.current?.reload();
|
|
||||||
return true;
|
|
||||||
} catch (error: any) {
|
|
||||||
message.error(error.message);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ProForm.Group>
|
|
||||||
<ProFormText
|
|
||||||
label="产品名称"
|
|
||||||
width="lg"
|
|
||||||
name="name"
|
|
||||||
rules={[{ required: true, message: '请输入产品名称' }]}
|
|
||||||
/>
|
|
||||||
<ProFormText
|
|
||||||
name="sku"
|
|
||||||
width="lg"
|
|
||||||
label="SKU"
|
|
||||||
tooltip="示例: TO-ZY-06MG-WG-S-0001"
|
|
||||||
placeholder="请输入SKU"
|
|
||||||
rules={[{ required: true, message: '请输入SKU' }]}
|
|
||||||
/>
|
|
||||||
<ProFormTextArea name="short_description" label="简短描述" width="lg" />
|
|
||||||
<ProFormTextArea name="description" label="描述" width="lg" />
|
|
||||||
|
|
||||||
{initialValues.type === 'simple' ? (
|
|
||||||
<>
|
|
||||||
<ProFormDigit
|
|
||||||
name="regular_price"
|
|
||||||
width="md"
|
|
||||||
label="常规价格"
|
|
||||||
fieldProps={{
|
|
||||||
precision: 2,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<ProFormDigit
|
|
||||||
name="sale_price"
|
|
||||||
width="md"
|
|
||||||
label="促销价格"
|
|
||||||
fieldProps={{
|
|
||||||
precision: 2,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<ProFormDigit
|
|
||||||
name="stock_quantity"
|
|
||||||
width="md"
|
|
||||||
label="库存数量"
|
|
||||||
fieldProps={{ precision: 0 }}
|
|
||||||
/>
|
|
||||||
<ProFormSelect
|
|
||||||
name="stock_status"
|
|
||||||
label="库存状态"
|
|
||||||
width="md"
|
|
||||||
valueEnum={PRODUCT_STOCK_STATUS_ENUM}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<></>
|
|
||||||
)}
|
|
||||||
<ProFormSelect
|
|
||||||
name="status"
|
|
||||||
label="产品状态"
|
|
||||||
width="md"
|
|
||||||
valueEnum={PRODUCT_STATUS_ENUM}
|
|
||||||
/>
|
|
||||||
<ProFormSelect
|
|
||||||
name="categories"
|
|
||||||
label="分类"
|
|
||||||
mode="tags"
|
|
||||||
width="lg"
|
|
||||||
placeholder="请输入分类,按回车确认"
|
|
||||||
/>
|
|
||||||
<ProForm.Group>
|
|
||||||
<ProFormSelect
|
|
||||||
name="tags"
|
|
||||||
label="标签"
|
|
||||||
mode="tags"
|
|
||||||
width="md"
|
|
||||||
placeholder="请输入标签,按回车确认"
|
|
||||||
/>
|
|
||||||
<Button onClick={handleAutoGenerateTags} style={{ marginTop: 30 }}>
|
|
||||||
自动生成
|
|
||||||
</Button>
|
|
||||||
</ProForm.Group>
|
|
||||||
</ProForm.Group>
|
|
||||||
<Divider />
|
|
||||||
<ProFormList
|
|
||||||
name="images"
|
|
||||||
label="产品图片"
|
|
||||||
initialValue={initialValues.images || [{}]}
|
|
||||||
creatorButtonProps={{
|
|
||||||
creatorButtonText: '添加图片',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ProForm.Group>
|
|
||||||
<ProFormText name="src" label="图片URL" width="lg" />
|
|
||||||
<ProFormText name="alt" label="替代文本" width="md" />
|
|
||||||
</ProForm.Group>
|
|
||||||
</ProFormList>
|
|
||||||
<Divider />
|
|
||||||
<ProFormList
|
|
||||||
name="attributes"
|
|
||||||
label="产品属性"
|
|
||||||
initialValue={initialValues.attributes || []}
|
|
||||||
creatorButtonProps={{
|
|
||||||
creatorButtonText: '添加属性',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ProForm.Group>
|
|
||||||
<ProFormText name="name" label="属性名称" width="md" />
|
|
||||||
<ProFormSelect
|
|
||||||
name="options"
|
|
||||||
label="选项"
|
|
||||||
width="md"
|
|
||||||
mode="tags"
|
|
||||||
placeholder="输入选项并回车"
|
|
||||||
/>
|
|
||||||
<ProFormSelect
|
|
||||||
name="visible"
|
|
||||||
label="可见性"
|
|
||||||
width="xs"
|
|
||||||
options={[
|
|
||||||
{ label: '可见', value: true },
|
|
||||||
{ label: '隐藏', value: false },
|
|
||||||
]}
|
|
||||||
initialValue={true}
|
|
||||||
/>
|
|
||||||
<ProFormSelect
|
|
||||||
name="variation"
|
|
||||||
label="用于变体"
|
|
||||||
width="xs"
|
|
||||||
options={[
|
|
||||||
{ label: '是', value: true },
|
|
||||||
{ label: '否', value: false },
|
|
||||||
]}
|
|
||||||
initialValue={false}
|
|
||||||
/>
|
|
||||||
</ProForm.Group>
|
|
||||||
</ProFormList>
|
|
||||||
</DrawerForm>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const UpdateVaritation: React.FC<{
|
|
||||||
tableRef: React.MutableRefObject<ActionType | undefined>;
|
|
||||||
values: any;
|
|
||||||
siteId?: string;
|
|
||||||
productId?: string | number;
|
|
||||||
}> = ({
|
|
||||||
tableRef,
|
|
||||||
values: initialValues,
|
|
||||||
siteId,
|
|
||||||
productId: propProductId,
|
|
||||||
}) => {
|
|
||||||
const { message } = App.useApp();
|
|
||||||
|
|
||||||
// 转换初始值,将字符串价格转换为数字以便编辑
|
|
||||||
const formValues = {
|
|
||||||
...initialValues,
|
|
||||||
regular_price: initialValues.regular_price
|
|
||||||
? parseFloat(initialValues.regular_price)
|
|
||||||
: 0,
|
|
||||||
sale_price: initialValues.sale_price
|
|
||||||
? parseFloat(initialValues.sale_price)
|
|
||||||
: 0,
|
|
||||||
stock_quantity: initialValues.stock_quantity
|
|
||||||
? parseInt(initialValues.stock_quantity)
|
|
||||||
: 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DrawerForm
|
|
||||||
title="编辑变体"
|
|
||||||
initialValues={formValues}
|
|
||||||
trigger={
|
|
||||||
<Button type="link" title="编辑变体" icon={<EditOutlined />}>
|
|
||||||
编辑变体
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
autoFocusFirstInput
|
|
||||||
drawerProps={{
|
|
||||||
destroyOnHidden: true,
|
|
||||||
}}
|
|
||||||
onFinish={async (values) => {
|
|
||||||
const productId =
|
|
||||||
propProductId ||
|
|
||||||
initialValues.externalProductId ||
|
|
||||||
initialValues.parent_id ||
|
|
||||||
initialValues.product_id;
|
|
||||||
|
|
||||||
if (!siteId || !productId) {
|
|
||||||
message.error('缺少站点ID或产品ID');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 将数字字段转换为字符串以匹配DTO
|
|
||||||
const variationData = {
|
|
||||||
...values,
|
|
||||||
regular_price: values.regular_price?.toString() || '',
|
|
||||||
sale_price: values.sale_price?.toString() || '',
|
|
||||||
price:
|
|
||||||
values.sale_price?.toString() ||
|
|
||||||
values.regular_price?.toString() ||
|
|
||||||
'',
|
|
||||||
};
|
|
||||||
|
|
||||||
const variationId =
|
|
||||||
initialValues.externalVariationId || initialValues.id;
|
|
||||||
await request(
|
|
||||||
`/site-api/${siteId}/products/${productId}/variations/${variationId}`,
|
|
||||||
{
|
|
||||||
method: 'PUT',
|
|
||||||
data: variationData,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
message.success('更新变体成功');
|
|
||||||
tableRef.current?.reload();
|
|
||||||
return true;
|
|
||||||
} catch (error: any) {
|
|
||||||
message.error(error.message || '更新失败');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ProForm.Group>
|
|
||||||
<ProFormText label="变体名称" width="lg" name="name" />
|
|
||||||
<ProFormText
|
|
||||||
name="sku"
|
|
||||||
width="lg"
|
|
||||||
label="SKU"
|
|
||||||
tooltip="示例: TO-ZY-06MG-WG-S-0001"
|
|
||||||
placeholder="请输入SKU"
|
|
||||||
rules={[{ required: true, message: '请输入SKU' }]}
|
|
||||||
/>
|
|
||||||
<ProFormDigit
|
|
||||||
name="regular_price"
|
|
||||||
width="lg"
|
|
||||||
label="常规价格"
|
|
||||||
fieldProps={{
|
|
||||||
precision: 2,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<ProFormDigit
|
|
||||||
name="sale_price"
|
|
||||||
width="lg"
|
|
||||||
label="促销价格"
|
|
||||||
fieldProps={{
|
|
||||||
precision: 2,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<ProFormDigit
|
|
||||||
name="stock_quantity"
|
|
||||||
width="lg"
|
|
||||||
label="库存数量"
|
|
||||||
fieldProps={{ precision: 0 }}
|
|
||||||
/>
|
|
||||||
<ProFormSelect
|
|
||||||
name="stock_status"
|
|
||||||
label="库存状态"
|
|
||||||
width="lg"
|
|
||||||
valueEnum={PRODUCT_STOCK_STATUS_ENUM}
|
|
||||||
/>
|
|
||||||
<ProFormSelect
|
|
||||||
name="status"
|
|
||||||
label="产品状态"
|
|
||||||
width="lg"
|
|
||||||
valueEnum={PRODUCT_STATUS_ENUM}
|
|
||||||
/>
|
|
||||||
</ProForm.Group>
|
|
||||||
</DrawerForm>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const BatchDeleteProducts: React.FC<{
|
|
||||||
tableRef: React.MutableRefObject<ActionType | undefined>;
|
|
||||||
selectedRowKeys: React.Key[];
|
|
||||||
setSelectedRowKeys: (keys: React.Key[]) => void;
|
|
||||||
siteId?: string;
|
|
||||||
}> = ({ tableRef, selectedRowKeys, setSelectedRowKeys, siteId }) => {
|
|
||||||
const { message, modal } = App.useApp();
|
|
||||||
const hasSelection = selectedRowKeys && selectedRowKeys.length > 0;
|
|
||||||
|
|
||||||
const handleBatchDelete = () => {
|
|
||||||
if (!siteId) return;
|
|
||||||
modal.confirm({
|
|
||||||
title: '确认批量删除',
|
|
||||||
content: `确定要删除选中的 ${selectedRowKeys.length} 个产品吗?`,
|
|
||||||
onOk: async () => {
|
|
||||||
try {
|
|
||||||
const res = await request(`/site-api/${siteId}/products/batch`, {
|
|
||||||
method: 'POST',
|
|
||||||
data: { delete: selectedRowKeys },
|
|
||||||
});
|
|
||||||
if (res.success) {
|
|
||||||
message.success('批量删除成功');
|
|
||||||
} else {
|
|
||||||
message.warning(res.message || '部分删除失败');
|
|
||||||
}
|
|
||||||
tableRef.current?.reload();
|
|
||||||
setSelectedRowKeys([]);
|
|
||||||
} catch (error: any) {
|
|
||||||
message.error('批量删除失败');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
type="primary"
|
|
||||||
danger
|
|
||||||
title="批量删除"
|
|
||||||
disabled={!hasSelection}
|
|
||||||
onClick={handleBatchDelete}
|
|
||||||
icon={<DeleteFilled />}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const BatchEditProducts: React.FC<{
|
|
||||||
tableRef: React.MutableRefObject<ActionType | undefined>;
|
|
||||||
selectedRowKeys: React.Key[];
|
|
||||||
setSelectedRowKeys: (keys: React.Key[]) => void;
|
|
||||||
selectedRows: any[];
|
|
||||||
siteId?: string;
|
|
||||||
}> = ({
|
|
||||||
tableRef,
|
|
||||||
selectedRowKeys,
|
|
||||||
setSelectedRowKeys,
|
|
||||||
selectedRows,
|
|
||||||
siteId,
|
|
||||||
}) => {
|
|
||||||
const { message } = App.useApp();
|
|
||||||
return (
|
|
||||||
<ModalForm
|
|
||||||
title="批量编辑产品"
|
|
||||||
trigger={
|
|
||||||
<Button
|
|
||||||
disabled={!selectedRowKeys.length}
|
|
||||||
type="primary"
|
|
||||||
icon={<EditOutlined />}
|
|
||||||
>
|
|
||||||
批量编辑
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
width={600}
|
|
||||||
modalProps={{ destroyOnHidden: true }}
|
|
||||||
onFinish={async (values) => {
|
|
||||||
if (!siteId) return false;
|
|
||||||
const updatePayload = selectedRows.map((row) => ({
|
|
||||||
id: row.id,
|
|
||||||
...values,
|
|
||||||
}));
|
|
||||||
try {
|
|
||||||
const res = await request(`/site-api/${siteId}/products/batch`, {
|
|
||||||
method: 'POST',
|
|
||||||
data: { update: updatePayload },
|
|
||||||
});
|
|
||||||
if (res.success) {
|
|
||||||
message.success('批量编辑成功');
|
|
||||||
tableRef.current?.reload();
|
|
||||||
setSelectedRowKeys([]);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
message.error(res.message || '批量编辑失败');
|
|
||||||
return false;
|
|
||||||
} catch (e: any) {
|
|
||||||
message.error(e.message || '批量编辑失败');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ProForm.Group>
|
|
||||||
<ProFormSelect
|
|
||||||
name="status"
|
|
||||||
label="产品状态"
|
|
||||||
valueEnum={PRODUCT_STATUS_ENUM}
|
|
||||||
/>
|
|
||||||
<ProFormSelect
|
|
||||||
name="stock_status"
|
|
||||||
label="库存状态"
|
|
||||||
valueEnum={PRODUCT_STOCK_STATUS_ENUM}
|
|
||||||
/>
|
|
||||||
<ProFormDigit
|
|
||||||
name="stock_quantity"
|
|
||||||
label="库存数量"
|
|
||||||
fieldProps={{ precision: 0 }}
|
|
||||||
/>
|
|
||||||
</ProForm.Group>
|
|
||||||
</ModalForm>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
// Disable for now
|
|
||||||
export const SetComponent: React.FC<any> = () => null; // Disable for now (relies on local productcontrollerProductbysku?)
|
|
||||||
|
|
||||||
export const ImportCsv: React.FC<{
|
|
||||||
tableRef: React.MutableRefObject<ActionType | undefined>;
|
|
||||||
siteId?: string;
|
|
||||||
}> = ({ tableRef, siteId }) => {
|
|
||||||
const { message } = App.useApp();
|
|
||||||
return (
|
|
||||||
<ModalForm
|
|
||||||
title="批量导入产品"
|
|
||||||
trigger={
|
|
||||||
<Button type="primary" ghost icon={<PlusOutlined />}>
|
|
||||||
批量导入
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
width={600}
|
|
||||||
modalProps={{ destroyOnHidden: true }}
|
|
||||||
onFinish={async (values) => {
|
|
||||||
if (!siteId) return false;
|
|
||||||
const csvText = values.csv || '';
|
|
||||||
const itemsList = values.items || [];
|
|
||||||
try {
|
|
||||||
const res = await request(`/site-api/${siteId}/products/import`, {
|
|
||||||
method: 'POST',
|
|
||||||
data: { csv: csvText, items: itemsList },
|
|
||||||
});
|
|
||||||
if (res.success) {
|
|
||||||
message.success('导入完成');
|
|
||||||
tableRef.current?.reload();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
message.error(res.message || '导入失败');
|
|
||||||
return false;
|
|
||||||
} catch (e: any) {
|
|
||||||
message.error(e.message || '导入失败');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ProFormTextArea
|
|
||||||
name="csv"
|
|
||||||
label="CSV文本"
|
|
||||||
placeholder="粘贴CSV,首行为表头"
|
|
||||||
/>
|
|
||||||
<ProFormList name="items" label="或手动输入产品" initialValue={[]}>
|
|
||||||
<ProForm.Group>
|
|
||||||
<ProFormText name="name" label="名称" />
|
|
||||||
<ProFormText name="sku" label="SKU" />
|
|
||||||
<ProFormDigit
|
|
||||||
name="regular_price"
|
|
||||||
label="常规价"
|
|
||||||
fieldProps={{ precision: 2 }}
|
|
||||||
/>
|
|
||||||
<ProFormDigit
|
|
||||||
name="sale_price"
|
|
||||||
label="促销价"
|
|
||||||
fieldProps={{ precision: 2 }}
|
|
||||||
/>
|
|
||||||
</ProForm.Group>
|
|
||||||
</ProFormList>
|
|
||||||
</ModalForm>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
// Disable for now
|
|
||||||
|
|
@ -1,177 +0,0 @@
|
||||||
// 定义配置接口
|
|
||||||
export interface TagConfig {
|
|
||||||
brands: string[];
|
|
||||||
fruits: string[];
|
|
||||||
mints: string[];
|
|
||||||
flavors: string[];
|
|
||||||
strengths: string[];
|
|
||||||
sizes: string[];
|
|
||||||
humidities: string[];
|
|
||||||
categories: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description 从产品名称中解析出品牌,口味,毫克含量和干燥度
|
|
||||||
*/
|
|
||||||
export const parseName = (
|
|
||||||
name: string,
|
|
||||||
brands: string[],
|
|
||||||
): [string, string, string, string] => {
|
|
||||||
const nm = name.trim();
|
|
||||||
const dryMatch = nm.match(/\(([^)]*)\)/);
|
|
||||||
const dryness = dryMatch ? dryMatch[1].trim() : '';
|
|
||||||
|
|
||||||
const mgMatch = nm.match(/(\d+)\s*MG/i);
|
|
||||||
const mg = mgMatch ? mgMatch[1] : '';
|
|
||||||
|
|
||||||
for (const b of brands) {
|
|
||||||
if (nm.toUpperCase().startsWith(b.toUpperCase())) {
|
|
||||||
const brand = b;
|
|
||||||
const start = b.length;
|
|
||||||
const end = mgMatch ? mgMatch.index : nm.length;
|
|
||||||
let flavorPart = nm.substring(start, end);
|
|
||||||
flavorPart = flavorPart.replace(/-/g, ' ').trim();
|
|
||||||
flavorPart = flavorPart.replace(/\s*\([^)]*\)$/, '').trim();
|
|
||||||
return [brand, flavorPart, mg, dryness];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const firstWord = nm.split(' ')[0] || '';
|
|
||||||
const brand = firstWord;
|
|
||||||
const end = mgMatch ? mgMatch.index : nm.length;
|
|
||||||
const flavorPart = nm.substring(brand.length, end).trim();
|
|
||||||
return [brand, flavorPart, mg, dryness];
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description 将口味部分拆分为规范的令牌
|
|
||||||
*/
|
|
||||||
export const splitFlavorTokens = (flavorPart: string): string[] => {
|
|
||||||
const rawTokens = flavorPart.match(/[A-Za-z]+/g) || [];
|
|
||||||
const tokens: string[] = [];
|
|
||||||
const EXCEPT_SPLIT = new Set(['spearmint', 'peppermint']);
|
|
||||||
|
|
||||||
for (const tok of rawTokens) {
|
|
||||||
const t = tok.toLowerCase();
|
|
||||||
if (t.endsWith('mint') && t.length > 4 && !EXCEPT_SPLIT.has(t)) {
|
|
||||||
const pre = t.slice(0, -4);
|
|
||||||
if (pre) {
|
|
||||||
tokens.push(pre);
|
|
||||||
}
|
|
||||||
tokens.push('mint');
|
|
||||||
} else {
|
|
||||||
tokens.push(t);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return tokens;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description 根据口味分类额外的标签(如 Fruit, Mint)
|
|
||||||
*/
|
|
||||||
export const classifyExtraTags = (
|
|
||||||
flavorPart: string,
|
|
||||||
fruits: string[],
|
|
||||||
mints: string[],
|
|
||||||
): string[] => {
|
|
||||||
const tokens = splitFlavorTokens(flavorPart);
|
|
||||||
const fLower = flavorPart.toLowerCase();
|
|
||||||
const isFruit =
|
|
||||||
fruits.some((key) => fLower.includes(key.toLowerCase())) ||
|
|
||||||
tokens.some((t) => fruits.map((k) => k.toLowerCase()).includes(t));
|
|
||||||
const isMint =
|
|
||||||
mints.some((key) => fLower.includes(key.toLowerCase())) ||
|
|
||||||
tokens.includes('mint');
|
|
||||||
|
|
||||||
const extras: string[] = [];
|
|
||||||
if (isFruit) extras.push('Fruit');
|
|
||||||
if (isMint) extras.push('Mint');
|
|
||||||
return extras;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description 在文本中匹配属性关键词
|
|
||||||
*/
|
|
||||||
export const matchAttributes = (text: string, keys: string[]): string[] => {
|
|
||||||
const matched = new Set<string>();
|
|
||||||
for (const key of keys) {
|
|
||||||
const escapedKey = key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
||||||
const regex = new RegExp(`\\b${escapedKey}\\b`, 'i');
|
|
||||||
if (regex.test(text)) {
|
|
||||||
matched.add(key.charAt(0).toUpperCase() + key.slice(1));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return Array.from(matched);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description 计算最终的 Tags 字符串
|
|
||||||
*/
|
|
||||||
export const computeTags = (
|
|
||||||
name: string,
|
|
||||||
sku: string,
|
|
||||||
config: TagConfig,
|
|
||||||
): string => {
|
|
||||||
const [brand, flavorPart, mg, dryness] = parseName(name, config.brands);
|
|
||||||
const tokens = splitFlavorTokens(flavorPart);
|
|
||||||
|
|
||||||
const flavorKeysLower = config.flavors.map((k) => k.toLowerCase());
|
|
||||||
|
|
||||||
const tokensForFlavor = tokens.filter((t) =>
|
|
||||||
flavorKeysLower.includes(t.toLowerCase()),
|
|
||||||
);
|
|
||||||
|
|
||||||
const flavorTag = tokensForFlavor
|
|
||||||
.map((t) => t.charAt(0).toUpperCase() + t.slice(1))
|
|
||||||
.join('');
|
|
||||||
|
|
||||||
let tags: string[] = [];
|
|
||||||
if (brand) tags.push(brand);
|
|
||||||
if (flavorTag) tags.push(flavorTag);
|
|
||||||
|
|
||||||
for (const t of tokensForFlavor) {
|
|
||||||
const isFruitKey = config.fruits.some(
|
|
||||||
(k) => k.toLowerCase() === t.toLowerCase(),
|
|
||||||
);
|
|
||||||
if (isFruitKey && t.toLowerCase() !== 'fruit') {
|
|
||||||
tags.push(t.charAt(0).toUpperCase() + t.slice(1));
|
|
||||||
}
|
|
||||||
if (t.toLowerCase() === 'mint') {
|
|
||||||
tags.push('Mint');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tags.push(...matchAttributes(name, config.sizes));
|
|
||||||
tags.push(...matchAttributes(name, config.humidities));
|
|
||||||
tags.push(...matchAttributes(name, config.categories));
|
|
||||||
tags.push(...matchAttributes(name, config.strengths));
|
|
||||||
|
|
||||||
if (/mix/i.test(name) || (sku && /mix/i.test(sku))) {
|
|
||||||
tags.push('Mix Pack');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mg) {
|
|
||||||
tags.push(`${mg} mg`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (dryness) {
|
|
||||||
if (/moist/i.test(dryness)) {
|
|
||||||
tags.push('Moisture');
|
|
||||||
} else {
|
|
||||||
tags.push(dryness.charAt(0).toUpperCase() + dryness.slice(1));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tags.push(...classifyExtraTags(flavorPart, config.fruits, config.mints));
|
|
||||||
|
|
||||||
const seen = new Set<string>();
|
|
||||||
const finalTags = tags.filter((t) => {
|
|
||||||
if (t && !seen.has(t)) {
|
|
||||||
seen.add(t);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
|
|
||||||
return finalTags.join(', ');
|
|
||||||
};
|
|
||||||
|
|
@ -1,50 +0,0 @@
|
||||||
{
|
|
||||||
"admin_id": 0,
|
|
||||||
"admin_name": "",
|
|
||||||
"birthday": 0,
|
|
||||||
"contact": "",
|
|
||||||
"country_id": 14,
|
|
||||||
"created_at": 1765351077,
|
|
||||||
"domain": "auspouches.com",
|
|
||||||
"email": "daniel.waring81@gmail.com",
|
|
||||||
"first_name": "Dan",
|
|
||||||
"first_pay_at": 1765351308,
|
|
||||||
"gender": 0,
|
|
||||||
"id": 44898147,
|
|
||||||
"ip": "1.146.111.163",
|
|
||||||
"is_cart": 0,
|
|
||||||
"is_event_sub": 1,
|
|
||||||
"is_sub": 1,
|
|
||||||
"is_verified": 1,
|
|
||||||
"last_name": "Waring",
|
|
||||||
"last_order_id": 236122,
|
|
||||||
"login_at": 1765351340,
|
|
||||||
"note": "",
|
|
||||||
"order_at": 1765351224,
|
|
||||||
"orders_count": 1,
|
|
||||||
"pay_at": 1765351308,
|
|
||||||
"source_device": "phone",
|
|
||||||
"tags": [],
|
|
||||||
"total_spent": "203.81",
|
|
||||||
"updated_at": 1765351515,
|
|
||||||
"utm_medium": "referral",
|
|
||||||
"utm_source": "checkout.cartadicreditopay.com",
|
|
||||||
"visit_at": 1765351513,
|
|
||||||
"country": {
|
|
||||||
"chinese_name": "澳大利亚",
|
|
||||||
"country_code2": "AU",
|
|
||||||
"country_name": "Australia"
|
|
||||||
},
|
|
||||||
"sysinfo": {
|
|
||||||
"user_agent": "Mozilla/5.0 (Linux; Android 16; Pixel 8 Pro Build/BP3A.251105.015; ) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/142.0.7444.212 Mobile Safari/537.36 MetaIAB Facebook",
|
|
||||||
"timezone": "Etc/GMT-10",
|
|
||||||
"os": "Android",
|
|
||||||
"browser": "Pixel 8",
|
|
||||||
"language": "en-GB",
|
|
||||||
"screen_size": "528X1174",
|
|
||||||
"viewport_size": "527X1026",
|
|
||||||
"ip": "1.146.111.163"
|
|
||||||
},
|
|
||||||
"default_address": [],
|
|
||||||
"addresses": []
|
|
||||||
}
|
|
||||||
|
|
@ -29,7 +29,7 @@ const ListPage: React.FC = () => {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'SKU',
|
title: 'SKU',
|
||||||
dataIndex: 'sku',
|
dataIndex: 'productSku',
|
||||||
hideInSearch: true,
|
hideInSearch: true,
|
||||||
},
|
},
|
||||||
...points
|
...points
|
||||||
|
|
@ -88,13 +88,13 @@ const ListPage: React.FC = () => {
|
||||||
render(_, record) {
|
render(_, record) {
|
||||||
return (
|
return (
|
||||||
<ProFormDigit
|
<ProFormDigit
|
||||||
key={record.sku}
|
key={record.productSku}
|
||||||
initialValue={0}
|
initialValue={0}
|
||||||
fieldProps={{
|
fieldProps={{
|
||||||
onChange(value) {
|
onChange(value) {
|
||||||
setReal({
|
setReal({
|
||||||
...real,
|
...real,
|
||||||
[record.sku]: value,
|
[record.productSku]: value,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
|
|
@ -107,7 +107,7 @@ const ListPage: React.FC = () => {
|
||||||
dataIndex: 'restockQuantityReal',
|
dataIndex: 'restockQuantityReal',
|
||||||
hideInSearch: true,
|
hideInSearch: true,
|
||||||
render(_, record) {
|
render(_, record) {
|
||||||
return <ProFormDigit key={'b_' + record.sku} />;
|
return <ProFormDigit key={'b_' + record.productSku} />;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -138,7 +138,7 @@ const ListPage: React.FC = () => {
|
||||||
render(_, record) {
|
render(_, record) {
|
||||||
if (!record.availableDays) return '-';
|
if (!record.availableDays) return '-';
|
||||||
const availableDays = Number(record.availableDays);
|
const availableDays = Number(record.availableDays);
|
||||||
const quantity = real?.[record.sku] || 0;
|
const quantity = real?.[record.productSku] || 0;
|
||||||
const day =
|
const day =
|
||||||
availableDays +
|
availableDays +
|
||||||
Math.floor(
|
Math.floor(
|
||||||
|
|
@ -154,7 +154,7 @@ const ListPage: React.FC = () => {
|
||||||
render(_, record) {
|
render(_, record) {
|
||||||
if (!record.availableDays) return '-';
|
if (!record.availableDays) return '-';
|
||||||
const availableDays = Number(record.availableDays);
|
const availableDays = Number(record.availableDays);
|
||||||
const quantity = real?.[record.sku] || 0;
|
const quantity = real?.[record.productSku] || 0;
|
||||||
const day =
|
const day =
|
||||||
availableDays +
|
availableDays +
|
||||||
Math.floor(
|
Math.floor(
|
||||||
|
|
|
||||||
|
|
@ -19,12 +19,9 @@ import {
|
||||||
} from '@ant-design/pro-components';
|
} from '@ant-design/pro-components';
|
||||||
import { Button, Space, Tag } from 'antd';
|
import { Button, Space, Tag } from 'antd';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import weekOfYear from 'dayjs/plugin/weekOfYear';
|
|
||||||
import ReactECharts from 'echarts-for-react';
|
import ReactECharts from 'echarts-for-react';
|
||||||
import * as countries from 'i18n-iso-countries';
|
|
||||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
|
|
||||||
dayjs.extend(weekOfYear);
|
|
||||||
const highlightText = (text: string, keyword: string) => {
|
const highlightText = (text: string, keyword: string) => {
|
||||||
if (!keyword) return text;
|
if (!keyword) return text;
|
||||||
const parts = text.split(new RegExp(`(${keyword})`, 'gi'));
|
const parts = text.split(new RegExp(`(${keyword})`, 'gi'));
|
||||||
|
|
@ -39,17 +36,6 @@ const highlightText = (text: string, keyword: 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 [xAxis, setXAxis] = useState([]);
|
const [xAxis, setXAxis] = useState([]);
|
||||||
const [series, setSeries] = useState<any[]>([]);
|
const [series, setSeries] = useState<any[]>([]);
|
||||||
|
|
@ -142,23 +128,7 @@ const ListPage: React.FC = () => {
|
||||||
});
|
});
|
||||||
if (success) {
|
if (success) {
|
||||||
const res = data?.sort(() => -1);
|
const res = data?.sort(() => -1);
|
||||||
const formatMap = {
|
setXAxis(res?.map((v) => dayjs(v.order_date).format('YYYY-MM-DD')));
|
||||||
month: 'YYYY-MM',
|
|
||||||
week: 'YYYY年第WW周',
|
|
||||||
day: 'YYYY-MM-DD',
|
|
||||||
};
|
|
||||||
const format = formatMap[params.grouping] || 'YYYY-MM-DD';
|
|
||||||
|
|
||||||
if (params.grouping === 'week') {
|
|
||||||
setXAxis(
|
|
||||||
res?.map((v) => {
|
|
||||||
const [year, week] = v.order_date.split('-');
|
|
||||||
return `${year}年第${week}周`;
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
setXAxis(res?.map((v) => dayjs(v.order_date).format(format)));
|
|
||||||
}
|
|
||||||
setSeries([
|
setSeries([
|
||||||
{
|
{
|
||||||
name: 'TOGO CPC订单数',
|
name: 'TOGO CPC订单数',
|
||||||
|
|
@ -613,39 +583,17 @@ const ListPage: React.FC = () => {
|
||||||
name="date"
|
name="date"
|
||||||
/>
|
/>
|
||||||
{/* <ProFormText label="关键词" name="keyword" /> */}
|
{/* <ProFormText label="关键词" name="keyword" /> */}
|
||||||
<ProFormSelect
|
|
||||||
label="统计周期"
|
|
||||||
name="grouping"
|
|
||||||
initialValue="day"
|
|
||||||
options={[
|
|
||||||
{ label: '月', value: 'month' },
|
|
||||||
{ label: '周', value: 'week' },
|
|
||||||
{ label: '日', value: 'day' },
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
<ProFormSelect
|
<ProFormSelect
|
||||||
label="站点"
|
label="站点"
|
||||||
name="siteId"
|
name="siteId"
|
||||||
request={async () => {
|
request={async () => {
|
||||||
const { data = [] } = await sitecontrollerAll();
|
const { data = [] } = await sitecontrollerAll();
|
||||||
return data.map((item) => ({
|
return data.map((item) => ({
|
||||||
label: item.name,
|
label: item.siteName,
|
||||||
value: item.id,
|
value: item.id,
|
||||||
}));
|
}));
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ProFormSelect
|
|
||||||
name="country"
|
|
||||||
label="区域"
|
|
||||||
mode="multiple"
|
|
||||||
placeholder="请选择区域"
|
|
||||||
showSearch
|
|
||||||
filterOption={(input, option) =>
|
|
||||||
(option?.label ?? '').toLowerCase().includes(input.toLowerCase())
|
|
||||||
}
|
|
||||||
options={getCountryOptions()}
|
|
||||||
/>
|
|
||||||
{/* <ProFormSelect
|
{/* <ProFormSelect
|
||||||
label="类型"
|
label="类型"
|
||||||
name="purchaseType"
|
name="purchaseType"
|
||||||
|
|
@ -757,7 +705,7 @@ const DailyOrders: React.FC<{
|
||||||
request: async () => {
|
request: async () => {
|
||||||
const { data = [] } = await sitecontrollerAll();
|
const { data = [] } = await sitecontrollerAll();
|
||||||
return data.map((item) => ({
|
return data.map((item) => ({
|
||||||
label: item.name,
|
label: item.siteName,
|
||||||
value: item.id,
|
value: item.id,
|
||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
|
|
@ -963,7 +911,7 @@ export const HistoryOrder: React.FC<{
|
||||||
request: async () => {
|
request: async () => {
|
||||||
const { data = [] } = await sitecontrollerAll();
|
const { data = [] } = await sitecontrollerAll();
|
||||||
return data.map((item) => ({
|
return data.map((item) => ({
|
||||||
label: item.name,
|
label: item.siteName,
|
||||||
value: item.id,
|
value: item.id,
|
||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,72 +1,39 @@
|
||||||
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
import React, { useEffect, useState, useMemo, useRef } from "react"
|
||||||
|
|
||||||
import {
|
|
||||||
statisticscontrollerGetinativeusersbymonth,
|
|
||||||
statisticscontrollerGetordersource,
|
|
||||||
} from '@/servers/api/statistics';
|
|
||||||
import {
|
import {
|
||||||
ActionType,
|
ActionType,
|
||||||
PageContainer,
|
PageContainer, ProColumns, ProTable,
|
||||||
ProColumns,
|
} from '@ant-design/pro-components';
|
||||||
ProForm,
|
import { statisticscontrollerGetordersorce, statisticscontrollerGetinativeusersbymonth } from "@/servers/api/statistics";
|
||||||
ProFormSelect,
|
|
||||||
ProTable,
|
|
||||||
} from '@ant-design/pro-components';
|
|
||||||
import { Space, Tag } from 'antd';
|
|
||||||
import dayjs from 'dayjs';
|
|
||||||
import ReactECharts from 'echarts-for-react';
|
import ReactECharts from 'echarts-for-react';
|
||||||
import * as countries from 'i18n-iso-countries';
|
import { App, Button, Space, Tag } from 'antd';
|
||||||
import zhCN from 'i18n-iso-countries/langs/zh';
|
import { HistoryOrder } from "../Order";
|
||||||
import { HistoryOrder } from '../Order';
|
import dayjs from 'dayjs';
|
||||||
countries.registerLocale(zhCN);
|
|
||||||
const ListPage: React.FC = () => {
|
const ListPage: React.FC = () => {
|
||||||
|
|
||||||
const [data, setData] = useState({});
|
const [data, setData] = useState({});
|
||||||
const initialValues = {
|
|
||||||
country: ['CA'],
|
|
||||||
};
|
|
||||||
function handleSubmit(values: typeof initialValues) {
|
|
||||||
statisticscontrollerGetordersource({ params: values }).then(
|
|
||||||
({ data, success }) => {
|
|
||||||
if (success) setData(data);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
handleSubmit(initialValues);
|
statisticscontrollerGetordersorce().then(({ data, success }) => {
|
||||||
|
if(success) setData(data)
|
||||||
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const option = useMemo(() => {
|
const option = useMemo(() => {
|
||||||
if (!data.inactiveRes) return {};
|
if(!data.inactiveRes) return {}
|
||||||
const xAxisData = data?.inactiveRes
|
const xAxisData = data?.inactiveRes?.map(v=> v.order_month)?.sort(_=>-1)
|
||||||
?.map((v) => v.order_month)
|
const arr = data?.res?.map(v=>v.first_order_month_group)
|
||||||
?.sort((_) => -1);
|
const uniqueArr = arr.filter((item, index) => arr.indexOf(item) === index).sort((a,b)=> a.localeCompare(b))
|
||||||
const arr = data?.res?.map((v) => v.first_order_month_group);
|
|
||||||
const uniqueArr = arr
|
|
||||||
.filter((item, index) => arr.indexOf(item) === index)
|
|
||||||
.sort((a, b) => a.localeCompare(b));
|
|
||||||
const series = [
|
const series = [
|
||||||
{
|
{
|
||||||
name: '新客户',
|
name: '新客户',
|
||||||
type: 'bar',
|
type: 'bar',
|
||||||
data: data?.inactiveRes?.map((v) => v.new_user_count)?.sort((_) => -1),
|
data: data?.inactiveRes?.map(v=> v.new_user_count)?.sort(_=>-1),
|
||||||
label: {
|
label: {
|
||||||
show: true,
|
show: true,
|
||||||
formatter: function (params) {
|
|
||||||
if (!params.value) return '';
|
|
||||||
return (
|
|
||||||
Math.abs(params.value) +
|
|
||||||
'\n' +
|
|
||||||
Math.abs(
|
|
||||||
data?.inactiveRes?.find(
|
|
||||||
(item) => item.order_month === params.name,
|
|
||||||
)?.new_user_total || 0,
|
|
||||||
)
|
|
||||||
);
|
|
||||||
},
|
|
||||||
color: '#000000',
|
|
||||||
},
|
},
|
||||||
emphasis: {
|
emphasis: {
|
||||||
focus: 'series',
|
focus: 'series'
|
||||||
},
|
},
|
||||||
xAxisIndex: 0,
|
xAxisIndex: 0,
|
||||||
yAxisIndex: 0,
|
yAxisIndex: 0,
|
||||||
|
|
@ -74,123 +41,86 @@ const ListPage: React.FC = () => {
|
||||||
{
|
{
|
||||||
name: '老客户',
|
name: '老客户',
|
||||||
type: 'bar',
|
type: 'bar',
|
||||||
data: data?.inactiveRes?.map((v) => v.old_user_count)?.sort((_) => -1),
|
data: data?.inactiveRes?.map(v=> v.old_user_count)?.sort(_=>-1),
|
||||||
label: {
|
label: {
|
||||||
show: true,
|
show: true,
|
||||||
formatter: function (params) {
|
|
||||||
if (!params.value) return '';
|
|
||||||
return (
|
|
||||||
Math.abs(params.value) +
|
|
||||||
'\n' +
|
|
||||||
Math.abs(
|
|
||||||
data?.inactiveRes?.find(
|
|
||||||
(item) => item.order_month === params.name,
|
|
||||||
)?.old_user_total || 0,
|
|
||||||
)
|
|
||||||
);
|
|
||||||
},
|
|
||||||
color: '#000000',
|
|
||||||
},
|
},
|
||||||
emphasis: {
|
emphasis: {
|
||||||
focus: 'series',
|
focus: 'series'
|
||||||
},
|
},
|
||||||
xAxisIndex: 0,
|
xAxisIndex: 0,
|
||||||
yAxisIndex: 0,
|
yAxisIndex: 0,
|
||||||
},
|
},
|
||||||
...uniqueArr?.map((v) => {
|
...uniqueArr?.map(v => {
|
||||||
data?.res?.filter((item) => item.order_month === v);
|
data?.res?.filter(item => item.order_month === v)
|
||||||
return {
|
return {
|
||||||
name: v,
|
name: v,
|
||||||
type: 'bar',
|
type: "bar",
|
||||||
stack: 'total',
|
stack: "total",
|
||||||
label: {
|
label: {
|
||||||
show: true,
|
"show": true,
|
||||||
formatter: function (params) {
|
formatter: function(params) {
|
||||||
if (!params.value) return '';
|
if(!params.value) return ''
|
||||||
return (
|
return Math.abs(params.value)
|
||||||
Math.abs(params.value) +
|
|
||||||
'\n' +
|
|
||||||
+Math.abs(
|
|
||||||
data?.res?.find(
|
|
||||||
(item) =>
|
|
||||||
item.order_month === params.name &&
|
|
||||||
item.first_order_month_group === v,
|
|
||||||
)?.total || 0,
|
|
||||||
)
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
color: '#000000',
|
color: '#fff'
|
||||||
},
|
},
|
||||||
data: xAxisData.map((month) => {
|
"data": xAxisData.map(month => {
|
||||||
return (
|
return (data?.res?.find(item => item.order_month === month && item.first_order_month_group === v)?.order_count || 0)
|
||||||
data?.res?.find(
|
|
||||||
(item) =>
|
|
||||||
item.order_month === month &&
|
|
||||||
item.first_order_month_group === v,
|
|
||||||
)?.order_count || 0
|
|
||||||
);
|
|
||||||
}),
|
}),
|
||||||
xAxisIndex: 0,
|
xAxisIndex: 0,
|
||||||
yAxisIndex: 0,
|
yAxisIndex: 0,
|
||||||
};
|
}
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
name: '未复购客户',
|
name: '未复购客户',
|
||||||
type: 'bar',
|
type: 'bar',
|
||||||
data: data?.inactiveRes
|
data: data?.inactiveRes?.map(v=> -v.inactive_user_count)?.sort(_=>-1),
|
||||||
?.map((v) => -v.inactive_user_count)
|
stack: "total",
|
||||||
?.sort((_) => -1),
|
|
||||||
stack: 'total',
|
|
||||||
label: {
|
label: {
|
||||||
show: true,
|
show: true,
|
||||||
},
|
},
|
||||||
emphasis: {
|
emphasis: {
|
||||||
focus: 'series',
|
focus: 'series'
|
||||||
},
|
},
|
||||||
xAxisIndex: 1,
|
xAxisIndex: 1,
|
||||||
yAxisIndex: 1,
|
yAxisIndex: 1,
|
||||||
barWidth: '60%',
|
barWidth: "60%",
|
||||||
|
|
||||||
itemStyle: {
|
itemStyle: {
|
||||||
color: '#f44336',
|
color: '#f44336'
|
||||||
|
}
|
||||||
},
|
},
|
||||||
},
|
]
|
||||||
];
|
|
||||||
return {
|
return {
|
||||||
grid: [
|
grid: [
|
||||||
{ top: '10%', height: '70%' },
|
{ top: '10%', height: '70%' },
|
||||||
{ bottom: '10%', height: '10%' },
|
{ bottom: '10%', height: '10%' }
|
||||||
],
|
],
|
||||||
legend: {
|
legend: {
|
||||||
selectedMode: false,
|
selectedMode: false
|
||||||
},
|
},
|
||||||
xAxis: [
|
xAxis: [{
|
||||||
{
|
|
||||||
type: 'category',
|
type: 'category',
|
||||||
data: xAxisData,
|
data: xAxisData,
|
||||||
gridIndex: 0,
|
gridIndex: 0,
|
||||||
},
|
},{
|
||||||
{
|
|
||||||
type: 'category',
|
type: 'category',
|
||||||
data: xAxisData,
|
data: xAxisData,
|
||||||
gridIndex: 1,
|
gridIndex: 1,
|
||||||
},
|
}],
|
||||||
],
|
yAxis: [{
|
||||||
yAxis: [
|
|
||||||
{
|
|
||||||
type: 'value',
|
type: 'value',
|
||||||
gridIndex: 0,
|
gridIndex: 0,
|
||||||
},
|
},{
|
||||||
{
|
|
||||||
type: 'value',
|
type: 'value',
|
||||||
gridIndex: 1,
|
gridIndex: 1,
|
||||||
},
|
}],
|
||||||
],
|
|
||||||
series,
|
series,
|
||||||
};
|
}
|
||||||
}, [data]);
|
}, [data])
|
||||||
|
|
||||||
const [tableData, setTableData] = useState<any[]>([]);
|
const [tableData, setTableData] = useState<any[]>([])
|
||||||
const actionRef = useRef<ActionType>();
|
const actionRef = useRef<ActionType>();
|
||||||
const columns: ProColumns[] = [
|
const columns: ProColumns[] = [
|
||||||
{
|
{
|
||||||
|
|
@ -290,44 +220,25 @@ const ListPage: React.FC = () => {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
return(
|
||||||
return (
|
|
||||||
<PageContainer ghost>
|
<PageContainer ghost>
|
||||||
<ProForm
|
|
||||||
initialValues={initialValues}
|
|
||||||
layout="inline"
|
|
||||||
onFinish={handleSubmit}
|
|
||||||
>
|
|
||||||
<ProFormSelect
|
|
||||||
name="country"
|
|
||||||
label="区域"
|
|
||||||
mode="multiple"
|
|
||||||
placeholder="请选择区域"
|
|
||||||
showSearch
|
|
||||||
filterOption={(input, option) =>
|
|
||||||
(option?.label ?? '').toLowerCase().includes(input.toLowerCase())
|
|
||||||
}
|
|
||||||
options={getCountryOptions()}
|
|
||||||
/>
|
|
||||||
</ProForm>
|
|
||||||
<ReactECharts
|
<ReactECharts
|
||||||
option={option}
|
option={option}
|
||||||
style={{ height: 1050 }}
|
style={{ height: 1050 }}
|
||||||
onEvents={{
|
onEvents={{
|
||||||
click: async (params) => {
|
click: async (params) => {
|
||||||
if (params.componentType === 'series') {
|
if (params.componentType === 'series') {
|
||||||
setTableData([]);
|
setTableData([])
|
||||||
const { success, data } =
|
const {success, data} = await statisticscontrollerGetinativeusersbymonth({
|
||||||
await statisticscontrollerGetinativeusersbymonth({
|
month: params.name
|
||||||
month: params.name,
|
})
|
||||||
});
|
if(success) setTableData(data)
|
||||||
if (success) setTableData(data);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
{
|
||||||
{tableData?.length ? (
|
tableData?.length ?
|
||||||
<ProTable
|
<ProTable
|
||||||
search={false}
|
search={false}
|
||||||
headerTitle="查询表格"
|
headerTitle="查询表格"
|
||||||
|
|
@ -336,22 +247,11 @@ const ListPage: React.FC = () => {
|
||||||
dataSource={tableData}
|
dataSource={tableData}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
/>
|
/>
|
||||||
) : (
|
:<></>
|
||||||
<></>
|
|
||||||
)}
|
}
|
||||||
</PageContainer>
|
</PageContainer>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
// 获取所有国家/地区的选项
|
|
||||||
const getCountryOptions = () => {
|
|
||||||
// 获取所有国家的 ISO 代码
|
|
||||||
const countryCodes = countries.getAlpha2Codes();
|
|
||||||
// 将国家代码转换为选项数组
|
|
||||||
|
|
||||||
return Object.keys(countryCodes).map((code) => ({
|
|
||||||
label: countries.getName(code, 'zh') || code, // 使用中文名称, 如果没有则使用代码
|
|
||||||
value: code,
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
export default ListPage;
|
export default ListPage;
|
||||||
|
|
@ -96,14 +96,14 @@ const ListPage: React.FC = () => {
|
||||||
render(_, record) {
|
render(_, record) {
|
||||||
return (
|
return (
|
||||||
<ProFormDigit
|
<ProFormDigit
|
||||||
key={record.sku}
|
key={record.productSku}
|
||||||
width={100}
|
width={100}
|
||||||
fieldProps={{
|
fieldProps={{
|
||||||
defaultValue: 0,
|
defaultValue: 0,
|
||||||
onChange(value) {
|
onChange(value) {
|
||||||
setSavety({
|
setSavety({
|
||||||
...savety,
|
...savety,
|
||||||
[record.sku]: value,
|
[record.productSku]: value,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
|
|
@ -129,7 +129,7 @@ const ListPage: React.FC = () => {
|
||||||
hideInSearch: true,
|
hideInSearch: true,
|
||||||
render(_, record) {
|
render(_, record) {
|
||||||
const base = record.lastMonthSales;
|
const base = record.lastMonthSales;
|
||||||
return 3 * count * base + (savety[record.sku] || 0);
|
return 3 * count * base + (savety[record.productSku] || 0);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -139,10 +139,10 @@ const ListPage: React.FC = () => {
|
||||||
const base = record.lastMonthSales;
|
const base = record.lastMonthSales;
|
||||||
return (
|
return (
|
||||||
<ProFormDigit
|
<ProFormDigit
|
||||||
key={'fix' + record.sku + (savety[record.sku] || 0)}
|
key={'fix' + record.productSku + (savety[record.productSku] || 0)}
|
||||||
width={100}
|
width={100}
|
||||||
fieldProps={{
|
fieldProps={{
|
||||||
defaultValue: 3 * count * base + (savety[record.sku] || 0),
|
defaultValue: 3 * count * base + (savety[record.productSku] || 0),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,249 +0,0 @@
|
||||||
import { ordercontrollerGetordersales } from '@/servers/api/order';
|
|
||||||
import { sitecontrollerAll } from '@/servers/api/site';
|
|
||||||
import {
|
|
||||||
ActionType,
|
|
||||||
PageContainer,
|
|
||||||
ProColumns,
|
|
||||||
ProFormSwitch,
|
|
||||||
ProTable,
|
|
||||||
} from '@ant-design/pro-components';
|
|
||||||
import { Button } from 'antd';
|
|
||||||
import dayjs from 'dayjs';
|
|
||||||
import { saveAs } from 'file-saver';
|
|
||||||
import { useRef, useState } from 'react';
|
|
||||||
import * as XLSX from 'xlsx';
|
|
||||||
|
|
||||||
const ListPage: React.FC = () => {
|
|
||||||
const actionRef = useRef<ActionType>();
|
|
||||||
const formRef = useRef();
|
|
||||||
const [total, setTotal] = useState(0);
|
|
||||||
const [isSource, setIsSource] = useState(false);
|
|
||||||
const [yooneTotal, setYooneTotal] = useState({});
|
|
||||||
|
|
||||||
const columns: ProColumns<API.OrderSaleDTO>[] = [
|
|
||||||
{
|
|
||||||
title: '时间段',
|
|
||||||
dataIndex: 'dateRange',
|
|
||||||
valueType: 'dateTimeRange',
|
|
||||||
hideInTable: true,
|
|
||||||
formItemProps: {
|
|
||||||
rules: [
|
|
||||||
{
|
|
||||||
required: true,
|
|
||||||
message: '请选择时间段',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '排除套装',
|
|
||||||
dataIndex: 'exceptPackage',
|
|
||||||
valueType: 'switch',
|
|
||||||
hideInTable: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '产品名称',
|
|
||||||
dataIndex: 'sku',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '产品名称',
|
|
||||||
dataIndex: 'name',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '站点',
|
|
||||||
dataIndex: 'siteId',
|
|
||||||
valueType: 'select',
|
|
||||||
request: async () => {
|
|
||||||
const { data = [] } = await sitecontrollerAll();
|
|
||||||
return data.map((item) => ({
|
|
||||||
label: item.name,
|
|
||||||
value: item.id,
|
|
||||||
}));
|
|
||||||
},
|
|
||||||
hideInTable: true,
|
|
||||||
},
|
|
||||||
// {
|
|
||||||
// title: '分类',
|
|
||||||
// dataIndex: 'categoryName',
|
|
||||||
// hideInSearch: true,
|
|
||||||
// hideInTable: isSource,
|
|
||||||
// },
|
|
||||||
{
|
|
||||||
title: '数量',
|
|
||||||
dataIndex: 'totalQuantity',
|
|
||||||
hideInSearch: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '一单订单数',
|
|
||||||
dataIndex: 'firstOrderCount',
|
|
||||||
hideInSearch: true,
|
|
||||||
render(_, record) {
|
|
||||||
if (isSource) return record.firstOrderCount;
|
|
||||||
return `${record.firstOrderCount}(${record.firstOrderYOONEBoxCount})`;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '两单订单数',
|
|
||||||
dataIndex: 'secondOrderCount',
|
|
||||||
hideInSearch: true,
|
|
||||||
render(_, record) {
|
|
||||||
if (isSource) return record.secondOrderCount;
|
|
||||||
return `${record.secondOrderCount}(${record.secondOrderYOONEBoxCount})`;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '三单订单数',
|
|
||||||
dataIndex: 'thirdOrderCount',
|
|
||||||
hideInSearch: true,
|
|
||||||
render(_, record) {
|
|
||||||
if (isSource) return record.thirdOrderCount;
|
|
||||||
return `${record.thirdOrderCount}(${record.thirdOrderYOONEBoxCount})`;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '三单以上订单数',
|
|
||||||
dataIndex: 'moreThirdOrderCount',
|
|
||||||
hideInSearch: true,
|
|
||||||
render(_, record) {
|
|
||||||
if (isSource) return record.moreThirdOrderCount;
|
|
||||||
return `${record.moreThirdOrderCount}(${record.moreThirdOrderYOONEBoxCount})`;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '订单数',
|
|
||||||
dataIndex: 'totalOrders',
|
|
||||||
hideInSearch: true,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
return (
|
|
||||||
<PageContainer ghost>
|
|
||||||
<ProTable
|
|
||||||
headerTitle="查询表格"
|
|
||||||
actionRef={actionRef}
|
|
||||||
formRef={formRef}
|
|
||||||
rowKey="id"
|
|
||||||
params={{ isSource }}
|
|
||||||
form={{
|
|
||||||
// ignoreRules: false,
|
|
||||||
initialValues: {
|
|
||||||
dateRange: [dayjs().startOf('month'), dayjs().endOf('month')],
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
request={async ({ dateRange, ...param }) => {
|
|
||||||
const [startDate, endDate] = dateRange.values();
|
|
||||||
const { data, success } = await ordercontrollerGetordersales({
|
|
||||||
startDate,
|
|
||||||
endDate,
|
|
||||||
...param,
|
|
||||||
});
|
|
||||||
if (success) {
|
|
||||||
setTotal(data?.totalQuantity || 0);
|
|
||||||
setYooneTotal({
|
|
||||||
yoone3Quantity: data?.yoone3Quantity || 0,
|
|
||||||
yoone6Quantity: data?.yoone6Quantity || 0,
|
|
||||||
yoone9Quantity: data?.yoone9Quantity || 0,
|
|
||||||
yoone12Quantity: data?.yoone12Quantity || 0,
|
|
||||||
yoone12QuantityNew: data?.yoone12QuantityNew || 0,
|
|
||||||
yoone15Quantity: data?.yoone15Quantity || 0,
|
|
||||||
yoone18Quantity: data?.yoone18Quantity || 0,
|
|
||||||
zexQuantity: data?.zexQuantity || 0,
|
|
||||||
});
|
|
||||||
return {
|
|
||||||
total: data?.total || 0,
|
|
||||||
data: data?.items || [],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
setTotal(0);
|
|
||||||
setYooneTotal({});
|
|
||||||
return {
|
|
||||||
data: [],
|
|
||||||
};
|
|
||||||
}}
|
|
||||||
columns={columns}
|
|
||||||
dateFormatter="number"
|
|
||||||
footer={() => `总计: ${total}`}
|
|
||||||
toolBarRender={() => [
|
|
||||||
<Button
|
|
||||||
type="primary"
|
|
||||||
onClick={async () => {
|
|
||||||
const { dateRange, param } = formRef.current?.getFieldsValue();
|
|
||||||
const [startDate, endDate] = dateRange.values();
|
|
||||||
const { data, success } = await ordercontrollerGetordersales({
|
|
||||||
startDate: dayjs(startDate).valueOf(),
|
|
||||||
endDate: dayjs(endDate).valueOf(),
|
|
||||||
...param,
|
|
||||||
current: 1,
|
|
||||||
pageSize: 20000,
|
|
||||||
});
|
|
||||||
if (!success) return;
|
|
||||||
// 表头
|
|
||||||
const headers = ['产品名', '数量'];
|
|
||||||
|
|
||||||
// 数据行
|
|
||||||
const rows = (data?.items || []).map((item) => {
|
|
||||||
return [item.name, item.totalQuantity];
|
|
||||||
});
|
|
||||||
|
|
||||||
// 导出
|
|
||||||
const sheet = XLSX.utils.aoa_to_sheet([headers, ...rows]);
|
|
||||||
const book = XLSX.utils.book_new();
|
|
||||||
XLSX.utils.book_append_sheet(book, sheet, '销售');
|
|
||||||
const buffer = XLSX.write(book, {
|
|
||||||
bookType: 'xlsx',
|
|
||||||
type: 'array',
|
|
||||||
});
|
|
||||||
const blob = new Blob([buffer], {
|
|
||||||
type: 'application/octet-stream',
|
|
||||||
});
|
|
||||||
saveAs(blob, '销售.xlsx');
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
导出
|
|
||||||
</Button>,
|
|
||||||
<ProFormSwitch
|
|
||||||
label="原产品"
|
|
||||||
fieldProps={{
|
|
||||||
value: isSource,
|
|
||||||
onChange: () => setIsSource(!isSource),
|
|
||||||
}}
|
|
||||||
/>,
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
background: '#fff',
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'row',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
padding: '10px',
|
|
||||||
marginTop: '20px',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
YOONE:{' '}
|
|
||||||
{(yooneTotal.yoone3Quantity || 0) +
|
|
||||||
(yooneTotal.yoone6Quantity || 0) +
|
|
||||||
(yooneTotal.yoone9Quantity || 0) +
|
|
||||||
(yooneTotal.yoone12Quantity || 0) +
|
|
||||||
(yooneTotal.yoone15Quantity || 0) +
|
|
||||||
(yooneTotal.yoone18Quantity || 0) +
|
|
||||||
(yooneTotal.zexQuantity || 0)}
|
|
||||||
</div>
|
|
||||||
<div>YOONE 3MG: {yooneTotal.yoone3Quantity || 0}</div>
|
|
||||||
<div>YOONE 6MG: {yooneTotal.yoone6Quantity || 0}</div>
|
|
||||||
<div>YOONE 9MG: {yooneTotal.yoone9Quantity || 0}</div>
|
|
||||||
<div>YOONE 12MG新: {yooneTotal.yoone12QuantityNew || 0}</div>
|
|
||||||
<div>
|
|
||||||
YOONE 12MG白:{' '}
|
|
||||||
{(yooneTotal.yoone12Quantity || 0) -
|
|
||||||
(yooneTotal.yoone12QuantityNew || 0)}
|
|
||||||
</div>
|
|
||||||
<div>YOONE 15MG: {yooneTotal.yoone15Quantity || 0}</div>
|
|
||||||
<div>YOONE 18MG: {yooneTotal.yoone18Quantity || 0}</div>
|
|
||||||
<div>ZEX: {yooneTotal.zexQuantity || 0}</div>
|
|
||||||
</div>
|
|
||||||
</PageContainer>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ListPage;
|
|
||||||
|
|
@ -41,10 +41,6 @@ const ListPage: React.FC = () => {
|
||||||
valueType: 'switch',
|
valueType: 'switch',
|
||||||
hideInTable: true,
|
hideInTable: true,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
title: '产品名称',
|
|
||||||
dataIndex: 'sku',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
title: '产品名称',
|
title: '产品名称',
|
||||||
dataIndex: 'name',
|
dataIndex: 'name',
|
||||||
|
|
@ -56,7 +52,7 @@ const ListPage: React.FC = () => {
|
||||||
request: async () => {
|
request: async () => {
|
||||||
const { data = [] } = await sitecontrollerAll();
|
const { data = [] } = await sitecontrollerAll();
|
||||||
return data.map((item) => ({
|
return data.map((item) => ({
|
||||||
label: item.name,
|
label: item.siteName,
|
||||||
value: item.id,
|
value: item.id,
|
||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
|
|
@ -77,31 +73,37 @@ const ListPage: React.FC = () => {
|
||||||
title: '一单订单数',
|
title: '一单订单数',
|
||||||
dataIndex: 'firstOrderCount',
|
dataIndex: 'firstOrderCount',
|
||||||
hideInSearch: true,
|
hideInSearch: true,
|
||||||
|
render(_, record) {
|
||||||
|
if (isSource) return record.firstOrderCount;
|
||||||
|
return `${record.firstOrderCount}(${record.firstOrderYOONEBoxCount})`;
|
||||||
},
|
},
|
||||||
{
|
|
||||||
title: '一单YOONE盒数',
|
|
||||||
dataIndex: 'firstOrderYOONEBoxCount',
|
|
||||||
hideInSearch: true,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '两单订单数',
|
title: '两单订单数',
|
||||||
dataIndex: 'secondOrderCount',
|
dataIndex: 'secondOrderCount',
|
||||||
hideInSearch: true,
|
hideInSearch: true,
|
||||||
|
render(_, record) {
|
||||||
|
if (isSource) return record.secondOrderCount;
|
||||||
|
return `${record.secondOrderCount}(${record.secondOrderYOONEBoxCount})`;
|
||||||
},
|
},
|
||||||
{
|
|
||||||
title: '两单YOONE盒数',
|
|
||||||
dataIndex: 'secondOrderYOONEBoxCount',
|
|
||||||
hideInSearch: true,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '三单订单数',
|
title: '三单订单数',
|
||||||
dataIndex: 'thirdOrderCount',
|
dataIndex: 'thirdOrderCount',
|
||||||
hideInSearch: true,
|
hideInSearch: true,
|
||||||
|
render(_, record) {
|
||||||
|
if (isSource) return record.thirdOrderCount;
|
||||||
|
return `${record.thirdOrderCount}(${record.thirdOrderYOONEBoxCount})`;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '三单YOONE盒数',
|
title: '三单以上订单数',
|
||||||
dataIndex: 'thirdOrderYOONEBoxCount',
|
dataIndex: 'moreThirdOrderCount',
|
||||||
hideInSearch: true,
|
hideInSearch: true,
|
||||||
|
render(_, record) {
|
||||||
|
if (isSource) return record.moreThirdOrderCount;
|
||||||
|
return `${record.moreThirdOrderCount}(${record.moreThirdOrderYOONEBoxCount})`;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '订单数',
|
title: '订单数',
|
||||||
|
|
@ -221,17 +223,14 @@ const ListPage: React.FC = () => {
|
||||||
(yooneTotal.yoone12Quantity || 0) +
|
(yooneTotal.yoone12Quantity || 0) +
|
||||||
(yooneTotal.yoone15Quantity || 0) +
|
(yooneTotal.yoone15Quantity || 0) +
|
||||||
(yooneTotal.yoone18Quantity || 0) +
|
(yooneTotal.yoone18Quantity || 0) +
|
||||||
(yooneTotal.zexQuantity || 0)}
|
(yooneTotal.zexQuantity || 0)
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
<div>YOONE 3MG: {yooneTotal.yoone3Quantity || 0}</div>
|
<div>YOONE 3MG: {yooneTotal.yoone3Quantity || 0}</div>
|
||||||
<div>YOONE 6MG: {yooneTotal.yoone6Quantity || 0}</div>
|
<div>YOONE 6MG: {yooneTotal.yoone6Quantity || 0}</div>
|
||||||
<div>YOONE 9MG: {yooneTotal.yoone9Quantity || 0}</div>
|
<div>YOONE 9MG: {yooneTotal.yoone9Quantity || 0}</div>
|
||||||
<div>YOONE 12MG新: {yooneTotal.yoone12QuantityNew || 0}</div>
|
<div>YOONE 12MG新: {yooneTotal.yoone12QuantityNew || 0}</div>
|
||||||
<div>
|
<div>YOONE 12MG白: {(yooneTotal.yoone12Quantity || 0) - (yooneTotal.yoone12QuantityNew || 0)}</div>
|
||||||
YOONE 12MG白:{' '}
|
|
||||||
{(yooneTotal.yoone12Quantity || 0) -
|
|
||||||
(yooneTotal.yoone12QuantityNew || 0)}
|
|
||||||
</div>
|
|
||||||
<div>YOONE 15MG: {yooneTotal.yoone15Quantity || 0}</div>
|
<div>YOONE 15MG: {yooneTotal.yoone15Quantity || 0}</div>
|
||||||
<div>YOONE 18MG: {yooneTotal.yoone18Quantity || 0}</div>
|
<div>YOONE 18MG: {yooneTotal.yoone18Quantity || 0}</div>
|
||||||
<div>ZEX: {yooneTotal.zexQuantity || 0}</div>
|
<div>ZEX: {yooneTotal.zexQuantity || 0}</div>
|
||||||
|
|
|
||||||
|
|
@ -23,31 +23,27 @@ const ListPage: React.FC = () => {
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
const columns: ProColumns<API.StockDTO>[] = [
|
const columns: ProColumns<API.StockDTO>[] = [
|
||||||
{
|
|
||||||
title: 'SKU',
|
|
||||||
dataIndex: 'sku',
|
|
||||||
hideInSearch: true,
|
|
||||||
sorter: true,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
title: '产品名称',
|
title: '产品名称',
|
||||||
dataIndex: 'name',
|
dataIndex: 'productName',
|
||||||
sorter: true,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '中文名',
|
title: '中文名',
|
||||||
dataIndex: 'nameCn',
|
dataIndex: 'productNameCn',
|
||||||
|
hideInSearch: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'SKU',
|
||||||
|
dataIndex: 'productSku',
|
||||||
hideInSearch: true,
|
hideInSearch: true,
|
||||||
},
|
},
|
||||||
|
|
||||||
...points?.map((point: API.StockPoint) => ({
|
...points?.map((point: API.StockPoint) => ({
|
||||||
title: point.name,
|
title: point.name,
|
||||||
dataIndex: `point_${point.id}`,
|
dataIndex: `point_${point.name}`,
|
||||||
hideInSearch: true,
|
hideInSearch: true,
|
||||||
sorter: true,
|
|
||||||
render(_: any, record: API.StockDTO) {
|
render(_: any, record: API.StockDTO) {
|
||||||
const quantity = record.stockPoint?.find(
|
const quantity = record.stockPoint?.find(
|
||||||
(item: any) => item.id === point.id,
|
(item) => item.id === point.id,
|
||||||
)?.quantity;
|
)?.quantity;
|
||||||
return quantity || 0;
|
return quantity || 0;
|
||||||
},
|
},
|
||||||
|
|
@ -78,25 +74,8 @@ const ListPage: React.FC = () => {
|
||||||
actionRef={actionRef}
|
actionRef={actionRef}
|
||||||
rowKey="id"
|
rowKey="id"
|
||||||
request={async (params) => {
|
request={async (params) => {
|
||||||
const { sorter, ...rest } = params;
|
const { data, success } = await stockcontrollerGetstocks(params);
|
||||||
const queryParams: any = { ...rest };
|
|
||||||
|
|
||||||
if (sorter) {
|
|
||||||
const order: Record<string, 'asc' | 'desc'> = {};
|
|
||||||
for (const key in sorter) {
|
|
||||||
const value = sorter[key];
|
|
||||||
if (value === 'ascend') {
|
|
||||||
order[key] = 'asc';
|
|
||||||
} else if (value === 'descend') {
|
|
||||||
order[key] = 'desc';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (Object.keys(order).length > 0) {
|
|
||||||
queryParams.order = order;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const { data, success } = await stockcontrollerGetstocks(queryParams);
|
|
||||||
return {
|
return {
|
||||||
total: data?.total || 0,
|
total: data?.total || 0,
|
||||||
data: data?.items || [],
|
data: data?.items || [],
|
||||||
|
|
@ -117,18 +96,12 @@ const ListPage: React.FC = () => {
|
||||||
const headers = ['产品名', 'SKU', ...points.map((p) => p.name)];
|
const headers = ['产品名', 'SKU', ...points.map((p) => p.name)];
|
||||||
|
|
||||||
// 数据行
|
// 数据行
|
||||||
const rows = (data?.items || []).map((item: API.StockDTO) => {
|
const rows = (data?.items || []).map((item) => {
|
||||||
// 处理stockPoint可能为undefined的情况,并正确定义类型
|
const stockMap = new Map(
|
||||||
const stockMap = new Map<number, number>(
|
item.stockPoint.map((sp) => [sp.id, sp.quantity]),
|
||||||
(item.stockPoint || []).map((sp: any) => [
|
|
||||||
sp.id || 0,
|
|
||||||
sp.quantity || 0,
|
|
||||||
]),
|
|
||||||
);
|
);
|
||||||
const stockRow = points.map(
|
const stockRow = points.map((p) => stockMap.get(p.id) || 0);
|
||||||
(p) => stockMap.get(p.id || 0) || 0,
|
return [item.productName, item.productSku, ...stockRow];
|
||||||
);
|
|
||||||
return [item.productName || '', item.sku || '', ...stockRow];
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// 导出
|
// 导出
|
||||||
|
|
|
||||||
|
|
@ -94,7 +94,7 @@ const PurchaseOrderPage: React.FC = () => {
|
||||||
<Divider type="vertical" />
|
<Divider type="vertical" />
|
||||||
<Popconfirm
|
<Popconfirm
|
||||||
title="删除"
|
title="删除"
|
||||||
description="确认删除?"
|
description="确认删除?"
|
||||||
onConfirm={async () => {
|
onConfirm={async () => {
|
||||||
try {
|
try {
|
||||||
const { success, message: errMsg } =
|
const { success, message: errMsg } =
|
||||||
|
|
@ -120,7 +120,7 @@ const PurchaseOrderPage: React.FC = () => {
|
||||||
<Divider type="vertical" />
|
<Divider type="vertical" />
|
||||||
<Popconfirm
|
<Popconfirm
|
||||||
title="入库"
|
title="入库"
|
||||||
description="确认已到达?"
|
description="确认已到达?"
|
||||||
onConfirm={async () => {
|
onConfirm={async () => {
|
||||||
try {
|
try {
|
||||||
const { success, message: errMsg } =
|
const { success, message: errMsg } =
|
||||||
|
|
@ -285,7 +285,7 @@ const CreateForm: React.FC<{
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
name="sku"
|
name="productSku"
|
||||||
label={'产品' + (idx + 1)}
|
label={'产品' + (idx + 1)}
|
||||||
width="lg"
|
width="lg"
|
||||||
placeholder="请选择产品"
|
placeholder="请选择产品"
|
||||||
|
|
@ -297,7 +297,7 @@ const CreateForm: React.FC<{
|
||||||
transform={(value) => {
|
transform={(value) => {
|
||||||
return value?.value || value;
|
return value?.value || value;
|
||||||
}}
|
}}
|
||||||
debounceTime={300} // 防抖,减少请求频率
|
debounceTime={300} // 防抖,减少请求频率
|
||||||
rules={[{ required: true, message: '请选择产品' }]}
|
rules={[{ required: true, message: '请选择产品' }]}
|
||||||
onChange={(_, option) => {
|
onChange={(_, option) => {
|
||||||
form.setFieldValue(
|
form.setFieldValue(
|
||||||
|
|
@ -347,9 +347,9 @@ const UpdateForm: React.FC<{
|
||||||
...values,
|
...values,
|
||||||
items: values?.items?.map((item: API.PurchaseOrderItem) => ({
|
items: values?.items?.map((item: API.PurchaseOrderItem) => ({
|
||||||
...item,
|
...item,
|
||||||
sku: {
|
productSku: {
|
||||||
label: item.productName,
|
label: item.productName,
|
||||||
value: item.sku,
|
value: item.productSku,
|
||||||
},
|
},
|
||||||
})),
|
})),
|
||||||
};
|
};
|
||||||
|
|
@ -427,7 +427,9 @@ const UpdateForm: React.FC<{
|
||||||
<ProFormTextArea label="备注" name="note" width={'lg'} />
|
<ProFormTextArea label="备注" name="note" width={'lg'} />
|
||||||
<ProFormDependency name={['items']}>
|
<ProFormDependency name={['items']}>
|
||||||
{({ items }) => {
|
{({ items }) => {
|
||||||
return '数量:' + items?.reduce((acc, cur) => acc + cur.quantity, 0);
|
return (
|
||||||
|
'数量:' + items?.reduce((acc, cur) => acc + cur.quantity, 0)
|
||||||
|
);
|
||||||
}}
|
}}
|
||||||
</ProFormDependency>
|
</ProFormDependency>
|
||||||
<ProFormList<API.PurchaseOrderItem>
|
<ProFormList<API.PurchaseOrderItem>
|
||||||
|
|
@ -466,7 +468,7 @@ const UpdateForm: React.FC<{
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
name="sku"
|
name="productSku"
|
||||||
label="产品"
|
label="产品"
|
||||||
width="lg"
|
width="lg"
|
||||||
placeholder="请选择产品"
|
placeholder="请选择产品"
|
||||||
|
|
@ -478,7 +480,7 @@ const UpdateForm: React.FC<{
|
||||||
transform={(value) => {
|
transform={(value) => {
|
||||||
return value?.value || value;
|
return value?.value || value;
|
||||||
}}
|
}}
|
||||||
debounceTime={300} // 防抖,减少请求频率
|
debounceTime={300} // 防抖,减少请求频率
|
||||||
rules={[{ required: true, message: '请选择产品' }]}
|
rules={[{ required: true, message: '请选择产品' }]}
|
||||||
onChange={(_, option) => {
|
onChange={(_, option) => {
|
||||||
form.setFieldValue(
|
form.setFieldValue(
|
||||||
|
|
@ -526,7 +528,16 @@ const DetailForm: React.FC<{
|
||||||
const detailsActionRef = useRef<ActionType>();
|
const detailsActionRef = useRef<ActionType>();
|
||||||
const { message } = App.useApp();
|
const { message } = App.useApp();
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
const initialValues = values;
|
const initialValues = {
|
||||||
|
...values,
|
||||||
|
items: values?.items?.map((item: API.PurchaseOrderItem) => ({
|
||||||
|
...item,
|
||||||
|
productSku: {
|
||||||
|
label: item.productName,
|
||||||
|
value: item.productSku,
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
};
|
||||||
return (
|
return (
|
||||||
<DrawerForm<API.UpdatePurchaseOrderDTO>
|
<DrawerForm<API.UpdatePurchaseOrderDTO>
|
||||||
title="详情"
|
title="详情"
|
||||||
|
|
@ -585,30 +596,87 @@ const DetailForm: React.FC<{
|
||||||
rules={[{ required: true, message: '请选择预计到货时间' }]}
|
rules={[{ required: true, message: '请选择预计到货时间' }]}
|
||||||
/>
|
/>
|
||||||
<ProFormTextArea label="备注" name="note" width={'lg'} />
|
<ProFormTextArea label="备注" name="note" width={'lg'} />
|
||||||
<ProTable<API.PurchaseOrderItem>
|
<ProFormList<API.PurchaseOrderItem>
|
||||||
columns={[
|
name="items"
|
||||||
|
rules={[
|
||||||
{
|
{
|
||||||
title: '产品',
|
required: true,
|
||||||
dataIndex: 'productName',
|
message: '至少需要一个商品',
|
||||||
},
|
validator: (_, value) =>
|
||||||
{
|
value && value.length > 0
|
||||||
title: '数量',
|
? Promise.resolve()
|
||||||
dataIndex: 'quantity',
|
: Promise.reject('至少需要一个商品'),
|
||||||
valueType: 'digit',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '价格',
|
|
||||||
dataIndex: 'price',
|
|
||||||
valueType: 'money',
|
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
dataSource={values.items || []}
|
creatorButtonProps={{ children: '新增', size: 'large' }}
|
||||||
rowKey="sku"
|
wrapperCol={{ span: 24 }}
|
||||||
pagination={false}
|
>
|
||||||
search={false}
|
{(fields, idx, { remove }) => (
|
||||||
options={false}
|
<div key={idx}>
|
||||||
toolBarRender={false}
|
<ProForm.Group>
|
||||||
|
<ProFormSelect
|
||||||
|
request={async ({ keyWords }) => {
|
||||||
|
if (keyWords.length < 2) return [];
|
||||||
|
try {
|
||||||
|
const { data } = await productcontrollerSearchproducts({
|
||||||
|
name: keyWords,
|
||||||
|
});
|
||||||
|
return (
|
||||||
|
data?.map((item) => {
|
||||||
|
return {
|
||||||
|
label: `${item.name} - ${item.nameCn}`,
|
||||||
|
value: item.sku,
|
||||||
|
};
|
||||||
|
}) || []
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
name="productSku"
|
||||||
|
label="产品"
|
||||||
|
width="lg"
|
||||||
|
placeholder="请选择产品"
|
||||||
|
tooltip="至少输入3个字符"
|
||||||
|
fieldProps={{
|
||||||
|
showSearch: true,
|
||||||
|
filterOption: false,
|
||||||
|
}}
|
||||||
|
transform={(value) => {
|
||||||
|
return value?.value || value;
|
||||||
|
}}
|
||||||
|
debounceTime={300} // 防抖,减少请求频率
|
||||||
|
rules={[{ required: true, message: '请选择产品' }]}
|
||||||
|
onChange={(_, option) => {
|
||||||
|
form.setFieldValue(
|
||||||
|
['items', fields.key, 'productName'],
|
||||||
|
option?.title,
|
||||||
|
);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
|
<ProFormText name="productName" label="产品名称" hidden={true} />
|
||||||
|
<ProFormDigit
|
||||||
|
name="quantity"
|
||||||
|
label="数量"
|
||||||
|
placeholder="请输入数量"
|
||||||
|
rules={[{ required: true, message: '请输入数量' }]}
|
||||||
|
fieldProps={{
|
||||||
|
precision: 0,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<ProFormDigit
|
||||||
|
name="price"
|
||||||
|
label="价格"
|
||||||
|
placeholder="请输入价格"
|
||||||
|
rules={[{ required: true, message: '请输入价格' }]}
|
||||||
|
fieldProps={{
|
||||||
|
precision: 2,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</ProForm.Group>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</ProFormList>
|
||||||
</DrawerForm>
|
</DrawerForm>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ const ListPage: React.FC = () => {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'SKU',
|
title: 'SKU',
|
||||||
dataIndex: 'sku',
|
dataIndex: 'productSku',
|
||||||
hideInSearch: true,
|
hideInSearch: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -31,12 +31,12 @@ const ListPage: React.FC = () => {
|
||||||
dataIndex: 'operationType',
|
dataIndex: 'operationType',
|
||||||
valueType: 'select',
|
valueType: 'select',
|
||||||
valueEnum: {
|
valueEnum: {
|
||||||
in: {
|
'in': {
|
||||||
text: '入库',
|
text: '入库'
|
||||||
},
|
|
||||||
out: {
|
|
||||||
text: '出库',
|
|
||||||
},
|
},
|
||||||
|
"out": {
|
||||||
|
text: '出库'
|
||||||
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -95,7 +95,7 @@ const TransferPage: React.FC = () => {
|
||||||
<Divider type="vertical" />
|
<Divider type="vertical" />
|
||||||
<Popconfirm
|
<Popconfirm
|
||||||
title="入库"
|
title="入库"
|
||||||
description="确认已到达?"
|
description="确认已到达?"
|
||||||
onConfirm={async () => {
|
onConfirm={async () => {
|
||||||
try {
|
try {
|
||||||
const { success, message: errMsg } =
|
const { success, message: errMsg } =
|
||||||
|
|
@ -116,7 +116,7 @@ const TransferPage: React.FC = () => {
|
||||||
<Divider type="vertical" />
|
<Divider type="vertical" />
|
||||||
<Popconfirm
|
<Popconfirm
|
||||||
title="丢失"
|
title="丢失"
|
||||||
description="确认该批货已丢失?"
|
description="确认该批货已丢失?"
|
||||||
onConfirm={async () => {
|
onConfirm={async () => {
|
||||||
try {
|
try {
|
||||||
const { success, message: errMsg } =
|
const { success, message: errMsg } =
|
||||||
|
|
@ -137,7 +137,7 @@ const TransferPage: React.FC = () => {
|
||||||
<Divider type="vertical" />
|
<Divider type="vertical" />
|
||||||
<Popconfirm
|
<Popconfirm
|
||||||
title="取消"
|
title="取消"
|
||||||
description="确认取消?"
|
description="确认取消?"
|
||||||
onConfirm={async () => {
|
onConfirm={async () => {
|
||||||
try {
|
try {
|
||||||
const { success, message: errMsg } =
|
const { success, message: errMsg } =
|
||||||
|
|
@ -207,7 +207,7 @@ const CreateForm: React.FC<{
|
||||||
drawerProps={{
|
drawerProps={{
|
||||||
destroyOnHidden: true,
|
destroyOnHidden: true,
|
||||||
}}
|
}}
|
||||||
onFinish={async ({ orderNumber, ...values }) => {
|
onFinish={async ({orderNumber,...values}) => {
|
||||||
try {
|
try {
|
||||||
const { success, message: errMsg } =
|
const { success, message: errMsg } =
|
||||||
await stockcontrollerCreatetransfer(values);
|
await stockcontrollerCreatetransfer(values);
|
||||||
|
|
@ -272,38 +272,24 @@ const CreateForm: React.FC<{
|
||||||
rules={[{ required: true, message: '请选择源目标仓库' }]}
|
rules={[{ required: true, message: '请选择源目标仓库' }]}
|
||||||
/>
|
/>
|
||||||
<ProFormTextArea name="note" label="备注" />
|
<ProFormTextArea name="note" label="备注" />
|
||||||
<ProFormText
|
<ProFormText name={'orderNumber'} addonAfter={<Button onClick={async () => {
|
||||||
name={'orderNumber'}
|
const orderNumber = await form.getFieldValue('orderNumber')
|
||||||
addonAfter={
|
const { data } = await stockcontrollerGetpurchaseorder({orderNumber})
|
||||||
<Button
|
|
||||||
onClick={async () => {
|
|
||||||
const orderNumber = await form.getFieldValue('orderNumber');
|
|
||||||
const { data } = await stockcontrollerGetpurchaseorder({
|
|
||||||
orderNumber,
|
|
||||||
});
|
|
||||||
form.setFieldsValue({
|
form.setFieldsValue({
|
||||||
items: data?.map(
|
items: data?.map(
|
||||||
(item: { productName: string; sku: string }) => ({
|
(item: { productName: string; productSku: string }) => ({
|
||||||
...item,
|
...item,
|
||||||
sku: {
|
productSku: {
|
||||||
label: item.productName,
|
label: item.productName,
|
||||||
value: item.sku,
|
value: item.productSku,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
});
|
})
|
||||||
}}
|
}}>引用</Button>} />
|
||||||
>
|
|
||||||
引用
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<ProFormDependency name={['items']}>
|
<ProFormDependency name={['items']}>
|
||||||
{({ items }) => {
|
{({ items }) => {
|
||||||
return (
|
return '数量:' + (items?.reduce?.((acc, cur) => acc + cur.quantity, 0)||0);
|
||||||
'数量:' +
|
|
||||||
(items?.reduce?.((acc, cur) => acc + cur.quantity, 0) || 0)
|
|
||||||
);
|
|
||||||
}}
|
}}
|
||||||
</ProFormDependency>
|
</ProFormDependency>
|
||||||
<ProFormList
|
<ProFormList
|
||||||
|
|
@ -343,7 +329,7 @@ const CreateForm: React.FC<{
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
name="sku"
|
name="productSku"
|
||||||
label={'产品' + (idx + 1)}
|
label={'产品' + (idx + 1)}
|
||||||
width="lg"
|
width="lg"
|
||||||
placeholder="请选择产品"
|
placeholder="请选择产品"
|
||||||
|
|
@ -354,7 +340,7 @@ const CreateForm: React.FC<{
|
||||||
transform={(value) => {
|
transform={(value) => {
|
||||||
return value?.value || value;
|
return value?.value || value;
|
||||||
}}
|
}}
|
||||||
debounceTime={300} // 防抖,减少请求频率
|
debounceTime={300} // 防抖,减少请求频率
|
||||||
rules={[{ required: true, message: '请选择产品' }]}
|
rules={[{ required: true, message: '请选择产品' }]}
|
||||||
onChange={(_, option) => {
|
onChange={(_, option) => {
|
||||||
form.setFieldValue(
|
form.setFieldValue(
|
||||||
|
|
@ -392,9 +378,9 @@ const UpdateForm: React.FC<{
|
||||||
...values,
|
...values,
|
||||||
items: values?.items?.map((item: API.PurchaseOrderItem) => ({
|
items: values?.items?.map((item: API.PurchaseOrderItem) => ({
|
||||||
...item,
|
...item,
|
||||||
sku: {
|
productSku: {
|
||||||
label: item.productName,
|
label: item.productName,
|
||||||
value: item.sku,
|
value: item.productSku,
|
||||||
},
|
},
|
||||||
})),
|
})),
|
||||||
};
|
};
|
||||||
|
|
@ -519,7 +505,7 @@ const UpdateForm: React.FC<{
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
name="sku"
|
name="productSku"
|
||||||
label={'产品' + (idx + 1)}
|
label={'产品' + (idx + 1)}
|
||||||
width="lg"
|
width="lg"
|
||||||
placeholder="请选择产品"
|
placeholder="请选择产品"
|
||||||
|
|
@ -530,7 +516,7 @@ const UpdateForm: React.FC<{
|
||||||
transform={(value) => {
|
transform={(value) => {
|
||||||
return value?.value || value;
|
return value?.value || value;
|
||||||
}}
|
}}
|
||||||
debounceTime={300} // 防抖,减少请求频率
|
debounceTime={300} // 防抖,减少请求频率
|
||||||
rules={[{ required: true, message: '请选择产品' }]}
|
rules={[{ required: true, message: '请选择产品' }]}
|
||||||
onChange={(_, option) => {
|
onChange={(_, option) => {
|
||||||
form.setFieldValue(
|
form.setFieldValue(
|
||||||
|
|
@ -564,13 +550,15 @@ const DetailForm: React.FC<{
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
const initialValues = {
|
const initialValues = {
|
||||||
...values,
|
...values,
|
||||||
items: values?.items?.map((item: { productName: string; sku: string }) => ({
|
items: values?.items?.map(
|
||||||
|
(item: { productName: string; productSku: string }) => ({
|
||||||
...item,
|
...item,
|
||||||
sku: {
|
productSku: {
|
||||||
label: item.productName,
|
label: item.productName,
|
||||||
value: item.sku,
|
value: item.productSku,
|
||||||
},
|
},
|
||||||
})),
|
}),
|
||||||
|
),
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<DrawerForm
|
<DrawerForm
|
||||||
|
|
@ -674,7 +662,7 @@ const DetailForm: React.FC<{
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
name="sku"
|
name="productSku"
|
||||||
label="产品"
|
label="产品"
|
||||||
width="lg"
|
width="lg"
|
||||||
placeholder="请选择产品"
|
placeholder="请选择产品"
|
||||||
|
|
@ -685,7 +673,7 @@ const DetailForm: React.FC<{
|
||||||
transform={(value) => {
|
transform={(value) => {
|
||||||
return value?.value || value;
|
return value?.value || value;
|
||||||
}}
|
}}
|
||||||
debounceTime={300} // 防抖,减少请求频率
|
debounceTime={300} // 防抖,减少请求频率
|
||||||
rules={[{ required: true, message: '请选择产品' }]}
|
rules={[{ required: true, message: '请选择产品' }]}
|
||||||
onChange={(_, option) => {
|
onChange={(_, option) => {
|
||||||
form.setFieldValue(
|
form.setFieldValue(
|
||||||
|
|
|
||||||
|
|
@ -11,35 +11,12 @@ import {
|
||||||
PageContainer,
|
PageContainer,
|
||||||
ProColumns,
|
ProColumns,
|
||||||
ProForm,
|
ProForm,
|
||||||
ProFormSelect,
|
|
||||||
ProFormText,
|
ProFormText,
|
||||||
ProTable,
|
ProTable,
|
||||||
} from '@ant-design/pro-components';
|
} from '@ant-design/pro-components';
|
||||||
import { App, Button, Divider, Popconfirm, Space, Tag } from 'antd';
|
import { App, Button, Divider, Popconfirm } from 'antd';
|
||||||
import * as countries from 'i18n-iso-countries';
|
|
||||||
import zhCN from 'i18n-iso-countries/langs/zh';
|
|
||||||
import { useRef } from 'react';
|
import { useRef } from 'react';
|
||||||
|
|
||||||
// 初始化中文语言包
|
|
||||||
countries.registerLocale(zhCN);
|
|
||||||
|
|
||||||
// 区域数据项类型
|
|
||||||
interface AreaItem {
|
|
||||||
code: 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>();
|
||||||
|
|
@ -60,22 +37,6 @@ const ListPage: React.FC = () => {
|
||||||
title: '联系电话',
|
title: '联系电话',
|
||||||
dataIndex: 'contactPhone',
|
dataIndex: 'contactPhone',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
title: '区域',
|
|
||||||
dataIndex: 'areas',
|
|
||||||
render: (_, record: any) => {
|
|
||||||
if (!record.areas || record.areas.length === 0) {
|
|
||||||
return <Tag color="blue">全球</Tag>;
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<Space wrap>
|
|
||||||
{record.areas.map((area: any) => (
|
|
||||||
<Tag key={area.code}>{area.name}</Tag>
|
|
||||||
))}
|
|
||||||
</Space>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
title: '创建时间',
|
title: '创建时间',
|
||||||
dataIndex: 'createdAt',
|
dataIndex: 'createdAt',
|
||||||
|
|
@ -91,7 +52,7 @@ const ListPage: React.FC = () => {
|
||||||
<Divider type="vertical" />
|
<Divider type="vertical" />
|
||||||
<Popconfirm
|
<Popconfirm
|
||||||
title="删除"
|
title="删除"
|
||||||
description="确认删除?"
|
description="确认删除?"
|
||||||
onConfirm={async () => {
|
onConfirm={async () => {
|
||||||
try {
|
try {
|
||||||
const { success, message: errMsg } =
|
const { success, message: errMsg } =
|
||||||
|
|
@ -199,18 +160,6 @@ const CreateForm: React.FC<{
|
||||||
placeholder="请输入联系电话"
|
placeholder="请输入联系电话"
|
||||||
rules={[{ required: true, message: '请输入联系电话' }]}
|
rules={[{ required: true, message: '请输入联系电话' }]}
|
||||||
/>
|
/>
|
||||||
<ProFormSelect
|
|
||||||
name="areas"
|
|
||||||
label="区域"
|
|
||||||
width="lg"
|
|
||||||
mode="multiple"
|
|
||||||
placeholder="留空表示全球"
|
|
||||||
showSearch
|
|
||||||
filterOption={(input, option) =>
|
|
||||||
(option?.label ?? '').toLowerCase().includes(input.toLowerCase())
|
|
||||||
}
|
|
||||||
options={getCountryOptions()}
|
|
||||||
/>
|
|
||||||
</ProForm.Group>
|
</ProForm.Group>
|
||||||
</DrawerForm>
|
</DrawerForm>
|
||||||
);
|
);
|
||||||
|
|
@ -224,11 +173,7 @@ const UpdateForm: React.FC<{
|
||||||
return (
|
return (
|
||||||
<DrawerForm<API.UpdateStockPointDTO>
|
<DrawerForm<API.UpdateStockPointDTO>
|
||||||
title="编辑"
|
title="编辑"
|
||||||
initialValues={{
|
initialValues={initialValues}
|
||||||
...initialValues,
|
|
||||||
areas:
|
|
||||||
(initialValues as any).areas?.map((area: any) => area.code) ?? [],
|
|
||||||
}}
|
|
||||||
trigger={
|
trigger={
|
||||||
<Button type="primary">
|
<Button type="primary">
|
||||||
<EditOutlined />
|
<EditOutlined />
|
||||||
|
|
@ -286,18 +231,6 @@ const UpdateForm: React.FC<{
|
||||||
placeholder="请输入联系电话"
|
placeholder="请输入联系电话"
|
||||||
rules={[{ required: true, message: '请输入联系电话' }]}
|
rules={[{ required: true, message: '请输入联系电话' }]}
|
||||||
/>
|
/>
|
||||||
<ProFormSelect
|
|
||||||
name="areas"
|
|
||||||
label="区域"
|
|
||||||
width="lg"
|
|
||||||
mode="multiple"
|
|
||||||
placeholder="留空表示全球"
|
|
||||||
showSearch
|
|
||||||
filterOption={(input, option) =>
|
|
||||||
(option?.label ?? '').toLowerCase().includes(input.toLowerCase())
|
|
||||||
}
|
|
||||||
options={getCountryOptions()}
|
|
||||||
/>
|
|
||||||
</ProForm.Group>
|
</ProForm.Group>
|
||||||
</DrawerForm>
|
</DrawerForm>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,4 @@
|
||||||
import { sitecontrollerAll } from '@/servers/api/site';
|
import React, { useRef, useState } from 'react';
|
||||||
import {
|
|
||||||
subscriptioncontrollerList,
|
|
||||||
subscriptioncontrollerSync,
|
|
||||||
} from '@/servers/api/subscription';
|
|
||||||
import {
|
import {
|
||||||
ActionType,
|
ActionType,
|
||||||
DrawerForm,
|
DrawerForm,
|
||||||
|
|
@ -11,10 +7,14 @@ import {
|
||||||
ProFormSelect,
|
ProFormSelect,
|
||||||
ProTable,
|
ProTable,
|
||||||
} from '@ant-design/pro-components';
|
} from '@ant-design/pro-components';
|
||||||
import { App, Button, Drawer, List, Tag } from 'antd';
|
import { App, Button, Tag, Drawer, List } from 'antd';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import React, { useRef, useState } from 'react';
|
|
||||||
import { request } from 'umi';
|
import { request } from 'umi';
|
||||||
|
import {
|
||||||
|
subscriptioncontrollerList,
|
||||||
|
subscriptioncontrollerSync,
|
||||||
|
} from '@/servers/api/subscription';
|
||||||
|
import { sitecontrollerAll } from '@/servers/api/site';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 订阅状态枚举(用于筛选与展示)
|
* 订阅状态枚举(用于筛选与展示)
|
||||||
|
|
@ -29,7 +29,7 @@ const SUBSCRIPTION_STATUS_ENUM: Record<string, { text: string }> = {
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 订阅列表页:展示,筛选,触发订阅同步
|
* 订阅列表页:展示、筛选、触发订阅同步
|
||||||
*/
|
*/
|
||||||
const ListPage: React.FC = () => {
|
const ListPage: React.FC = () => {
|
||||||
// 表格操作引用:用于在同步后触发表格刷新
|
// 表格操作引用:用于在同步后触发表格刷新
|
||||||
|
|
@ -51,7 +51,7 @@ const ListPage: React.FC = () => {
|
||||||
request: async () => {
|
request: async () => {
|
||||||
const { data = [] } = await sitecontrollerAll();
|
const { data = [] } = await sitecontrollerAll();
|
||||||
return data.map((item) => ({
|
return data.map((item) => ({
|
||||||
label: item.name,
|
label: item.siteName,
|
||||||
value: item.id,
|
value: item.id,
|
||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
|
|
@ -75,13 +75,9 @@ const ListPage: React.FC = () => {
|
||||||
dataIndex: 'status',
|
dataIndex: 'status',
|
||||||
valueType: 'select',
|
valueType: 'select',
|
||||||
valueEnum: SUBSCRIPTION_STATUS_ENUM,
|
valueEnum: SUBSCRIPTION_STATUS_ENUM,
|
||||||
// 以 Tag 形式展示,更易辨识
|
// 以 Tag 形式展示,更易辨识
|
||||||
render: (_, row) =>
|
render: (_, row) =>
|
||||||
row?.status ? (
|
row?.status ? <Tag>{SUBSCRIPTION_STATUS_ENUM[row.status]?.text || row.status}</Tag> : '-',
|
||||||
<Tag>{SUBSCRIPTION_STATUS_ENUM[row.status]?.text || row.status}</Tag>
|
|
||||||
) : (
|
|
||||||
'-'
|
|
||||||
),
|
|
||||||
width: 120,
|
width: 120,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -156,10 +152,10 @@ const ListPage: React.FC = () => {
|
||||||
const { success, data, message: errMsg } = resp as any;
|
const { success, data, message: errMsg } = resp as any;
|
||||||
if (!success) throw new Error(errMsg || '获取失败');
|
if (!success) throw new Error(errMsg || '获取失败');
|
||||||
// 仅保留与父订单号完全一致的订单(避免模糊匹配误入)
|
// 仅保留与父订单号完全一致的订单(避免模糊匹配误入)
|
||||||
const candidates: any[] = (
|
const candidates: any[] = (Array.isArray(data) ? data : []).filter(
|
||||||
Array.isArray(data) ? data : []
|
(c: any) => String(c?.externalOrderId) === parentNumber
|
||||||
).filter((c: any) => String(c?.externalOrderId) === parentNumber);
|
);
|
||||||
// 拉取详情,补充状态,金额,时间
|
// 拉取详情,补充状态、金额、时间
|
||||||
const details = [] as any[];
|
const details = [] as any[];
|
||||||
for (const c of candidates) {
|
for (const c of candidates) {
|
||||||
const d = await request(`/order/${c.id}`, { method: 'GET' });
|
const d = await request(`/order/${c.id}`, { method: 'GET' });
|
||||||
|
|
@ -168,7 +164,7 @@ const ListPage: React.FC = () => {
|
||||||
details.push({
|
details.push({
|
||||||
id: c.id,
|
id: c.id,
|
||||||
externalOrderId: c.externalOrderId,
|
externalOrderId: c.externalOrderId,
|
||||||
name: c.name,
|
siteName: c.siteName,
|
||||||
status: od?.status,
|
status: od?.status,
|
||||||
total: od?.total,
|
total: od?.total,
|
||||||
currency_symbol: od?.currency_symbol,
|
currency_symbol: od?.currency_symbol,
|
||||||
|
|
@ -179,7 +175,7 @@ const ListPage: React.FC = () => {
|
||||||
details.push({
|
details.push({
|
||||||
id: c.id,
|
id: c.id,
|
||||||
externalOrderId: c.externalOrderId,
|
externalOrderId: c.externalOrderId,
|
||||||
name: c.name,
|
siteName: c.siteName,
|
||||||
relationship: 'Parent Order',
|
relationship: 'Parent Order',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -205,7 +201,7 @@ const ListPage: React.FC = () => {
|
||||||
rowKey="id"
|
rowKey="id"
|
||||||
actionRef={actionRef}
|
actionRef={actionRef}
|
||||||
/**
|
/**
|
||||||
* 列表数据请求;保持与后端分页参数一致
|
* 列表数据请求;保持与后端分页参数一致
|
||||||
* 兼容后端 data.items 或 data.list 返回字段
|
* 兼容后端 data.items 或 data.list 返回字段
|
||||||
*/
|
*/
|
||||||
request={async (params) => {
|
request={async (params) => {
|
||||||
|
|
@ -220,7 +216,7 @@ const ListPage: React.FC = () => {
|
||||||
// 工具栏:订阅同步入口
|
// 工具栏:订阅同步入口
|
||||||
toolBarRender={() => [<SyncForm key="sync" tableRef={actionRef} />]}
|
toolBarRender={() => [<SyncForm key="sync" tableRef={actionRef} />]}
|
||||||
/>
|
/>
|
||||||
{/* 关联订单抽屉:展示订单号,关系,时间,状态与金额 */}
|
{/* 关联订单抽屉:展示订单号、关系、时间、状态与金额 */}
|
||||||
<Drawer
|
<Drawer
|
||||||
open={drawerOpen}
|
open={drawerOpen}
|
||||||
title={drawerTitle}
|
title={drawerTitle}
|
||||||
|
|
@ -234,22 +230,14 @@ const ListPage: React.FC = () => {
|
||||||
<List.Item>
|
<List.Item>
|
||||||
<List.Item.Meta
|
<List.Item.Meta
|
||||||
title={`#${item?.externalOrderId || '-'}`}
|
title={`#${item?.externalOrderId || '-'}`}
|
||||||
description={`关系:${item?.relationship || '-'},站点:${
|
description={`关系:${item?.relationship || '-'},站点:${item?.siteName || '-'}`}
|
||||||
item?.name || '-'
|
|
||||||
}`}
|
|
||||||
/>
|
/>
|
||||||
<div style={{ display: 'flex', gap: 12, alignItems: 'center' }}>
|
<div style={{ display: 'flex', gap: 12, alignItems: 'center' }}>
|
||||||
<span>
|
<span>{item?.date_created ? dayjs(item.date_created).format('YYYY-MM-DD HH:mm') : '-'}</span>
|
||||||
{item?.date_created
|
|
||||||
? dayjs(item.date_created).format('YYYY-MM-DD HH:mm')
|
|
||||||
: '-'}
|
|
||||||
</span>
|
|
||||||
<Tag>{item?.status || '-'}</Tag>
|
<Tag>{item?.status || '-'}</Tag>
|
||||||
<span>
|
<span>
|
||||||
{item?.currency_symbol || ''}
|
{item?.currency_symbol || ''}
|
||||||
{typeof item?.total === 'number'
|
{typeof item?.total === 'number' ? item.total.toFixed(2) : item?.total ?? '-'}
|
||||||
? item.total.toFixed(2)
|
|
||||||
: item?.total ?? '-'}
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</List.Item>
|
</List.Item>
|
||||||
|
|
@ -281,14 +269,12 @@ const SyncForm: React.FC<{
|
||||||
/**
|
/**
|
||||||
* 提交逻辑:
|
* 提交逻辑:
|
||||||
* 1. 必填校验由 ProForm + rules 保证
|
* 1. 必填校验由 ProForm + rules 保证
|
||||||
* 2. 调用同步接口,失败时友好提示
|
* 2. 调用同步接口,失败时友好提示
|
||||||
* 3. 成功后刷新列表
|
* 3. 成功后刷新列表
|
||||||
*/
|
*/
|
||||||
onFinish={async (values) => {
|
onFinish={async (values) => {
|
||||||
try {
|
try {
|
||||||
const { success, message: errMsg } = await subscriptioncontrollerSync(
|
const { success, message: errMsg } = await subscriptioncontrollerSync(values);
|
||||||
values,
|
|
||||||
);
|
|
||||||
if (!success) {
|
if (!success) {
|
||||||
throw new Error(errMsg);
|
throw new Error(errMsg);
|
||||||
}
|
}
|
||||||
|
|
@ -309,7 +295,7 @@ const SyncForm: React.FC<{
|
||||||
request={async () => {
|
request={async () => {
|
||||||
const { data = [] } = await sitecontrollerAll();
|
const { data = [] } = await sitecontrollerAll();
|
||||||
return data.map((item) => ({
|
return data.map((item) => ({
|
||||||
label: item.name,
|
label: item.siteName,
|
||||||
value: item.id,
|
value: item.id,
|
||||||
}));
|
}));
|
||||||
}}
|
}}
|
||||||
|
|
|
||||||
|
|
@ -1,24 +1,34 @@
|
||||||
import { CopyOutlined, DeleteFilled } from '@ant-design/icons';
|
import React, { useEffect, useRef, useState } from 'react';
|
||||||
|
import {
|
||||||
|
App,
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
Divider,
|
||||||
|
Drawer,
|
||||||
|
Empty,
|
||||||
|
Popconfirm,
|
||||||
|
Space,
|
||||||
|
Tag,
|
||||||
|
} from 'antd';
|
||||||
import { ActionType, ProDescriptions } from '@ant-design/pro-components';
|
import { ActionType, ProDescriptions } from '@ant-design/pro-components';
|
||||||
import { App, Button, Card, Divider, Drawer, Empty, Popconfirm } from 'antd';
|
import { CopyOutlined, DeleteFilled } from '@ant-design/icons';
|
||||||
import React, { useEffect, useRef } from 'react';
|
|
||||||
|
|
||||||
// 服务器 API 引用(保持与原 index.tsx 一致)
|
// 服务器 API 引用(保持与原 index.tsx 一致)
|
||||||
import { logisticscontrollerDelshipment } from '@/servers/api/logistics';
|
|
||||||
import {
|
import {
|
||||||
ordercontrollerChangestatus,
|
ordercontrollerChangestatus,
|
||||||
ordercontrollerGetorderdetail,
|
ordercontrollerGetorderdetail,
|
||||||
ordercontrollerSyncorderbyid,
|
ordercontrollerSyncorderbyid,
|
||||||
} from '@/servers/api/order';
|
} from '@/servers/api/order';
|
||||||
|
import { logisticscontrollerDelshipment } from '@/servers/api/logistics';
|
||||||
import { sitecontrollerAll } from '@/servers/api/site';
|
import { sitecontrollerAll } from '@/servers/api/site';
|
||||||
|
|
||||||
// 工具与子组件
|
// 工具与子组件
|
||||||
import { ORDER_STATUS_ENUM } from '@/constants';
|
|
||||||
import { formatShipmentState, formatSource } from '@/utils/format';
|
import { formatShipmentState, formatSource } from '@/utils/format';
|
||||||
import RelatedOrders from './RelatedOrders';
|
import RelatedOrders from './RelatedOrders';
|
||||||
|
import { ORDER_STATUS_ENUM } from '@/constants';
|
||||||
|
|
||||||
// 为保持原文件结构简单,此处从 index.tsx 引入的子组件仍由原文件导出或保持原状
|
// 为保持原文件结构简单,此处从 index.tsx 引入的子组件仍由原文件导出或保持原状
|
||||||
// 若后续需要彻底解耦,可将 OrderNote / Shipping / SalesChange 也独立到文件
|
// 若后续需要彻底解耦,可将 OrderNote / Shipping / SalesChange 也独立到文件
|
||||||
// 当前按你的要求仅抽离详情 Drawer
|
// 当前按你的要求仅抽离详情 Drawer
|
||||||
|
|
||||||
type OrderRecord = API.Order;
|
type OrderRecord = API.Order;
|
||||||
|
|
@ -49,18 +59,14 @@ const OrderDetailDrawer: React.FC<OrderDetailDrawerProps> = ({
|
||||||
|
|
||||||
// 加载详情数据(与 index.tsx 中完全保持一致)
|
// 加载详情数据(与 index.tsx 中完全保持一致)
|
||||||
const initRequest = async () => {
|
const initRequest = async () => {
|
||||||
const { data, success }: API.OrderDetailRes =
|
const { data, success }: API.OrderDetailRes = await ordercontrollerGetorderdetail({ orderId });
|
||||||
await ordercontrollerGetorderdetail({ orderId });
|
|
||||||
if (!success || !data) return { data: {} } as any;
|
if (!success || !data) return { data: {} } as any;
|
||||||
data.sales = data.sales?.reduce(
|
data.sales = data.sales?.reduce((acc: API.OrderSale[], cur: API.OrderSale) => {
|
||||||
(acc: API.OrderSale[], cur: API.OrderSale) => {
|
|
||||||
const idx = acc.findIndex((v: any) => v.productId === cur.productId);
|
const idx = acc.findIndex((v: any) => v.productId === cur.productId);
|
||||||
if (idx === -1) acc.push(cur);
|
if (idx === -1) acc.push(cur);
|
||||||
else acc[idx].quantity += cur.quantity;
|
else acc[idx].quantity += cur.quantity;
|
||||||
return acc;
|
return acc;
|
||||||
},
|
}, []);
|
||||||
[],
|
|
||||||
);
|
|
||||||
return { data } as any;
|
return { data } as any;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -118,7 +124,7 @@ const OrderDetailDrawer: React.FC<OrderDetailDrawerProps> = ({
|
||||||
<Popconfirm
|
<Popconfirm
|
||||||
key="btn-after-sale"
|
key="btn-after-sale"
|
||||||
title="转至售后"
|
title="转至售后"
|
||||||
description="确认转至售后?"
|
description="确认转至售后?"
|
||||||
onConfirm={async () => {
|
onConfirm={async () => {
|
||||||
try {
|
try {
|
||||||
const { success, message: errMsg } =
|
const { success, message: errMsg } =
|
||||||
|
|
@ -145,7 +151,7 @@ const OrderDetailDrawer: React.FC<OrderDetailDrawerProps> = ({
|
||||||
<Popconfirm
|
<Popconfirm
|
||||||
key="btn-cancel"
|
key="btn-cancel"
|
||||||
title="转至取消"
|
title="转至取消"
|
||||||
description="确认转至取消?"
|
description="确认转至取消?"
|
||||||
onConfirm={async () => {
|
onConfirm={async () => {
|
||||||
try {
|
try {
|
||||||
const { success, message: errMsg } =
|
const { success, message: errMsg } =
|
||||||
|
|
@ -168,7 +174,7 @@ const OrderDetailDrawer: React.FC<OrderDetailDrawerProps> = ({
|
||||||
<Popconfirm
|
<Popconfirm
|
||||||
key="btn-refund"
|
key="btn-refund"
|
||||||
title="转至退款"
|
title="转至退款"
|
||||||
description="确认转至退款?"
|
description="确认转至退款?"
|
||||||
onConfirm={async () => {
|
onConfirm={async () => {
|
||||||
try {
|
try {
|
||||||
const { success, message: errMsg } =
|
const { success, message: errMsg } =
|
||||||
|
|
@ -191,7 +197,7 @@ const OrderDetailDrawer: React.FC<OrderDetailDrawerProps> = ({
|
||||||
<Popconfirm
|
<Popconfirm
|
||||||
key="btn-completed"
|
key="btn-completed"
|
||||||
title="转至完成"
|
title="转至完成"
|
||||||
description="确认转至完成?"
|
description="确认转至完成?"
|
||||||
onConfirm={async () => {
|
onConfirm={async () => {
|
||||||
try {
|
try {
|
||||||
const { success, message: errMsg } =
|
const { success, message: errMsg } =
|
||||||
|
|
@ -214,178 +220,65 @@ const OrderDetailDrawer: React.FC<OrderDetailDrawerProps> = ({
|
||||||
: []),
|
: []),
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<ProDescriptions
|
<ProDescriptions labelStyle={{ width: '100px' }} actionRef={ref} request={initRequest}>
|
||||||
labelStyle={{ width: '100px' }}
|
<ProDescriptions.Item label="站点" dataIndex="siteId" valueType="select" request={async () => {
|
||||||
actionRef={ref}
|
|
||||||
request={initRequest}
|
|
||||||
>
|
|
||||||
<ProDescriptions.Item
|
|
||||||
label="站点"
|
|
||||||
dataIndex="siteId"
|
|
||||||
valueType="select"
|
|
||||||
request={async () => {
|
|
||||||
const { data = [] } = await sitecontrollerAll();
|
const { data = [] } = await sitecontrollerAll();
|
||||||
return data.map((item) => ({
|
return data.map((item) => ({ label: item.siteName, value: item.id }));
|
||||||
label: item.name,
|
}} />
|
||||||
value: item.id,
|
<ProDescriptions.Item label="订单日期" dataIndex="date_created" valueType="dateTime" />
|
||||||
}));
|
<ProDescriptions.Item label="订单状态" dataIndex="orderStatus" valueType="select" valueEnum={ORDER_STATUS_ENUM as any} />
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<ProDescriptions.Item
|
|
||||||
label="订单日期"
|
|
||||||
dataIndex="date_created"
|
|
||||||
valueType="dateTime"
|
|
||||||
/>
|
|
||||||
<ProDescriptions.Item
|
|
||||||
label="订单状态"
|
|
||||||
dataIndex="orderStatus"
|
|
||||||
valueType="select"
|
|
||||||
valueEnum={ORDER_STATUS_ENUM as any}
|
|
||||||
/>
|
|
||||||
<ProDescriptions.Item label="金额" dataIndex="total" />
|
<ProDescriptions.Item label="金额" dataIndex="total" />
|
||||||
<ProDescriptions.Item label="客户邮箱" dataIndex="customer_email" />
|
<ProDescriptions.Item label="客户邮箱" dataIndex="customer_email" />
|
||||||
<ProDescriptions.Item
|
<ProDescriptions.Item label="联系电话" span={3} render={(_, r: any) => (
|
||||||
label="联系电话"
|
<div><span>{r?.shipping?.phone || r?.billing?.phone || '-'}</span></div>
|
||||||
span={3}
|
)} />
|
||||||
render={(_, r: any) => (
|
|
||||||
<div>
|
|
||||||
<span>{r?.shipping?.phone || r?.billing?.phone || '-'}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<ProDescriptions.Item label="交易Id" dataIndex="transaction_id" />
|
<ProDescriptions.Item label="交易Id" dataIndex="transaction_id" />
|
||||||
<ProDescriptions.Item label="IP" dataIndex="customer_id_address" />
|
<ProDescriptions.Item label="IP" dataIndex="customer_id_address" />
|
||||||
<ProDescriptions.Item label="设备" dataIndex="device_type" />
|
<ProDescriptions.Item label="设备" dataIndex="device_type" />
|
||||||
<ProDescriptions.Item
|
<ProDescriptions.Item label="来源" render={(_, r: any) => formatSource(r.source_type, r.utm_source)} />
|
||||||
label="来源"
|
<ProDescriptions.Item label="原订单状态" dataIndex="status" valueType="select" valueEnum={ORDER_STATUS_ENUM as any} />
|
||||||
render={(_, r: any) => formatSource(r.source_type, r.utm_source)}
|
<ProDescriptions.Item label="支付链接" dataIndex="payment_url" span={3} copyable />
|
||||||
/>
|
<ProDescriptions.Item label="客户备注" dataIndex="customer_note" span={3} />
|
||||||
<ProDescriptions.Item
|
<ProDescriptions.Item label="发货信息" span={3} render={(_, r: any) => (
|
||||||
label="原订单状态"
|
|
||||||
dataIndex="status"
|
|
||||||
valueType="select"
|
|
||||||
valueEnum={ORDER_STATUS_ENUM as any}
|
|
||||||
/>
|
|
||||||
<ProDescriptions.Item
|
|
||||||
label="支付链接"
|
|
||||||
dataIndex="payment_url"
|
|
||||||
span={3}
|
|
||||||
copyable
|
|
||||||
/>
|
|
||||||
<ProDescriptions.Item
|
|
||||||
label="客户备注"
|
|
||||||
dataIndex="customer_note"
|
|
||||||
span={3}
|
|
||||||
/>
|
|
||||||
<ProDescriptions.Item
|
|
||||||
label="发货信息"
|
|
||||||
span={3}
|
|
||||||
render={(_, r: any) => (
|
|
||||||
<div>
|
<div>
|
||||||
<div>
|
<div>company:<span>{r?.shipping?.company || r?.billing?.company || '-'}</span></div>
|
||||||
company:
|
<div>first_name:<span>{r?.shipping?.first_name || r?.billing?.first_name || '-'}</span></div>
|
||||||
<span>
|
<div>last_name:<span>{r?.shipping?.last_name || r?.billing?.last_name || '-'}</span></div>
|
||||||
{r?.shipping?.company || r?.billing?.company || '-'}
|
<div>country:<span>{r?.shipping?.country || r?.billing?.country || '-'}</span></div>
|
||||||
</span>
|
<div>state:<span>{r?.shipping?.state || r?.billing?.state || '-'}</span></div>
|
||||||
|
<div>city:<span>{r?.shipping?.city || r?.billing?.city || '-'}</span></div>
|
||||||
|
<div>postcode:<span>{r?.shipping?.postcode || r?.billing?.postcode || '-'}</span></div>
|
||||||
|
<div>phone:<span>{r?.shipping?.phone || r?.billing?.phone || '-'}</span></div>
|
||||||
|
<div>address_1:<span>{r?.shipping?.address_1 || r?.billing?.address_1 || '-'}</span></div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
)} />
|
||||||
first_name:
|
<ProDescriptions.Item label="原始订单" span={3} render={(_, r: any) => (
|
||||||
<span>
|
|
||||||
{r?.shipping?.first_name || r?.billing?.first_name || '-'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
last_name:
|
|
||||||
<span>
|
|
||||||
{r?.shipping?.last_name || r?.billing?.last_name || '-'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
country:
|
|
||||||
<span>
|
|
||||||
{r?.shipping?.country || r?.billing?.country || '-'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
state:
|
|
||||||
<span>{r?.shipping?.state || r?.billing?.state || '-'}</span>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
city:<span>{r?.shipping?.city || r?.billing?.city || '-'}</span>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
postcode:
|
|
||||||
<span>
|
|
||||||
{r?.shipping?.postcode || r?.billing?.postcode || '-'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
phone:
|
|
||||||
<span>{r?.shipping?.phone || r?.billing?.phone || '-'}</span>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
address_1:
|
|
||||||
<span>
|
|
||||||
{r?.shipping?.address_1 || r?.billing?.address_1 || '-'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<ProDescriptions.Item
|
|
||||||
label="原始订单"
|
|
||||||
span={3}
|
|
||||||
render={(_, r: any) => (
|
|
||||||
<ul>
|
<ul>
|
||||||
{(r?.items || []).map((item: any) => (
|
{(r?.items || []).map((item: any) => (
|
||||||
<li key={item.id}>
|
<li key={item.id}>{item.name}:{item.quantity}</li>
|
||||||
{item.name}:{item.quantity}
|
|
||||||
</li>
|
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
)}
|
)} />
|
||||||
/>
|
<ProDescriptions.Item label="关联" span={3} render={(_, r: any) => (
|
||||||
<ProDescriptions.Item
|
<RelatedOrders data={r?.related} />
|
||||||
label="关联"
|
)} />
|
||||||
span={3}
|
<ProDescriptions.Item label="订单内容" span={3} render={(_, r: any) => (
|
||||||
render={(_, r: any) => <RelatedOrders data={r?.related} />}
|
|
||||||
/>
|
|
||||||
<ProDescriptions.Item
|
|
||||||
label="订单内容"
|
|
||||||
span={3}
|
|
||||||
render={(_, r: any) => (
|
|
||||||
<ul>
|
<ul>
|
||||||
{(r?.sales || []).map((item: any) => (
|
{(r?.sales || []).map((item: any) => (
|
||||||
<li key={item.id}>
|
<li key={item.id}>{item.name}:{item.quantity}</li>
|
||||||
{item.name}:{item.quantity}
|
|
||||||
</li>
|
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
)}
|
)} />
|
||||||
/>
|
<ProDescriptions.Item label="换货" span={3} render={(_, r: any) => (
|
||||||
<ProDescriptions.Item
|
|
||||||
label="换货"
|
|
||||||
span={3}
|
|
||||||
render={(_, r: any) => (
|
|
||||||
<SalesChangeComponent detailRef={ref} id={r.id as number} />
|
<SalesChangeComponent detailRef={ref} id={r.id as number} />
|
||||||
)}
|
)} />
|
||||||
/>
|
<ProDescriptions.Item label="备注" span={3} render={(_, r: any) => {
|
||||||
<ProDescriptions.Item
|
if (!r.notes || r.notes.length === 0) return (<Empty description="暂无备注" />);
|
||||||
label="备注"
|
|
||||||
span={3}
|
|
||||||
render={(_, r: any) => {
|
|
||||||
if (!r.notes || r.notes.length === 0)
|
|
||||||
return <Empty description="暂无备注" />;
|
|
||||||
return (
|
return (
|
||||||
<div style={{ width: '100%' }}>
|
<div style={{ width: '100%' }}>
|
||||||
{r.notes.map((note: any) => (
|
{r.notes.map((note: any) => (
|
||||||
<div style={{ marginBottom: 10 }} key={note.id}>
|
<div style={{ marginBottom: 10 }} key={note.id}>
|
||||||
<div
|
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||||
style={{
|
|
||||||
display: 'flex',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span>{note.username}</span>
|
<span>{note.username}</span>
|
||||||
<span>{note.createdAt}</span>
|
<span>{note.createdAt}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -394,90 +287,38 @@ const OrderDetailDrawer: React.FC<OrderDetailDrawerProps> = ({
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}}
|
}} />
|
||||||
/>
|
<ProDescriptions.Item label="物流信息" span={3} render={(_, r: any) => {
|
||||||
<ProDescriptions.Item
|
if (!r.shipment || r.shipment.length === 0) return (<Empty description="暂无物流信息" />);
|
||||||
label="物流信息"
|
|
||||||
span={3}
|
|
||||||
render={(_, r: any) => {
|
|
||||||
if (!r.shipment || r.shipment.length === 0)
|
|
||||||
return <Empty description="暂无物流信息" />;
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div style={{ display: 'flex', flexDirection: 'column', width: '100%' }}>
|
||||||
style={{
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
width: '100%',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{r.shipment.map((v: any) => (
|
{r.shipment.map((v: any) => (
|
||||||
<Card
|
<Card key={v.id} style={{ marginBottom: '10px' }} extra={formatShipmentState(v.state)} title={<>
|
||||||
key={v.id}
|
|
||||||
style={{ marginBottom: '10px' }}
|
|
||||||
extra={formatShipmentState(v.state)}
|
|
||||||
title={
|
|
||||||
<>
|
|
||||||
{v.tracking_provider}
|
{v.tracking_provider}
|
||||||
{v.primary_tracking_number}
|
{v.primary_tracking_number}
|
||||||
<CopyOutlined
|
<CopyOutlined onClick={async () => {
|
||||||
onClick={async () => {
|
try { await navigator.clipboard.writeText(v.tracking_url); message.success('复制成功!'); }
|
||||||
try {
|
catch { message.error('复制失败!'); }
|
||||||
await navigator.clipboard.writeText(
|
}} />
|
||||||
v.tracking_url,
|
</>}
|
||||||
);
|
actions={ (v.state === 'waiting-for-scheduling' || v.state === 'waiting-for-transit') ? [
|
||||||
message.success('复制成功!');
|
<Popconfirm key="action-cancel" title="取消运单" description="确认取消运单?" onConfirm={async () => {
|
||||||
} catch {
|
try { const { success, message: errMsg } = await logisticscontrollerDelshipment({ id: v.id }); if (!success) throw new Error(errMsg); tableRef.current?.reload(); ref.current?.reload?.(); }
|
||||||
message.error('复制失败!');
|
catch (error: any) { message.error(error.message); }
|
||||||
}
|
}}>
|
||||||
}}
|
<DeleteFilled />取消运单
|
||||||
/>
|
</Popconfirm>
|
||||||
</>
|
] : [] }
|
||||||
}
|
|
||||||
actions={
|
|
||||||
v.state === 'waiting-for-scheduling' ||
|
|
||||||
v.state === 'waiting-for-transit'
|
|
||||||
? [
|
|
||||||
<Popconfirm
|
|
||||||
key="action-cancel"
|
|
||||||
title="取消运单"
|
|
||||||
description="确认取消运单?"
|
|
||||||
onConfirm={async () => {
|
|
||||||
try {
|
|
||||||
const { success, message: errMsg } =
|
|
||||||
await logisticscontrollerDelshipment({
|
|
||||||
id: v.id,
|
|
||||||
});
|
|
||||||
if (!success) throw new Error(errMsg);
|
|
||||||
tableRef.current?.reload();
|
|
||||||
ref.current?.reload?.();
|
|
||||||
} catch (error: any) {
|
|
||||||
message.error(error.message);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<DeleteFilled />
|
<div>订单号: {Array.isArray(v?.orderIds) ? v.orderIds.join(',') : '-'}</div>
|
||||||
取消运单
|
{Array.isArray(v?.items) && v.items.map((item: any) => (
|
||||||
</Popconfirm>,
|
<div key={item.id}>{item.name}: {item.quantity}</div>
|
||||||
]
|
|
||||||
: []
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
订单号:{' '}
|
|
||||||
{Array.isArray(v?.orderIds) ? v.orderIds.join(',') : '-'}
|
|
||||||
</div>
|
|
||||||
{Array.isArray(v?.items) &&
|
|
||||||
v.items.map((item: any) => (
|
|
||||||
<div key={item.id}>
|
|
||||||
{item.name}: {item.quantity}
|
|
||||||
</div>
|
|
||||||
))}
|
))}
|
||||||
</Card>
|
</Card>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}}
|
}} />
|
||||||
/>
|
|
||||||
</ProDescriptions>
|
</ProDescriptions>
|
||||||
</Drawer>
|
</Drawer>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,62 +1,36 @@
|
||||||
|
import React from 'react';
|
||||||
import { Empty, Tag } from 'antd';
|
import { Empty, Tag } from 'antd';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import relativeTime from 'dayjs/plugin/relativeTime';
|
import relativeTime from 'dayjs/plugin/relativeTime';
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
dayjs.extend(relativeTime);
|
dayjs.extend(relativeTime);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* RelatedOrders 表格组件
|
* RelatedOrders 表格组件
|
||||||
* 用于展示订单详情中的关联数据(订阅/订单),按统一表格样式渲染
|
* 用于展示订单详情中的关联数据(订阅/订单),按统一表格样式渲染
|
||||||
* 本组件将订阅与订单统一归一化为五列展示,便于快速浏览
|
* 本组件将订阅与订单统一归一化为五列展示,便于快速浏览
|
||||||
*/
|
*/
|
||||||
const RelatedOrders: React.FC<{ data?: any[] }> = ({ data = [] }) => {
|
const RelatedOrders: React.FC<{ data?: any[] }> = ({ data = [] }) => {
|
||||||
const rows = (Array.isArray(data) ? data : []).map((it: any) => {
|
const rows = (Array.isArray(data) ? data : []).map((it: any) => {
|
||||||
const isSubscription =
|
const isSubscription = !!it?.externalSubscriptionId || !!it?.billing_period || !!it?.line_items;
|
||||||
!!it?.externalSubscriptionId || !!it?.billing_period || !!it?.line_items;
|
const number = isSubscription ? `#${it?.externalSubscriptionId || it?.id}` : `#${it?.externalOrderId || it?.id}`;
|
||||||
const number = isSubscription
|
|
||||||
? `#${it?.externalSubscriptionId || it?.id}`
|
|
||||||
: `#${it?.externalOrderId || it?.id}`;
|
|
||||||
const relationship = isSubscription ? 'Subscription' : 'Order';
|
const relationship = isSubscription ? 'Subscription' : 'Order';
|
||||||
const dateRaw =
|
const dateRaw = it?.start_date || it?.date_created || it?.createdAt || it?.updatedAt;
|
||||||
it?.start_date || it?.date_created || it?.createdAt || it?.updatedAt;
|
|
||||||
const dateText = dateRaw ? dayjs(dateRaw).fromNow() : '-';
|
const dateText = dateRaw ? dayjs(dateRaw).fromNow() : '-';
|
||||||
const status = (isSubscription ? it?.status : it?.orderStatus) || '-';
|
const status = (isSubscription ? it?.status : it?.orderStatus) || '-';
|
||||||
const statusLower = String(status).toLowerCase();
|
const statusLower = String(status).toLowerCase();
|
||||||
const color =
|
const color = statusLower === 'active' ? 'green' : statusLower === 'cancelled' ? 'red' : 'default';
|
||||||
statusLower === 'active'
|
|
||||||
? 'green'
|
|
||||||
: statusLower === 'cancelled'
|
|
||||||
? 'red'
|
|
||||||
: 'default';
|
|
||||||
const totalNum = Number(it?.total || 0);
|
const totalNum = Number(it?.total || 0);
|
||||||
const totalText = isSubscription
|
const totalText = isSubscription ? `$${totalNum.toFixed(2)} / ${it?.billing_period || 'period'}` : `$${totalNum.toFixed(2)}`;
|
||||||
? `$${totalNum.toFixed(2)} / ${it?.billing_period || 'period'}`
|
return { key: `${isSubscription ? 'sub' : 'order'}-${it?.id}`, number, relationship, dateText, status, color, totalText };
|
||||||
: `$${totalNum.toFixed(2)}`;
|
|
||||||
return {
|
|
||||||
key: `${isSubscription ? 'sub' : 'order'}-${it?.id}`,
|
|
||||||
number,
|
|
||||||
relationship,
|
|
||||||
dateText,
|
|
||||||
status,
|
|
||||||
color,
|
|
||||||
totalText,
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (rows.length === 0) return <Empty description="暂无关联" />;
|
if (rows.length === 0) return <Empty description="暂无关联" />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ width: '100%' }}>
|
<div style={{ width: '100%' }}>
|
||||||
{/* 表头(英文文案,符合国际化默认英文的要求) */}
|
{/* 表头(英文文案,符合国际化默认英文的要求) */}
|
||||||
<div
|
<div style={{ display: 'grid', gridTemplateColumns: '1.5fr 1fr 1fr 1fr 1fr', padding: '8px 0', fontWeight: 600 }}>
|
||||||
style={{
|
|
||||||
display: 'grid',
|
|
||||||
gridTemplateColumns: '1.5fr 1fr 1fr 1fr 1fr',
|
|
||||||
padding: '8px 0',
|
|
||||||
fontWeight: 600,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div>订单编号</div>
|
<div>订单编号</div>
|
||||||
<div>关系</div>
|
<div>关系</div>
|
||||||
<div>日期</div>
|
<div>日期</div>
|
||||||
|
|
@ -65,23 +39,11 @@ const RelatedOrders: React.FC<{ data?: any[] }> = ({ data = [] }) => {
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
{rows.map((r) => (
|
{rows.map((r) => (
|
||||||
<div
|
<div key={r.key} style={{ display: 'grid', gridTemplateColumns: '1.5fr 1fr 1fr 1fr 1fr', padding: '6px 0', borderTop: '1px solid #f0f0f0' }}>
|
||||||
key={r.key}
|
<div><a>{r.number}</a></div>
|
||||||
style={{
|
|
||||||
display: 'grid',
|
|
||||||
gridTemplateColumns: '1.5fr 1fr 1fr 1fr 1fr',
|
|
||||||
padding: '6px 0',
|
|
||||||
borderTop: '1px solid #f0f0f0',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<a>{r.number}</a>
|
|
||||||
</div>
|
|
||||||
<div>{r.relationship}</div>
|
<div>{r.relationship}</div>
|
||||||
<div style={{ color: '#1677ff' }}>{r.dateText}</div>
|
<div style={{ color: '#1677ff' }}>{r.dateText}</div>
|
||||||
<div>
|
<div><Tag color={r.color}>{r.status}</Tag></div>
|
||||||
<Tag color={r.color}>{r.status}</Tag>
|
|
||||||
</div>
|
|
||||||
<div>{r.totalText}</div>
|
<div>{r.totalText}</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,17 @@
|
||||||
import { ordercontrollerGetorders } from '@/servers/api/order';
|
|
||||||
import { sitecontrollerAll } from '@/servers/api/site';
|
|
||||||
import type {
|
|
||||||
ActionType,
|
|
||||||
ProColumns,
|
|
||||||
ProTableProps,
|
|
||||||
} from '@ant-design/pro-components';
|
|
||||||
import { ProTable } from '@ant-design/pro-components';
|
|
||||||
import { PageContainer } from '@ant-design/pro-layout';
|
|
||||||
import { App, Button, Tag } from 'antd';
|
|
||||||
import dayjs from 'dayjs';
|
|
||||||
import React, { useRef, useState } from 'react';
|
import React, { useRef, useState } from 'react';
|
||||||
|
import { PageContainer } from '@ant-design/pro-layout';
|
||||||
|
import type { ProColumns, ActionType, ProTableProps } from '@ant-design/pro-components';
|
||||||
|
import { ProTable } from '@ant-design/pro-components';
|
||||||
|
import { App, Tag, Button } from 'antd';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import { ordercontrollerGetorders } from '@/servers/api/order';
|
||||||
import OrderDetailDrawer from './OrderDetailDrawer';
|
import OrderDetailDrawer from './OrderDetailDrawer';
|
||||||
|
import { sitecontrollerAll } from '@/servers/api/site';
|
||||||
|
|
||||||
interface OrderItemRow {
|
interface OrderItemRow {
|
||||||
id: number;
|
id: number;
|
||||||
externalOrderId: string;
|
externalOrderId: string;
|
||||||
siteId: number;
|
siteId: string;
|
||||||
date_created: string;
|
date_created: string;
|
||||||
customer_email: string;
|
customer_email: string;
|
||||||
payment_method: string;
|
payment_method: string;
|
||||||
|
|
@ -47,10 +43,7 @@ const OrdersPage: React.FC = () => {
|
||||||
valueType: 'select',
|
valueType: 'select',
|
||||||
request: async () => {
|
request: async () => {
|
||||||
const { data = [] } = await sitecontrollerAll();
|
const { data = [] } = await sitecontrollerAll();
|
||||||
return (data || []).map((item: any) => ({
|
return (data || []).map((item: any) => ({ label: item.siteName, value: item.id }));
|
||||||
label: item.name,
|
|
||||||
value: item.id,
|
|
||||||
}));
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -58,10 +51,7 @@ const OrdersPage: React.FC = () => {
|
||||||
dataIndex: 'date_created',
|
dataIndex: 'date_created',
|
||||||
width: 180,
|
width: 180,
|
||||||
hideInSearch: true,
|
hideInSearch: true,
|
||||||
render: (_, row) =>
|
render: (_, row) => (row?.date_created ? dayjs(row.date_created).format('YYYY-MM-DD HH:mm') : '-'),
|
||||||
row?.date_created
|
|
||||||
? dayjs(row.date_created).format('YYYY-MM-DD HH:mm')
|
|
||||||
: '-',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '邮箱',
|
title: '邮箱',
|
||||||
|
|
@ -119,14 +109,7 @@ const OrdersPage: React.FC = () => {
|
||||||
|
|
||||||
const request: ProTableProps<OrderItemRow>['request'] = async (params) => {
|
const request: ProTableProps<OrderItemRow>['request'] = async (params) => {
|
||||||
try {
|
try {
|
||||||
const {
|
const { current = 1, pageSize = 10, siteId, keyword, customer_email, payment_method } = params as any;
|
||||||
current = 1,
|
|
||||||
pageSize = 10,
|
|
||||||
siteId,
|
|
||||||
keyword,
|
|
||||||
customer_email,
|
|
||||||
payment_method,
|
|
||||||
} = params as any;
|
|
||||||
const [startDate, endDate] = (params as any).dateRange || [];
|
const [startDate, endDate] = (params as any).dateRange || [];
|
||||||
const resp = await ordercontrollerGetorders({
|
const resp = await ordercontrollerGetorders({
|
||||||
current,
|
current,
|
||||||
|
|
@ -136,9 +119,7 @@ const OrdersPage: React.FC = () => {
|
||||||
customer_email,
|
customer_email,
|
||||||
payment_method,
|
payment_method,
|
||||||
isSubscriptionOnly: true as any,
|
isSubscriptionOnly: true as any,
|
||||||
startDate: startDate
|
startDate: startDate ? (dayjs(startDate).toISOString() as any) : undefined,
|
||||||
? (dayjs(startDate).toISOString() as any)
|
|
||||||
: undefined,
|
|
||||||
endDate: endDate ? (dayjs(endDate).toISOString() as any) : undefined,
|
endDate: endDate ? (dayjs(endDate).toISOString() as any) : undefined,
|
||||||
} as any);
|
} as any);
|
||||||
const { success, data, message: errMsg } = resp as any;
|
const { success, data, message: errMsg } = resp as any;
|
||||||
|
|
@ -155,16 +136,13 @@ const OrdersPage: React.FC = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageContainer title="订阅订单">
|
<PageContainer title='订阅订单'>
|
||||||
<ProTable<OrderItemRow>
|
<ProTable<OrderItemRow>
|
||||||
actionRef={actionRef}
|
actionRef={actionRef}
|
||||||
rowKey="id"
|
rowKey='id'
|
||||||
columns={columns}
|
columns={columns}
|
||||||
request={request}
|
request={request}
|
||||||
pagination={{
|
pagination={{ showSizeChanger: true }}
|
||||||
showSizeChanger: true,
|
|
||||||
showQuickJumper: true,
|
|
||||||
}}
|
|
||||||
search={{
|
search={{
|
||||||
labelWidth: 90,
|
labelWidth: 90,
|
||||||
span: 6,
|
span: 6,
|
||||||
|
|
|
||||||
|
|
@ -1,458 +0,0 @@
|
||||||
import {
|
|
||||||
templatecontrollerCreatetemplate,
|
|
||||||
templatecontrollerDeletetemplate,
|
|
||||||
templatecontrollerGettemplatelist,
|
|
||||||
templatecontrollerRendertemplatedirect,
|
|
||||||
templatecontrollerUpdatetemplate,
|
|
||||||
} from '@/servers/api/template';
|
|
||||||
import { EditOutlined, PlusOutlined, ReloadOutlined } from '@ant-design/icons';
|
|
||||||
import {
|
|
||||||
ActionType,
|
|
||||||
DrawerForm,
|
|
||||||
PageContainer,
|
|
||||||
ProColumns,
|
|
||||||
ProForm,
|
|
||||||
ProFormText,
|
|
||||||
ProTable,
|
|
||||||
} from '@ant-design/pro-components';
|
|
||||||
import Editor from '@monaco-editor/react';
|
|
||||||
import { App, Button, Card, Popconfirm, Space, Typography } from 'antd';
|
|
||||||
import { useEffect, useRef, useState } from 'react';
|
|
||||||
|
|
||||||
// 自定义hook,用于处理模板预览逻辑
|
|
||||||
const useTemplatePreview = () => {
|
|
||||||
const [renderedResult, setRenderedResult] = useState<string>('');
|
|
||||||
const [previewData, setPreviewData] = useState<any>(null);
|
|
||||||
|
|
||||||
// 防抖的预览效果
|
|
||||||
useEffect(() => {
|
|
||||||
if (!previewData || !previewData.value) {
|
|
||||||
setRenderedResult('请输入模板内容');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const timer = setTimeout(async () => {
|
|
||||||
let testData = {};
|
|
||||||
try {
|
|
||||||
if (previewData.testData) {
|
|
||||||
testData = JSON.parse(previewData.testData);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
testData = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 使用新的直接渲染API,传入模板内容和测试数据
|
|
||||||
const res = await templatecontrollerRendertemplatedirect({
|
|
||||||
template: previewData.value,
|
|
||||||
data: testData,
|
|
||||||
});
|
|
||||||
if (res.success) {
|
|
||||||
setRenderedResult(res.data as unknown as string);
|
|
||||||
} else {
|
|
||||||
setRenderedResult(`错误: ${res.message}`);
|
|
||||||
}
|
|
||||||
} catch (error: any) {
|
|
||||||
setRenderedResult(`错误: ${error.message}`);
|
|
||||||
}
|
|
||||||
}, 500); // 防抖 500ms
|
|
||||||
|
|
||||||
return () => clearTimeout(timer);
|
|
||||||
}, [previewData]);
|
|
||||||
|
|
||||||
// 处理实时预览逻辑
|
|
||||||
const handlePreview = (_changedValues: any, allValues: any) => {
|
|
||||||
setPreviewData(allValues);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 手动刷新预览
|
|
||||||
const refreshPreview = (formValues: any) => {
|
|
||||||
setPreviewData(formValues);
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
renderedResult,
|
|
||||||
handlePreview,
|
|
||||||
refreshPreview,
|
|
||||||
setPreviewData,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const List: React.FC = () => {
|
|
||||||
const actionRef = useRef<ActionType>();
|
|
||||||
const { message } = App.useApp();
|
|
||||||
|
|
||||||
const columns: ProColumns<API.Template>[] = [
|
|
||||||
{
|
|
||||||
title: '名称',
|
|
||||||
dataIndex: 'name',
|
|
||||||
tip: '名称是唯一的 key',
|
|
||||||
formItemProps: {
|
|
||||||
rules: [
|
|
||||||
{
|
|
||||||
required: true,
|
|
||||||
message: '名称为必填项',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '标题',
|
|
||||||
dataIndex: 'title',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '值',
|
|
||||||
dataIndex: 'value',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '更新时间',
|
|
||||||
dataIndex: 'updatedAt',
|
|
||||||
valueType: 'dateTime',
|
|
||||||
hideInSearch: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '创建时间',
|
|
||||||
dataIndex: 'createdAt',
|
|
||||||
valueType: 'dateTime',
|
|
||||||
hideInSearch: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '操作',
|
|
||||||
dataIndex: 'option',
|
|
||||||
valueType: 'option',
|
|
||||||
render: (_, record) => (
|
|
||||||
<Space>
|
|
||||||
<UpdateForm tableRef={actionRef} values={record} />
|
|
||||||
<Popconfirm
|
|
||||||
title="删除"
|
|
||||||
description="确认删除?"
|
|
||||||
onConfirm={async () => {
|
|
||||||
if (!record.id) return;
|
|
||||||
try {
|
|
||||||
await templatecontrollerDeletetemplate({ id: record.id });
|
|
||||||
actionRef.current?.reload();
|
|
||||||
} catch (error: any) {
|
|
||||||
message.error(error.message);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button type="primary" danger>
|
|
||||||
删除
|
|
||||||
</Button>
|
|
||||||
</Popconfirm>
|
|
||||||
</Space>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<PageContainer header={{ title: '模板列表' }}>
|
|
||||||
<ProTable<API.Template>
|
|
||||||
headerTitle="查询表格"
|
|
||||||
actionRef={actionRef}
|
|
||||||
rowKey="id"
|
|
||||||
toolBarRender={() => [<CreateForm tableRef={actionRef} />]}
|
|
||||||
request={async (params) => {
|
|
||||||
const response = (await templatecontrollerGettemplatelist(
|
|
||||||
params as any,
|
|
||||||
)) as any;
|
|
||||||
return {
|
|
||||||
data: response.items || [],
|
|
||||||
total: response.total || 0,
|
|
||||||
success: true,
|
|
||||||
};
|
|
||||||
}}
|
|
||||||
columns={columns}
|
|
||||||
/>
|
|
||||||
</PageContainer>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const CreateForm: React.FC<{
|
|
||||||
tableRef: React.MutableRefObject<ActionType | undefined>;
|
|
||||||
}> = ({ tableRef }) => {
|
|
||||||
const { message } = App.useApp();
|
|
||||||
const [form] = ProForm.useForm();
|
|
||||||
const { renderedResult, handlePreview, refreshPreview } =
|
|
||||||
useTemplatePreview();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DrawerForm<API.CreateTemplateDTO>
|
|
||||||
title="新建"
|
|
||||||
form={form}
|
|
||||||
trigger={
|
|
||||||
<Button type="primary">
|
|
||||||
<PlusOutlined />
|
|
||||||
新建
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
autoFocusFirstInput
|
|
||||||
drawerProps={{
|
|
||||||
destroyOnHidden: true,
|
|
||||||
width: 1200, // 增加抽屉宽度以容纳调试面板
|
|
||||||
}}
|
|
||||||
onValuesChange={handlePreview}
|
|
||||||
onFinish={async (values) => {
|
|
||||||
try {
|
|
||||||
await templatecontrollerCreatetemplate(values);
|
|
||||||
tableRef.current?.reload();
|
|
||||||
message.success('提交成功');
|
|
||||||
return true;
|
|
||||||
} catch (error: any) {
|
|
||||||
message.error(error.message);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div style={{ display: 'flex', gap: '20px' }}>
|
|
||||||
<div style={{ flex: 1 }}>
|
|
||||||
<ProFormText
|
|
||||||
name="name"
|
|
||||||
label="模板名称"
|
|
||||||
placeholder="请输入名称"
|
|
||||||
rules={[{ required: true, message: '请输入名称' }]}
|
|
||||||
/>
|
|
||||||
<ProForm.Item
|
|
||||||
name="value"
|
|
||||||
label="模板内容"
|
|
||||||
rules={[{ required: true, message: '请输入模板内容' }]}
|
|
||||||
>
|
|
||||||
<Editor
|
|
||||||
height="400px"
|
|
||||||
defaultLanguage="html"
|
|
||||||
options={{
|
|
||||||
minimap: { enabled: false },
|
|
||||||
lineNumbers: 'on',
|
|
||||||
scrollBeyondLastLine: false,
|
|
||||||
automaticLayout: true,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</ProForm.Item>
|
|
||||||
<ProForm.Item
|
|
||||||
name="testData"
|
|
||||||
label="测试数据 (JSON)"
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
validator: (_: any, value: any) => {
|
|
||||||
if (!value) return Promise.resolve();
|
|
||||||
try {
|
|
||||||
JSON.parse(value);
|
|
||||||
return Promise.resolve();
|
|
||||||
} catch (e) {
|
|
||||||
return Promise.reject(new Error('请输入有效的JSON格式'));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<Editor
|
|
||||||
height="200px"
|
|
||||||
defaultLanguage="json"
|
|
||||||
options={{
|
|
||||||
minimap: { enabled: false },
|
|
||||||
lineNumbers: 'on',
|
|
||||||
scrollBeyondLastLine: false,
|
|
||||||
automaticLayout: true,
|
|
||||||
formatOnPaste: true,
|
|
||||||
formatOnType: true,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</ProForm.Item>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style={{ flex: 1 }}>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
marginBottom: '8px',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Typography.Title level={5} style={{ margin: 0 }}>
|
|
||||||
实时预览
|
|
||||||
</Typography.Title>
|
|
||||||
<Button
|
|
||||||
type="text"
|
|
||||||
icon={<ReloadOutlined />}
|
|
||||||
onClick={() => {
|
|
||||||
// 获取当前表单数据并触发预览
|
|
||||||
const currentValues = form.getFieldsValue();
|
|
||||||
refreshPreview(currentValues);
|
|
||||||
}}
|
|
||||||
title="手动刷新预览"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<Card
|
|
||||||
styles={{
|
|
||||||
body: {
|
|
||||||
padding: '16px',
|
|
||||||
height: '600px',
|
|
||||||
overflow: 'auto',
|
|
||||||
backgroundColor: '#f5f5f5',
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<pre style={{ whiteSpace: 'pre-wrap', margin: 0 }}>
|
|
||||||
{renderedResult || '修改模板或测试数据后将自动预览结果...'}
|
|
||||||
</pre>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</DrawerForm>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const UpdateForm: React.FC<{
|
|
||||||
tableRef: React.MutableRefObject<ActionType | undefined>;
|
|
||||||
values: API.Template;
|
|
||||||
}> = ({ tableRef, values: initialValues }) => {
|
|
||||||
const { message } = App.useApp();
|
|
||||||
const [form] = ProForm.useForm();
|
|
||||||
const { renderedResult, handlePreview, refreshPreview, setPreviewData } =
|
|
||||||
useTemplatePreview();
|
|
||||||
|
|
||||||
// 组件挂载时初始化预览数据
|
|
||||||
useEffect(() => {
|
|
||||||
if (initialValues) {
|
|
||||||
setPreviewData({
|
|
||||||
name: initialValues.name,
|
|
||||||
value: initialValues.value,
|
|
||||||
testData: initialValues.testData,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [initialValues, setPreviewData]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DrawerForm<API.UpdateTemplateDTO>
|
|
||||||
title="编辑"
|
|
||||||
form={form}
|
|
||||||
initialValues={initialValues}
|
|
||||||
trigger={
|
|
||||||
<Button type="primary">
|
|
||||||
<EditOutlined />
|
|
||||||
编辑
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
autoFocusFirstInput
|
|
||||||
drawerProps={{
|
|
||||||
destroyOnHidden: true,
|
|
||||||
width: 1200, // 增加抽屉宽度以容纳调试面板
|
|
||||||
}}
|
|
||||||
onValuesChange={handlePreview}
|
|
||||||
onFinish={async (values) => {
|
|
||||||
if (!initialValues.id) return false;
|
|
||||||
try {
|
|
||||||
await templatecontrollerUpdatetemplate(
|
|
||||||
{ id: initialValues.id },
|
|
||||||
values,
|
|
||||||
);
|
|
||||||
message.success('提交成功');
|
|
||||||
tableRef.current?.reload();
|
|
||||||
return true;
|
|
||||||
} catch (error: any) {
|
|
||||||
message.error(error.message);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div style={{ display: 'flex', gap: '20px' }}>
|
|
||||||
<div style={{ flex: 1 }}>
|
|
||||||
<ProFormText
|
|
||||||
name="name"
|
|
||||||
label="模板名称"
|
|
||||||
placeholder="请输入名称"
|
|
||||||
rules={[{ required: true, message: '请输入名称' }]}
|
|
||||||
/>
|
|
||||||
<ProForm.Item
|
|
||||||
name="value"
|
|
||||||
label="模板内容"
|
|
||||||
rules={[{ required: true, message: '请输入模板内容' }]}
|
|
||||||
>
|
|
||||||
<Editor
|
|
||||||
height="400px"
|
|
||||||
defaultLanguage="html"
|
|
||||||
options={{
|
|
||||||
minimap: { enabled: false },
|
|
||||||
lineNumbers: 'on',
|
|
||||||
scrollBeyondLastLine: false,
|
|
||||||
automaticLayout: true,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</ProForm.Item>
|
|
||||||
<ProForm.Item
|
|
||||||
name="testData"
|
|
||||||
label="测试数据 (JSON)"
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
validator: (_: any, value: any) => {
|
|
||||||
if (!value) return Promise.resolve();
|
|
||||||
try {
|
|
||||||
JSON.parse(value);
|
|
||||||
return Promise.resolve();
|
|
||||||
} catch (e) {
|
|
||||||
return Promise.reject(new Error('请输入有效的JSON格式'));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<Editor
|
|
||||||
height="200px"
|
|
||||||
defaultLanguage="json"
|
|
||||||
options={{
|
|
||||||
minimap: { enabled: false },
|
|
||||||
lineNumbers: 'on',
|
|
||||||
scrollBeyondLastLine: false,
|
|
||||||
automaticLayout: true,
|
|
||||||
formatOnPaste: true,
|
|
||||||
formatOnType: true,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</ProForm.Item>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style={{ flex: 1 }}>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
marginBottom: '8px',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Typography.Title level={5} style={{ margin: 0 }}>
|
|
||||||
实时预览
|
|
||||||
</Typography.Title>
|
|
||||||
<Button
|
|
||||||
type="text"
|
|
||||||
icon={<ReloadOutlined />}
|
|
||||||
onClick={() => {
|
|
||||||
// 获取当前表单数据并触发预览
|
|
||||||
const currentValues = form.getFieldsValue();
|
|
||||||
refreshPreview(currentValues);
|
|
||||||
}}
|
|
||||||
title="手动刷新预览"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<Card
|
|
||||||
styles={{
|
|
||||||
body: {
|
|
||||||
padding: '16px',
|
|
||||||
height: '600px',
|
|
||||||
overflow: 'auto',
|
|
||||||
backgroundColor: '#f5f5f5',
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<pre style={{ whiteSpace: 'pre-wrap', margin: 0 }}>
|
|
||||||
{renderedResult || '修改模板或测试数据后将自动预览结果...'}
|
|
||||||
</pre>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</DrawerForm>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default List;
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import {
|
import {
|
||||||
logisticscontrollerGetlistbyorderid,
|
|
||||||
logisticscontrollerGetorderlist,
|
logisticscontrollerGetorderlist,
|
||||||
|
logisticscontrollerGetlistbyorderid
|
||||||
} from '@/servers/api/logistics';
|
} from '@/servers/api/logistics';
|
||||||
import { SearchOutlined } from '@ant-design/icons';
|
import { SearchOutlined } from '@ant-design/icons';
|
||||||
import { PageContainer, ProFormSelect } from '@ant-design/pro-components';
|
import { PageContainer, ProFormSelect } from '@ant-design/pro-components';
|
||||||
|
|
@ -16,12 +16,11 @@ const TrackPage: React.FC = () => {
|
||||||
debounceTime={500}
|
debounceTime={500}
|
||||||
request={async ({ keyWords }) => {
|
request={async ({ keyWords }) => {
|
||||||
if (!keyWords || keyWords.length < 3) return [];
|
if (!keyWords || keyWords.length < 3) return [];
|
||||||
const { data: trackList } = await logisticscontrollerGetorderlist({
|
const { data: trackList } =
|
||||||
number: keyWords,
|
await logisticscontrollerGetorderlist({ number: keyWords });
|
||||||
});
|
|
||||||
return trackList?.map((v) => {
|
return trackList?.map((v) => {
|
||||||
return {
|
return {
|
||||||
label: v.name + ' ' + v.externalOrderId,
|
label: v.siteName + ' ' + v.externalOrderId,
|
||||||
value: v.id,
|
value: v.id,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
@ -30,7 +29,7 @@ const TrackPage: React.FC = () => {
|
||||||
prefix: '订单号',
|
prefix: '订单号',
|
||||||
async onChange(value: string) {
|
async onChange(value: string) {
|
||||||
setId(value);
|
setId(value);
|
||||||
setData({});
|
setData({})
|
||||||
|
|
||||||
const { data } = await logisticscontrollerGetlistbyorderid({
|
const { data } = await logisticscontrollerGetlistbyorderid({
|
||||||
id,
|
id,
|
||||||
|
|
@ -54,7 +53,8 @@ const TrackPage: React.FC = () => {
|
||||||
),
|
),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{data?.item ? (
|
{
|
||||||
|
data?.item ?
|
||||||
<div>
|
<div>
|
||||||
<div>
|
<div>
|
||||||
<h4>原订单内容</h4>
|
<h4>原订单内容</h4>
|
||||||
|
|
@ -64,11 +64,10 @@ const TrackPage: React.FC = () => {
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div> : <></>
|
||||||
) : (
|
}
|
||||||
<></>
|
{
|
||||||
)}
|
data?.saleItem ?
|
||||||
{data?.saleItem ? (
|
|
||||||
<div>
|
<div>
|
||||||
<div>
|
<div>
|
||||||
<h4>订单内容</h4>
|
<h4>订单内容</h4>
|
||||||
|
|
@ -78,10 +77,8 @@ const TrackPage: React.FC = () => {
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div> : <></>
|
||||||
) : (
|
}
|
||||||
<></>
|
|
||||||
)}
|
|
||||||
</PageContainer>
|
</PageContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,581 +0,0 @@
|
||||||
import { UploadOutlined } from '@ant-design/icons';
|
|
||||||
import {
|
|
||||||
PageContainer,
|
|
||||||
ProForm,
|
|
||||||
ProFormSelect,
|
|
||||||
} from '@ant-design/pro-components';
|
|
||||||
import { request } from '@umijs/max';
|
|
||||||
import { Button, Card, Col, Input, message, Row, Upload } from 'antd';
|
|
||||||
import React, { useEffect, useState } from 'react';
|
|
||||||
import * as XLSX from 'xlsx';
|
|
||||||
|
|
||||||
// 定义配置接口
|
|
||||||
interface TagConfig {
|
|
||||||
brands: string[];
|
|
||||||
fruitKeys: string[];
|
|
||||||
mintKeys: string[];
|
|
||||||
flavorKeys: string[];
|
|
||||||
strengthKeys: string[];
|
|
||||||
sizeKeys: string[];
|
|
||||||
humidityKeys: string[];
|
|
||||||
categoryKeys: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
// 移植 Python 脚本中的核心函数
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description 从产品名称中解析出品牌,口味,毫克含量和干燥度
|
|
||||||
*/
|
|
||||||
const parseName = (
|
|
||||||
name: string,
|
|
||||||
brands: string[],
|
|
||||||
): [string, string, string, string] => {
|
|
||||||
const nm = name.trim();
|
|
||||||
const dryMatch = nm.match(/\(([^)]*)\)/);
|
|
||||||
const dryness = dryMatch ? dryMatch[1].trim() : '';
|
|
||||||
|
|
||||||
const mgMatch = nm.match(/(\d+)\s*MG/i);
|
|
||||||
const mg = mgMatch ? mgMatch[1] : '';
|
|
||||||
|
|
||||||
// 确保品牌按长度降序排序,避免部分匹配(如匹配到 VELO 而不是 VELO MAX)
|
|
||||||
// 这一步其实应该在传入 brands 之前就做好了,但这里再保险一下
|
|
||||||
// 实际调用时 sortedBrands 已经排好序了
|
|
||||||
for (const b of brands) {
|
|
||||||
if (nm.toUpperCase().startsWith(b.toUpperCase())) {
|
|
||||||
const brand = b; // 使用字典中的原始大小写
|
|
||||||
const start = b.length;
|
|
||||||
const end = mgMatch ? mgMatch.index : nm.length;
|
|
||||||
let flavorPart = nm.substring(start, end);
|
|
||||||
flavorPart = flavorPart.replace(/-/g, ' ').trim();
|
|
||||||
flavorPart = flavorPart.replace(/\s*\([^)]*\)$/, '').trim();
|
|
||||||
return [brand, flavorPart, mg, dryness];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const firstWord = nm.split(' ')[0] || '';
|
|
||||||
const brand = firstWord;
|
|
||||||
const end = mgMatch ? mgMatch.index : nm.length;
|
|
||||||
const flavorPart = nm.substring(brand.length, end).trim();
|
|
||||||
return [brand, flavorPart, mg, dryness];
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description 将口味部分拆分为规范的令牌
|
|
||||||
*/
|
|
||||||
const splitFlavorTokens = (flavorPart: string): string[] => {
|
|
||||||
const rawTokens = flavorPart.match(/[A-Za-z]+/g) || [];
|
|
||||||
const tokens: string[] = [];
|
|
||||||
const EXCEPT_SPLIT = new Set(['spearmint', 'peppermint']);
|
|
||||||
|
|
||||||
for (const tok of rawTokens) {
|
|
||||||
const t = tok.toLowerCase();
|
|
||||||
if (t.endsWith('mint') && t.length > 4 && !EXCEPT_SPLIT.has(t)) {
|
|
||||||
const pre = t.slice(0, -4);
|
|
||||||
if (pre) {
|
|
||||||
tokens.push(pre);
|
|
||||||
}
|
|
||||||
tokens.push('mint');
|
|
||||||
} else {
|
|
||||||
tokens.push(t);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return tokens;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description 根据口味分类额外的标签(如 Fruit, Mint)
|
|
||||||
*/
|
|
||||||
const classifyExtraTags = (
|
|
||||||
flavorPart: string,
|
|
||||||
fruitKeys: string[],
|
|
||||||
mintKeys: string[],
|
|
||||||
): string[] => {
|
|
||||||
const tokens = splitFlavorTokens(flavorPart);
|
|
||||||
const fLower = flavorPart.toLowerCase();
|
|
||||||
const isFruit =
|
|
||||||
fruitKeys.some((key) => fLower.includes(key.toLowerCase())) ||
|
|
||||||
tokens.some((t) => fruitKeys.map((k) => k.toLowerCase()).includes(t));
|
|
||||||
const isMint =
|
|
||||||
mintKeys.some((key) => fLower.includes(key.toLowerCase())) ||
|
|
||||||
tokens.includes('mint');
|
|
||||||
|
|
||||||
const extras: string[] = [];
|
|
||||||
if (isFruit) extras.push('Fruit');
|
|
||||||
if (isMint) extras.push('Mint');
|
|
||||||
return extras;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description 在文本中匹配属性关键词
|
|
||||||
*/
|
|
||||||
const matchAttributes = (text: string, keys: string[]): string[] => {
|
|
||||||
const matched = new Set<string>();
|
|
||||||
for (const key of keys) {
|
|
||||||
// 使用单词边界匹配,避免部分匹配
|
|
||||||
// 转义正则特殊字符
|
|
||||||
const escapedKey = key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
||||||
const regex = new RegExp(`\\b${escapedKey}\\b`, 'i');
|
|
||||||
if (regex.test(text)) {
|
|
||||||
matched.add(key.charAt(0).toUpperCase() + key.slice(1));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return Array.from(matched);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description 计算最终的 Tags 字符串
|
|
||||||
*/
|
|
||||||
const computeTags = (name: string, sku: string, config: TagConfig): string => {
|
|
||||||
const [brand, flavorPart, mg, dryness] = parseName(name, config.brands);
|
|
||||||
const tokens = splitFlavorTokens(flavorPart);
|
|
||||||
|
|
||||||
// 白名单模式:只保留在 flavorKeys 中的 token
|
|
||||||
// 且对比时忽略大小写
|
|
||||||
const flavorKeysLower = config.flavorKeys.map((k) => k.toLowerCase());
|
|
||||||
|
|
||||||
const tokensForFlavor = tokens.filter((t) =>
|
|
||||||
flavorKeysLower.includes(t.toLowerCase()),
|
|
||||||
);
|
|
||||||
|
|
||||||
// 将匹配到的 token 转为首字母大写
|
|
||||||
const flavorTag = tokensForFlavor
|
|
||||||
.map((t) => t.charAt(0).toUpperCase() + t.slice(1))
|
|
||||||
.join('');
|
|
||||||
|
|
||||||
let tags: string[] = [];
|
|
||||||
if (brand) tags.push(brand);
|
|
||||||
if (flavorTag) tags.push(flavorTag);
|
|
||||||
|
|
||||||
// 添加额外的口味描述词
|
|
||||||
for (const t of tokensForFlavor) {
|
|
||||||
// 检查是否在 fruitKeys 中 (忽略大小写)
|
|
||||||
const isFruitKey = config.fruitKeys.some(
|
|
||||||
(k) => k.toLowerCase() === t.toLowerCase(),
|
|
||||||
);
|
|
||||||
if (isFruitKey && t.toLowerCase() !== 'fruit') {
|
|
||||||
tags.push(t.charAt(0).toUpperCase() + t.slice(1));
|
|
||||||
}
|
|
||||||
if (t.toLowerCase() === 'mint') {
|
|
||||||
tags.push('Mint');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 匹配 Size (Slim, Mini etc.)
|
|
||||||
tags.push(...matchAttributes(name, config.sizeKeys));
|
|
||||||
|
|
||||||
// 匹配 Humidity (Dry, Moist etc.)
|
|
||||||
tags.push(...matchAttributes(name, config.humidityKeys));
|
|
||||||
|
|
||||||
// 匹配 Category
|
|
||||||
tags.push(...matchAttributes(name, config.categoryKeys));
|
|
||||||
|
|
||||||
// 匹配 Strength (Qualitative like "Strong" or exact matches in dict)
|
|
||||||
tags.push(...matchAttributes(name, config.strengthKeys));
|
|
||||||
|
|
||||||
// 保留原有的 Mix Pack 逻辑
|
|
||||||
if (/mix/i.test(name) || (sku && /mix/i.test(sku))) {
|
|
||||||
tags.push('Mix Pack');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 保留原有的 MG 提取逻辑 (Regex is robust for "6MG", "6 MG")
|
|
||||||
if (mg) {
|
|
||||||
tags.push(`${mg} mg`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 保留原有的 dryness 提取逻辑 (从括号中提取)
|
|
||||||
// 如果 dict 匹配已经覆盖了,去重时会处理
|
|
||||||
if (dryness) {
|
|
||||||
if (/moist/i.test(dryness)) {
|
|
||||||
tags.push('Moisture');
|
|
||||||
} else {
|
|
||||||
tags.push(dryness.charAt(0).toUpperCase() + dryness.slice(1));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tags.push(
|
|
||||||
...classifyExtraTags(flavorPart, config.fruitKeys, config.mintKeys),
|
|
||||||
);
|
|
||||||
|
|
||||||
// 去重并保留顺序
|
|
||||||
const seen = new Set<string>();
|
|
||||||
const finalTags = tags.filter((t) => {
|
|
||||||
// 简单的去重,忽略大小写差异? 或者完全匹配
|
|
||||||
// 这里使用完全匹配,因为前面已经做了一些格式化
|
|
||||||
if (t && !seen.has(t)) {
|
|
||||||
seen.add(t);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
|
|
||||||
return finalTags.join(', ');
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description WordPress 产品工具页面,用于处理产品 CSV 并生成 Tags
|
|
||||||
*/
|
|
||||||
const WpToolPage: React.FC = () => {
|
|
||||||
// 状态管理
|
|
||||||
const [form] = ProForm.useForm(); // 表单实例
|
|
||||||
const [file, setFile] = useState<File | null>(null); // 上传的文件
|
|
||||||
const [csvData, setCsvData] = useState<any[]>([]); // 解析后的 CSV 数据
|
|
||||||
const [processedData, setProcessedData] = useState<any[]>([]); // 处理后待下载的数据
|
|
||||||
const [isProcessing, setIsProcessing] = useState(false); // 是否正在处理中
|
|
||||||
const [isConfigLoading, setIsConfigLoading] = useState(false); // 是否正在加载配置
|
|
||||||
const [configLoadAttempts, setConfigLoadAttempts] = useState(0); // 配置加载重试次数
|
|
||||||
const [config, setConfig] = useState<TagConfig>({
|
|
||||||
// 动态配置
|
|
||||||
brands: [],
|
|
||||||
fruitKeys: [],
|
|
||||||
mintKeys: [],
|
|
||||||
flavorKeys: [],
|
|
||||||
strengthKeys: [],
|
|
||||||
sizeKeys: [],
|
|
||||||
humidityKeys: [],
|
|
||||||
categoryKeys: [],
|
|
||||||
});
|
|
||||||
|
|
||||||
// 在组件加载时获取字典数据
|
|
||||||
useEffect(() => {
|
|
||||||
const fetchAllConfigs = async () => {
|
|
||||||
try {
|
|
||||||
message.loading({
|
|
||||||
content: '正在加载字典配置...',
|
|
||||||
key: 'loading-config',
|
|
||||||
});
|
|
||||||
|
|
||||||
// 1. 获取所有字典列表以找到对应的 ID
|
|
||||||
const dictListResponse = await request('/dict/list');
|
|
||||||
// 处理后端统一响应格式
|
|
||||||
const dictList = dictListResponse?.data || dictListResponse || [];
|
|
||||||
|
|
||||||
// 2. 根据字典名称获取字典项
|
|
||||||
const getItems = async (dictName: string) => {
|
|
||||||
try {
|
|
||||||
const dict = dictList.find((d: any) => d.name === dictName);
|
|
||||||
if (!dict) {
|
|
||||||
console.warn(`Dictionary ${dictName} not found`);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
const response = await request('/dict/items', {
|
|
||||||
params: { dictId: dict.id },
|
|
||||||
});
|
|
||||||
// 处理后端统一响应格式,获取数据数组
|
|
||||||
const items = response?.data || response || [];
|
|
||||||
return items.map((item: any) => item.name);
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`Failed to fetch items for ${dictName}:`, error);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 3. 并行获取所有字典项
|
|
||||||
const [
|
|
||||||
brands,
|
|
||||||
fruitKeys,
|
|
||||||
mintKeys,
|
|
||||||
flavorKeys,
|
|
||||||
strengthKeys,
|
|
||||||
sizeKeys,
|
|
||||||
humidityKeys,
|
|
||||||
categoryKeys,
|
|
||||||
] = await Promise.all([
|
|
||||||
getItems('brand'),
|
|
||||||
getItems('fruit'),
|
|
||||||
getItems('mint'),
|
|
||||||
getItems('flavor'),
|
|
||||||
getItems('strength'),
|
|
||||||
getItems('size'),
|
|
||||||
getItems('humidity'),
|
|
||||||
getItems('category'),
|
|
||||||
]);
|
|
||||||
|
|
||||||
const newConfig = {
|
|
||||||
brands,
|
|
||||||
fruitKeys,
|
|
||||||
mintKeys,
|
|
||||||
flavorKeys,
|
|
||||||
strengthKeys,
|
|
||||||
sizeKeys,
|
|
||||||
humidityKeys,
|
|
||||||
categoryKeys,
|
|
||||||
};
|
|
||||||
|
|
||||||
setConfig(newConfig);
|
|
||||||
form.setFieldsValue(newConfig);
|
|
||||||
message.success({ content: '字典配置加载成功', key: 'loading-config' });
|
|
||||||
|
|
||||||
// 显示加载结果统计
|
|
||||||
const totalItems =
|
|
||||||
brands.length +
|
|
||||||
fruitKeys.length +
|
|
||||||
mintKeys.length +
|
|
||||||
flavorKeys.length +
|
|
||||||
strengthKeys.length +
|
|
||||||
sizeKeys.length +
|
|
||||||
humidityKeys.length +
|
|
||||||
categoryKeys.length;
|
|
||||||
console.log(`字典配置加载完成: 共 ${totalItems} 个配置项`);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to fetch configs:', error);
|
|
||||||
message.error({
|
|
||||||
content: '获取字典配置失败,请刷新页面重试',
|
|
||||||
key: 'loading-config',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
fetchAllConfigs();
|
|
||||||
}, [form]);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description 处理文件上传
|
|
||||||
* @param {File} uploadedFile - 用户上传的文件
|
|
||||||
*/
|
|
||||||
const handleFileUpload = (uploadedFile: File) => {
|
|
||||||
// 检查文件类型,虽然 xlsx 库更宽容,但最好还是保留基本验证
|
|
||||||
if (!uploadedFile.name.match(/\.(csv|xlsx|xls)$/)) {
|
|
||||||
message.error('请上传 CSV 或 Excel 格式的文件!');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
setFile(uploadedFile);
|
|
||||||
|
|
||||||
const reader = new FileReader();
|
|
||||||
reader.onload = (e) => {
|
|
||||||
try {
|
|
||||||
const data = e.target?.result;
|
|
||||||
const workbook = XLSX.read(data, { type: 'binary' });
|
|
||||||
const sheetName = workbook.SheetNames[0];
|
|
||||||
const worksheet = workbook.Sheets[sheetName];
|
|
||||||
const jsonData = XLSX.utils.sheet_to_json(worksheet, { header: 1 });
|
|
||||||
|
|
||||||
if (jsonData.length < 2) {
|
|
||||||
message.error('文件为空或缺少表头!');
|
|
||||||
setCsvData([]);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 将数组转换为对象数组
|
|
||||||
const headers = jsonData[0] as string[];
|
|
||||||
const rows = jsonData.slice(1).map((rowArray: any) => {
|
|
||||||
const rowData: { [key: string]: any } = {};
|
|
||||||
headers.forEach((header, index) => {
|
|
||||||
rowData[header] = rowArray[index];
|
|
||||||
});
|
|
||||||
return rowData;
|
|
||||||
});
|
|
||||||
|
|
||||||
message.success(`成功解析 ${rows.length} 条数据.`);
|
|
||||||
setCsvData(rows);
|
|
||||||
setProcessedData([]); // 清空旧的处理结果
|
|
||||||
} catch (error) {
|
|
||||||
message.error('文件解析失败,请检查文件格式!');
|
|
||||||
console.error('File Parse Error:', error);
|
|
||||||
setCsvData([]);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
reader.onerror = (error) => {
|
|
||||||
message.error('文件读取失败!');
|
|
||||||
console.error('File Read Error:', error);
|
|
||||||
};
|
|
||||||
reader.readAsBinaryString(uploadedFile);
|
|
||||||
|
|
||||||
return false; // 阻止 antd Upload 组件的默认上传行为
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description 将数据转换回 CSV 并触发下载
|
|
||||||
*/
|
|
||||||
const downloadData = (data: any[]) => {
|
|
||||||
if (data.length === 0) return;
|
|
||||||
|
|
||||||
// 创建一个新的工作簿
|
|
||||||
const workbook = XLSX.utils.book_new();
|
|
||||||
// 将 JSON 数据转换为工作表
|
|
||||||
const worksheet = XLSX.utils.json_to_sheet(data);
|
|
||||||
// 将工作表添加到工作簿
|
|
||||||
XLSX.utils.book_append_sheet(workbook, worksheet, 'Products with Tags');
|
|
||||||
|
|
||||||
// 生成文件名并触发下载
|
|
||||||
const fileName = `products_with_tags_${Date.now()}.xlsx`;
|
|
||||||
XLSX.writeFile(workbook, fileName);
|
|
||||||
|
|
||||||
message.success('下载任务已开始!');
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description 核心逻辑:根据配置处理 CSV 数据并生成 Tags
|
|
||||||
*/
|
|
||||||
const handleProcessData = async () => {
|
|
||||||
// 验证是否已上传并解析了数据
|
|
||||||
if (csvData.length === 0) {
|
|
||||||
message.warning('请先上传并成功解析一个 CSV 文件.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsProcessing(true);
|
|
||||||
message.loading({ content: '正在生成 Tags...', key: 'processing' });
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 获取表单中的最新配置
|
|
||||||
const config = await form.validateFields();
|
|
||||||
|
|
||||||
const {
|
|
||||||
brands,
|
|
||||||
fruitKeys,
|
|
||||||
mintKeys,
|
|
||||||
flavorKeys,
|
|
||||||
strengthKeys,
|
|
||||||
sizeKeys,
|
|
||||||
humidityKeys,
|
|
||||||
categoryKeys,
|
|
||||||
} = config;
|
|
||||||
|
|
||||||
// 确保品牌按长度降序排序
|
|
||||||
const sortedBrands = [...brands].sort((a, b) => b.length - a.length);
|
|
||||||
|
|
||||||
const dataWithTags = csvData.map((row) => {
|
|
||||||
const name = row.Name || '';
|
|
||||||
const sku = row.SKU || '';
|
|
||||||
try {
|
|
||||||
const tags = computeTags(name, sku, {
|
|
||||||
brands: sortedBrands,
|
|
||||||
fruitKeys,
|
|
||||||
mintKeys,
|
|
||||||
flavorKeys,
|
|
||||||
strengthKeys,
|
|
||||||
sizeKeys,
|
|
||||||
humidityKeys,
|
|
||||||
categoryKeys,
|
|
||||||
});
|
|
||||||
return { ...row, Tags: tags };
|
|
||||||
} catch (e) {
|
|
||||||
console.error(`Failed to process row with name: ${name}`, e);
|
|
||||||
return { ...row, Tags: row.Tags || '' }; // 保留原有 Tags 或为空
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
setProcessedData(dataWithTags);
|
|
||||||
message.success({
|
|
||||||
content: 'Tags 生成成功!正在自动下载...',
|
|
||||||
key: 'processing',
|
|
||||||
});
|
|
||||||
|
|
||||||
// 自动下载
|
|
||||||
downloadData(dataWithTags);
|
|
||||||
} catch (error) {
|
|
||||||
message.error({
|
|
||||||
content: '处理失败,请检查配置或文件.',
|
|
||||||
key: 'processing',
|
|
||||||
});
|
|
||||||
console.error('Processing Error:', error);
|
|
||||||
} finally {
|
|
||||||
setIsProcessing(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<PageContainer title="WordPress 产品工具">
|
|
||||||
<Row gutter={[16, 16]}>
|
|
||||||
{/* 左侧:配置表单 */}
|
|
||||||
<Col xs={24} md={10}>
|
|
||||||
<Card title="1. 配置映射规则">
|
|
||||||
<ProForm
|
|
||||||
form={form}
|
|
||||||
initialValues={config}
|
|
||||||
onFinish={handleProcessData}
|
|
||||||
submitter={false}
|
|
||||||
>
|
|
||||||
<ProFormSelect
|
|
||||||
name="brands"
|
|
||||||
label="品牌列表"
|
|
||||||
mode="tags"
|
|
||||||
placeholder="请输入品牌,按回车确认"
|
|
||||||
rules={[{ required: true, message: '至少需要一个品牌' }]}
|
|
||||||
tooltip="按品牌名称长度倒序匹配,请将较长的品牌(如 WHITE FOX)放在前面."
|
|
||||||
/>
|
|
||||||
<ProFormSelect
|
|
||||||
name="fruitKeys"
|
|
||||||
label="水果关键词"
|
|
||||||
mode="tags"
|
|
||||||
placeholder="请输入关键词,按回车确认"
|
|
||||||
/>
|
|
||||||
<ProFormSelect
|
|
||||||
name="mintKeys"
|
|
||||||
label="薄荷关键词"
|
|
||||||
mode="tags"
|
|
||||||
placeholder="请输入关键词,按回车确认"
|
|
||||||
/>
|
|
||||||
<ProFormSelect
|
|
||||||
name="flavorKeys"
|
|
||||||
label="口味白名单"
|
|
||||||
mode="tags"
|
|
||||||
placeholder="请输入关键词,按回车确认"
|
|
||||||
tooltip="只有在白名单中的词才会被识别为口味."
|
|
||||||
/>
|
|
||||||
<ProFormSelect
|
|
||||||
name="strengthKeys"
|
|
||||||
label="强度关键词"
|
|
||||||
mode="tags"
|
|
||||||
placeholder="请输入关键词,按回车确认"
|
|
||||||
/>
|
|
||||||
<ProFormSelect
|
|
||||||
name="sizeKeys"
|
|
||||||
label="尺寸关键词"
|
|
||||||
mode="tags"
|
|
||||||
placeholder="请输入关键词,按回车确认"
|
|
||||||
/>
|
|
||||||
<ProFormSelect
|
|
||||||
name="humidityKeys"
|
|
||||||
label="湿度关键词"
|
|
||||||
mode="tags"
|
|
||||||
placeholder="请输入关键词,按回车确认"
|
|
||||||
/>
|
|
||||||
<ProFormSelect
|
|
||||||
name="categoryKeys"
|
|
||||||
label="分类关键词"
|
|
||||||
mode="tags"
|
|
||||||
placeholder="请输入关键词,按回车确认"
|
|
||||||
/>
|
|
||||||
</ProForm>
|
|
||||||
</Card>
|
|
||||||
</Col>
|
|
||||||
|
|
||||||
{/* 右侧:文件上传与操作 */}
|
|
||||||
<Col xs={24} md={14}>
|
|
||||||
<Card title="2. 上传文件并操作">
|
|
||||||
<Upload
|
|
||||||
beforeUpload={handleFileUpload}
|
|
||||||
maxCount={1}
|
|
||||||
showUploadList={!!file}
|
|
||||||
onRemove={() => {
|
|
||||||
setFile(null);
|
|
||||||
setCsvData([]);
|
|
||||||
setProcessedData([]);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button icon={<UploadOutlined />}>选择 CSV 文件</Button>
|
|
||||||
</Upload>
|
|
||||||
<div style={{ marginTop: 16 }}>
|
|
||||||
<label style={{ display: 'block', marginBottom: 8 }}>
|
|
||||||
已上传文件
|
|
||||||
</label>
|
|
||||||
<Input value={file ? file.name : '暂未选择文件'} readOnly />
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
type="primary"
|
|
||||||
onClick={handleProcessData}
|
|
||||||
disabled={csvData.length === 0 || isProcessing}
|
|
||||||
loading={isProcessing}
|
|
||||||
style={{ marginTop: '20px' }}
|
|
||||||
>
|
|
||||||
生成并下载 Tags
|
|
||||||
</Button>
|
|
||||||
</Card>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
</PageContainer>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default WpToolPage;
|
|
||||||
|
|
@ -1,94 +0,0 @@
|
||||||
// @ts-ignore
|
|
||||||
/* eslint-disable */
|
|
||||||
import { request } from 'umi';
|
|
||||||
|
|
||||||
/** 获取区域列表(分页) GET /area/ */
|
|
||||||
export async function areacontrollerGetarealist(
|
|
||||||
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
|
|
||||||
params: API.areacontrollerGetarealistParams,
|
|
||||||
options?: { [key: string]: any },
|
|
||||||
) {
|
|
||||||
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 /area/ */
|
|
||||||
export async function areacontrollerCreatearea(
|
|
||||||
body: API.CreateAreaDTO,
|
|
||||||
options?: { [key: string]: any },
|
|
||||||
) {
|
|
||||||
return request<API.Area>('/area/', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
data: body,
|
|
||||||
...(options || {}),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 根据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>(`/area/${param0}`, {
|
|
||||||
method: 'GET',
|
|
||||||
params: { ...queryParams },
|
|
||||||
...(options || {}),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 更新区域 PUT /area/${param0} */
|
|
||||||
export async function areacontrollerUpdatearea(
|
|
||||||
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
|
|
||||||
params: API.areacontrollerUpdateareaParams,
|
|
||||||
body: API.UpdateAreaDTO,
|
|
||||||
options?: { [key: string]: any },
|
|
||||||
) {
|
|
||||||
const { id: param0, ...queryParams } = params;
|
|
||||||
return request<API.Area>(`/area/${param0}`, {
|
|
||||||
method: 'PUT',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
params: { ...queryParams },
|
|
||||||
data: body,
|
|
||||||
...(options || {}),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 删除区域 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>(`/area/${param0}`, {
|
|
||||||
method: 'DELETE',
|
|
||||||
params: { ...queryParams },
|
|
||||||
...(options || {}),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 获取国家列表 GET /area/countries */
|
|
||||||
export async function areacontrollerGetcountries(options?: {
|
|
||||||
[key: string]: any;
|
|
||||||
}) {
|
|
||||||
return request<any>('/area/countries', {
|
|
||||||
method: 'GET',
|
|
||||||
...(options || {}),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
@ -1,123 +0,0 @@
|
||||||
// @ts-ignore
|
|
||||||
/* eslint-disable */
|
|
||||||
import { request } from 'umi';
|
|
||||||
|
|
||||||
/** 此处后端没有提供注释 GET /category/ */
|
|
||||||
export async function categorycontrollerGetlist(
|
|
||||||
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
|
|
||||||
params: API.categorycontrollerGetlistParams,
|
|
||||||
options?: { [key: string]: any },
|
|
||||||
) {
|
|
||||||
return request<any>('/category/', {
|
|
||||||
method: 'GET',
|
|
||||||
params: {
|
|
||||||
...params,
|
|
||||||
pageSize: undefined,
|
|
||||||
...params['pageSize'],
|
|
||||||
current: undefined,
|
|
||||||
...params['current'],
|
|
||||||
},
|
|
||||||
...(options || {}),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 此处后端没有提供注释 POST /category/ */
|
|
||||||
export async function categorycontrollerCreate(
|
|
||||||
body: API.CreateCategoryDTO,
|
|
||||||
options?: { [key: string]: any },
|
|
||||||
) {
|
|
||||||
return request<any>('/category/', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
data: body,
|
|
||||||
...(options || {}),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 此处后端没有提供注释 PUT /category/${param0} */
|
|
||||||
export async function categorycontrollerUpdate(
|
|
||||||
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
|
|
||||||
params: API.categorycontrollerUpdateParams,
|
|
||||||
body: API.UpdateCategoryDTO,
|
|
||||||
options?: { [key: string]: any },
|
|
||||||
) {
|
|
||||||
const { id: param0, ...queryParams } = params;
|
|
||||||
return request<any>(`/category/${param0}`, {
|
|
||||||
method: 'PUT',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
params: { ...queryParams },
|
|
||||||
data: body,
|
|
||||||
...(options || {}),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 此处后端没有提供注释 DELETE /category/${param0} */
|
|
||||||
export async function categorycontrollerDelete(
|
|
||||||
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
|
|
||||||
params: API.categorycontrollerDeleteParams,
|
|
||||||
options?: { [key: string]: any },
|
|
||||||
) {
|
|
||||||
const { id: param0, ...queryParams } = params;
|
|
||||||
return request<any>(`/category/${param0}`, {
|
|
||||||
method: 'DELETE',
|
|
||||||
params: { ...queryParams },
|
|
||||||
...(options || {}),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 此处后端没有提供注释 GET /category/all */
|
|
||||||
export async function categorycontrollerGetall(options?: {
|
|
||||||
[key: string]: any;
|
|
||||||
}) {
|
|
||||||
return request<any>('/category/all', {
|
|
||||||
method: 'GET',
|
|
||||||
...(options || {}),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 此处后端没有提供注释 POST /category/attribute */
|
|
||||||
export async function categorycontrollerCreatecategoryattribute(
|
|
||||||
body: Record<string, any>,
|
|
||||||
options?: { [key: string]: any },
|
|
||||||
) {
|
|
||||||
return request<any>('/category/attribute', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'text/plain',
|
|
||||||
},
|
|
||||||
data: body,
|
|
||||||
...(options || {}),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 此处后端没有提供注释 GET /category/attribute/${param0} */
|
|
||||||
export async function categorycontrollerGetcategoryattributes(
|
|
||||||
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
|
|
||||||
params: API.categorycontrollerGetcategoryattributesParams,
|
|
||||||
options?: { [key: string]: any },
|
|
||||||
) {
|
|
||||||
const { categoryId: param0, ...queryParams } = params;
|
|
||||||
return request<any>(`/category/attribute/${param0}`, {
|
|
||||||
method: 'GET',
|
|
||||||
params: { ...queryParams },
|
|
||||||
...(options || {}),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 此处后端没有提供注释 DELETE /category/attribute/${param0} */
|
|
||||||
export async function categorycontrollerDeletecategoryattribute(
|
|
||||||
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
|
|
||||||
params: API.categorycontrollerDeletecategoryattributeParams,
|
|
||||||
options?: { [key: string]: any },
|
|
||||||
) {
|
|
||||||
const { id: param0, ...queryParams } = params;
|
|
||||||
return request<any>(`/category/attribute/${param0}`, {
|
|
||||||
method: 'DELETE',
|
|
||||||
params: { ...queryParams },
|
|
||||||
...(options || {}),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
@ -2,170 +2,28 @@
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
import { request } from 'umi';
|
import { request } from 'umi';
|
||||||
|
|
||||||
/** 此处后端没有提供注释 POST /customer/ */
|
|
||||||
export async function customercontrollerCreatecustomer(
|
|
||||||
body: API.CreateCustomerDTO,
|
|
||||||
options?: { [key: string]: any },
|
|
||||||
) {
|
|
||||||
return request<API.GetCustomerDTO>('/customer/', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
data: body,
|
|
||||||
...(options || {}),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 此处后端没有提供注释 GET /customer/${param0} */
|
|
||||||
export async function customercontrollerGetcustomerbyid(
|
|
||||||
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
|
|
||||||
params: API.customercontrollerGetcustomerbyidParams,
|
|
||||||
options?: { [key: string]: any },
|
|
||||||
) {
|
|
||||||
const { id: param0, ...queryParams } = params;
|
|
||||||
return request<API.GetCustomerDTO>(`/customer/${param0}`, {
|
|
||||||
method: 'GET',
|
|
||||||
params: { ...queryParams },
|
|
||||||
...(options || {}),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 此处后端没有提供注释 PUT /customer/${param0} */
|
|
||||||
export async function customercontrollerUpdatecustomer(
|
|
||||||
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
|
|
||||||
params: API.customercontrollerUpdatecustomerParams,
|
|
||||||
body: API.UpdateCustomerDTO,
|
|
||||||
options?: { [key: string]: any },
|
|
||||||
) {
|
|
||||||
const { id: param0, ...queryParams } = params;
|
|
||||||
return request<API.GetCustomerDTO>(`/customer/${param0}`, {
|
|
||||||
method: 'PUT',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
params: { ...queryParams },
|
|
||||||
data: body,
|
|
||||||
...(options || {}),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 此处后端没有提供注释 DELETE /customer/${param0} */
|
|
||||||
export async function customercontrollerDeletecustomer(
|
|
||||||
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
|
|
||||||
params: API.customercontrollerDeletecustomerParams,
|
|
||||||
options?: { [key: string]: any },
|
|
||||||
) {
|
|
||||||
const { id: param0, ...queryParams } = params;
|
|
||||||
return request<Record<string, any>>(`/customer/${param0}`, {
|
|
||||||
method: 'DELETE',
|
|
||||||
params: { ...queryParams },
|
|
||||||
...(options || {}),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 此处后端没有提供注释 POST /customer/addtag */
|
|
||||||
export async function customercontrollerAddtag(
|
|
||||||
body: API.CustomerTagDTO,
|
|
||||||
options?: { [key: string]: any },
|
|
||||||
) {
|
|
||||||
return request<Record<string, any>>('/customer/addtag', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
data: body,
|
|
||||||
...(options || {}),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 此处后端没有提供注释 PUT /customer/batch */
|
|
||||||
export async function customercontrollerBatchupdatecustomers(
|
|
||||||
body: API.BatchUpdateCustomerDTO,
|
|
||||||
options?: { [key: string]: any },
|
|
||||||
) {
|
|
||||||
return request<Record<string, any>>('/customer/batch', {
|
|
||||||
method: 'PUT',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
data: body,
|
|
||||||
...(options || {}),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 此处后端没有提供注释 POST /customer/batch */
|
|
||||||
export async function customercontrollerBatchcreatecustomers(
|
|
||||||
body: API.BatchCreateCustomerDTO,
|
|
||||||
options?: { [key: string]: any },
|
|
||||||
) {
|
|
||||||
return request<Record<string, any>>('/customer/batch', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
data: body,
|
|
||||||
...(options || {}),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 此处后端没有提供注释 DELETE /customer/batch */
|
|
||||||
export async function customercontrollerBatchdeletecustomers(
|
|
||||||
body: API.BatchDeleteCustomerDTO,
|
|
||||||
options?: { [key: string]: any },
|
|
||||||
) {
|
|
||||||
return request<Record<string, any>>('/customer/batch', {
|
|
||||||
method: 'DELETE',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
data: body,
|
|
||||||
...(options || {}),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 此处后端没有提供注释 POST /customer/deltag */
|
|
||||||
export async function customercontrollerDeltag(
|
|
||||||
body: API.CustomerTagDTO,
|
|
||||||
options?: { [key: string]: any },
|
|
||||||
) {
|
|
||||||
return request<Record<string, any>>('/customer/deltag', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
data: body,
|
|
||||||
...(options || {}),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 此处后端没有提供注释 GET /customer/gettags */
|
|
||||||
export async function customercontrollerGettags(options?: {
|
|
||||||
[key: string]: any;
|
|
||||||
}) {
|
|
||||||
return request<Record<string, any>>('/customer/gettags', {
|
|
||||||
method: 'GET',
|
|
||||||
...(options || {}),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 此处后端没有提供注释 GET /customer/list */
|
/** 此处后端没有提供注释 GET /customer/list */
|
||||||
export async function customercontrollerGetcustomerlist(options?: {
|
export async function customercontrollerGetcustomerlist(
|
||||||
[key: string]: any;
|
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
|
||||||
}) {
|
params: API.customercontrollerGetcustomerlistParams,
|
||||||
return request<API.ApiResponse>('/customer/list', {
|
options?: { [key: string]: any },
|
||||||
|
) {
|
||||||
|
return request<any>('/customer/list', {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
|
params: {
|
||||||
|
...params,
|
||||||
|
},
|
||||||
...(options || {}),
|
...(options || {}),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 此处后端没有提供注释 POST /customer/setrate */
|
/** 此处后端没有提供注释 PUT /customer/rate */
|
||||||
export async function customercontrollerSetrate(
|
export async function customercontrollerSetrate(
|
||||||
body: Record<string, any>,
|
body: Record<string, any>,
|
||||||
options?: { [key: string]: any },
|
options?: { [key: string]: any },
|
||||||
) {
|
) {
|
||||||
return request<Record<string, any>>('/customer/setrate', {
|
return request<API.BooleanRes>('/customer/rate', {
|
||||||
method: 'POST',
|
method: 'PUT',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'text/plain',
|
'Content-Type': 'text/plain',
|
||||||
},
|
},
|
||||||
|
|
@ -174,22 +32,12 @@ export async function customercontrollerSetrate(
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 此处后端没有提供注释 GET /customer/statistic/list */
|
/** 此处后端没有提供注释 POST /customer/tag/add */
|
||||||
export async function customercontrollerGetcustomerstatisticlist(options?: {
|
export async function customercontrollerAddtag(
|
||||||
[key: string]: any;
|
body: API.CustomerTagDTO,
|
||||||
}) {
|
|
||||||
return request<Record<string, any>>('/customer/statistic/list', {
|
|
||||||
method: 'GET',
|
|
||||||
...(options || {}),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 此处后端没有提供注释 POST /customer/sync */
|
|
||||||
export async function customercontrollerSynccustomers(
|
|
||||||
body: API.SyncCustomersDTO,
|
|
||||||
options?: { [key: string]: any },
|
options?: { [key: string]: any },
|
||||||
) {
|
) {
|
||||||
return request<Record<string, any>>('/customer/sync', {
|
return request<API.BooleanRes>('/customer/tag/add', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
|
|
@ -198,3 +46,28 @@ export async function customercontrollerSynccustomers(
|
||||||
...(options || {}),
|
...(options || {}),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 此处后端没有提供注释 DELETE /customer/tag/del */
|
||||||
|
export async function customercontrollerDeltag(
|
||||||
|
body: API.CustomerTagDTO,
|
||||||
|
options?: { [key: string]: any },
|
||||||
|
) {
|
||||||
|
return request<API.BooleanRes>('/customer/tag/del', {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
data: body,
|
||||||
|
...(options || {}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 此处后端没有提供注释 GET /customer/tags */
|
||||||
|
export async function customercontrollerGettags(options?: {
|
||||||
|
[key: string]: any;
|
||||||
|
}) {
|
||||||
|
return request<any>('/customer/tags', {
|
||||||
|
method: 'GET',
|
||||||
|
...(options || {}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,247 +0,0 @@
|
||||||
// @ts-ignore
|
|
||||||
/* eslint-disable */
|
|
||||||
import { request } from 'umi';
|
|
||||||
|
|
||||||
/** 此处后端没有提供注释 POST /dict/ */
|
|
||||||
export async function dictcontrollerCreatedict(
|
|
||||||
body: API.CreateDictDTO,
|
|
||||||
options?: { [key: string]: any },
|
|
||||||
) {
|
|
||||||
return request<any>('/dict/', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
data: body,
|
|
||||||
...(options || {}),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 此处后端没有提供注释 GET /dict/${param0} */
|
|
||||||
export async function dictcontrollerGetdict(
|
|
||||||
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
|
|
||||||
params: API.dictcontrollerGetdictParams,
|
|
||||||
options?: { [key: string]: any },
|
|
||||||
) {
|
|
||||||
const { id: param0, ...queryParams } = params;
|
|
||||||
return request<any>(`/dict/${param0}`, {
|
|
||||||
method: 'GET',
|
|
||||||
params: { ...queryParams },
|
|
||||||
...(options || {}),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 此处后端没有提供注释 PUT /dict/${param0} */
|
|
||||||
export async function dictcontrollerUpdatedict(
|
|
||||||
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
|
|
||||||
params: API.dictcontrollerUpdatedictParams,
|
|
||||||
body: API.UpdateDictDTO,
|
|
||||||
options?: { [key: string]: any },
|
|
||||||
) {
|
|
||||||
const { id: param0, ...queryParams } = params;
|
|
||||||
return request<any>(`/dict/${param0}`, {
|
|
||||||
method: 'PUT',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
params: { ...queryParams },
|
|
||||||
data: body,
|
|
||||||
...(options || {}),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 此处后端没有提供注释 DELETE /dict/${param0} */
|
|
||||||
export async function dictcontrollerDeletedict(
|
|
||||||
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
|
|
||||||
params: API.dictcontrollerDeletedictParams,
|
|
||||||
options?: { [key: string]: any },
|
|
||||||
) {
|
|
||||||
const { id: param0, ...queryParams } = params;
|
|
||||||
return request<any>(`/dict/${param0}`, {
|
|
||||||
method: 'DELETE',
|
|
||||||
params: { ...queryParams },
|
|
||||||
...(options || {}),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 此处后端没有提供注释 POST /dict/import */
|
|
||||||
export async function dictcontrollerImportdicts(
|
|
||||||
body: {},
|
|
||||||
files?: File[],
|
|
||||||
options?: { [key: string]: any },
|
|
||||||
) {
|
|
||||||
const formData = new FormData();
|
|
||||||
|
|
||||||
if (files) {
|
|
||||||
files.forEach((f) => formData.append('files', f || ''));
|
|
||||||
}
|
|
||||||
|
|
||||||
Object.keys(body).forEach((ele) => {
|
|
||||||
const item = (body as any)[ele];
|
|
||||||
|
|
||||||
if (item !== undefined && item !== null) {
|
|
||||||
if (typeof item === 'object' && !(item instanceof File)) {
|
|
||||||
if (item instanceof Array) {
|
|
||||||
item.forEach((f) => formData.append(ele, f || ''));
|
|
||||||
} else {
|
|
||||||
formData.append(
|
|
||||||
ele,
|
|
||||||
new Blob([JSON.stringify(item)], { type: 'application/json' }),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
formData.append(ele, item);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return request<any>('/dict/import', {
|
|
||||||
method: 'POST',
|
|
||||||
data: formData,
|
|
||||||
requestType: 'form',
|
|
||||||
...(options || {}),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 此处后端没有提供注释 POST /dict/item */
|
|
||||||
export async function dictcontrollerCreatedictitem(
|
|
||||||
body: API.CreateDictItemDTO,
|
|
||||||
options?: { [key: string]: any },
|
|
||||||
) {
|
|
||||||
return request<any>('/dict/item', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
data: body,
|
|
||||||
...(options || {}),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 此处后端没有提供注释 PUT /dict/item/${param0} */
|
|
||||||
export async function dictcontrollerUpdatedictitem(
|
|
||||||
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
|
|
||||||
params: API.dictcontrollerUpdatedictitemParams,
|
|
||||||
body: API.UpdateDictItemDTO,
|
|
||||||
options?: { [key: string]: any },
|
|
||||||
) {
|
|
||||||
const { id: param0, ...queryParams } = params;
|
|
||||||
return request<any>(`/dict/item/${param0}`, {
|
|
||||||
method: 'PUT',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
params: { ...queryParams },
|
|
||||||
data: body,
|
|
||||||
...(options || {}),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 此处后端没有提供注释 DELETE /dict/item/${param0} */
|
|
||||||
export async function dictcontrollerDeletedictitem(
|
|
||||||
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
|
|
||||||
params: API.dictcontrollerDeletedictitemParams,
|
|
||||||
options?: { [key: string]: any },
|
|
||||||
) {
|
|
||||||
const { id: param0, ...queryParams } = params;
|
|
||||||
return request<any>(`/dict/item/${param0}`, {
|
|
||||||
method: 'DELETE',
|
|
||||||
params: { ...queryParams },
|
|
||||||
...(options || {}),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 此处后端没有提供注释 GET /dict/item/export */
|
|
||||||
export async function dictcontrollerExportdictitems(
|
|
||||||
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
|
|
||||||
params: API.dictcontrollerExportdictitemsParams,
|
|
||||||
options?: { [key: string]: any },
|
|
||||||
) {
|
|
||||||
return request<any>('/dict/item/export', {
|
|
||||||
method: 'GET',
|
|
||||||
params: {
|
|
||||||
...params,
|
|
||||||
},
|
|
||||||
...(options || {}),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 此处后端没有提供注释 POST /dict/item/import */
|
|
||||||
export async function dictcontrollerImportdictitems(
|
|
||||||
body: Record<string, any>,
|
|
||||||
options?: { [key: string]: any },
|
|
||||||
) {
|
|
||||||
return request<API.ApiResponse>('/dict/item/import', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'text/plain',
|
|
||||||
},
|
|
||||||
data: body,
|
|
||||||
...(options || {}),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 此处后端没有提供注释 GET /dict/item/template */
|
|
||||||
export async function dictcontrollerDownloaddictitemtemplate(options?: {
|
|
||||||
[key: string]: any;
|
|
||||||
}) {
|
|
||||||
return request<any>('/dict/item/template', {
|
|
||||||
method: 'GET',
|
|
||||||
...(options || {}),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 此处后端没有提供注释 GET /dict/items */
|
|
||||||
export async function dictcontrollerGetdictitems(
|
|
||||||
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
|
|
||||||
params: API.dictcontrollerGetdictitemsParams,
|
|
||||||
options?: { [key: string]: any },
|
|
||||||
) {
|
|
||||||
return request<any>('/dict/items', {
|
|
||||||
method: 'GET',
|
|
||||||
params: {
|
|
||||||
...params,
|
|
||||||
},
|
|
||||||
...(options || {}),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 此处后端没有提供注释 GET /dict/items-by-name */
|
|
||||||
export async function dictcontrollerGetdictitemsbydictname(
|
|
||||||
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
|
|
||||||
params: API.dictcontrollerGetdictitemsbydictnameParams,
|
|
||||||
options?: { [key: string]: any },
|
|
||||||
) {
|
|
||||||
return request<any>('/dict/items-by-name', {
|
|
||||||
method: 'GET',
|
|
||||||
params: {
|
|
||||||
...params,
|
|
||||||
},
|
|
||||||
...(options || {}),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 此处后端没有提供注释 GET /dict/list */
|
|
||||||
export async function dictcontrollerGetdicts(
|
|
||||||
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
|
|
||||||
params: API.dictcontrollerGetdictsParams,
|
|
||||||
options?: { [key: string]: any },
|
|
||||||
) {
|
|
||||||
return request<any>('/dict/list', {
|
|
||||||
method: 'GET',
|
|
||||||
params: {
|
|
||||||
...params,
|
|
||||||
},
|
|
||||||
...(options || {}),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 此处后端没有提供注释 GET /dict/template */
|
|
||||||
export async function dictcontrollerDownloaddicttemplate(options?: {
|
|
||||||
[key: string]: any;
|
|
||||||
}) {
|
|
||||||
return request<any>('/dict/template', {
|
|
||||||
method: 'GET',
|
|
||||||
...(options || {}),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
@ -1,38 +1,28 @@
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
// API 更新时间:
|
// API 更新时间:
|
||||||
// API 唯一标识:
|
// API 唯一标识:
|
||||||
import * as area from './area';
|
|
||||||
import * as category from './category';
|
|
||||||
import * as customer from './customer';
|
import * as customer from './customer';
|
||||||
import * as dict from './dict';
|
|
||||||
import * as locales from './locales';
|
|
||||||
import * as logistics from './logistics';
|
import * as logistics from './logistics';
|
||||||
import * as order from './order';
|
import * as order from './order';
|
||||||
import * as product from './product';
|
import * as product from './product';
|
||||||
import * as site from './site';
|
import * as site from './site';
|
||||||
import * as siteApi from './siteApi';
|
|
||||||
import * as statistics from './statistics';
|
import * as statistics from './statistics';
|
||||||
import * as stock from './stock';
|
import * as stock from './stock';
|
||||||
import * as subscription from './subscription';
|
import * as subscription from './subscription';
|
||||||
import * as template from './template';
|
|
||||||
import * as user from './user';
|
import * as user from './user';
|
||||||
import * as webhook from './webhook';
|
import * as webhook from './webhook';
|
||||||
|
import * as wpProduct from './wpProduct';
|
||||||
export default {
|
export default {
|
||||||
area,
|
|
||||||
category,
|
|
||||||
customer,
|
customer,
|
||||||
dict,
|
|
||||||
locales,
|
|
||||||
logistics,
|
logistics,
|
||||||
order,
|
order,
|
||||||
product,
|
product,
|
||||||
siteApi,
|
|
||||||
site,
|
site,
|
||||||
statistics,
|
statistics,
|
||||||
stock,
|
stock,
|
||||||
subscription,
|
subscription,
|
||||||
template,
|
|
||||||
user,
|
user,
|
||||||
webhook,
|
webhook,
|
||||||
|
wpProduct,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
// @ts-ignore
|
|
||||||
/* eslint-disable */
|
|
||||||
import { request } from 'umi';
|
|
||||||
|
|
||||||
/** 此处后端没有提供注释 GET /locales/${param0} */
|
|
||||||
export async function localecontrollerGetlocale(
|
|
||||||
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
|
|
||||||
params: API.localecontrollerGetlocaleParams,
|
|
||||||
options?: { [key: string]: any },
|
|
||||||
) {
|
|
||||||
const { lang: param0, ...queryParams } = params;
|
|
||||||
return request<any>(`/locales/${param0}`, {
|
|
||||||
method: 'GET',
|
|
||||||
params: { ...queryParams },
|
|
||||||
...(options || {}),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
@ -30,20 +30,6 @@ export async function ordercontrollerDelorder(
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 此处后端没有提供注释 GET /order/${param0}/related */
|
|
||||||
export async function ordercontrollerGetrelatedbyorder(
|
|
||||||
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
|
|
||||||
params: API.ordercontrollerGetrelatedbyorderParams,
|
|
||||||
options?: { [key: string]: any },
|
|
||||||
) {
|
|
||||||
const { orderId: param0, ...queryParams } = params;
|
|
||||||
return request<any>(`/order/${param0}/related`, {
|
|
||||||
method: 'GET',
|
|
||||||
params: { ...queryParams },
|
|
||||||
...(options || {}),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 此处后端没有提供注释 POST /order/createNote */
|
/** 此处后端没有提供注释 POST /order/createNote */
|
||||||
export async function ordercontrollerCreatenote(
|
export async function ordercontrollerCreatenote(
|
||||||
body: API.CreateOrderNoteDTO,
|
body: API.CreateOrderNoteDTO,
|
||||||
|
|
@ -59,21 +45,6 @@ export async function ordercontrollerCreatenote(
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 此处后端没有提供注释 POST /order/export */
|
|
||||||
export async function ordercontrollerExportorder(
|
|
||||||
body: Record<string, any>,
|
|
||||||
options?: { [key: string]: any },
|
|
||||||
) {
|
|
||||||
return request<any>('/order/export', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'text/plain',
|
|
||||||
},
|
|
||||||
data: body,
|
|
||||||
...(options || {}),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 此处后端没有提供注释 POST /order/getOrderByNumber */
|
/** 此处后端没有提供注释 POST /order/getOrderByNumber */
|
||||||
export async function ordercontrollerGetorderbynumber(
|
export async function ordercontrollerGetorderbynumber(
|
||||||
body: string,
|
body: string,
|
||||||
|
|
@ -89,36 +60,6 @@ export async function ordercontrollerGetorderbynumber(
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 此处后端没有提供注释 GET /order/getOrderItemList */
|
|
||||||
export async function ordercontrollerGetorderitemlist(
|
|
||||||
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
|
|
||||||
params: API.ordercontrollerGetorderitemlistParams,
|
|
||||||
options?: { [key: string]: any },
|
|
||||||
) {
|
|
||||||
return request<any>('/order/getOrderItemList', {
|
|
||||||
method: 'GET',
|
|
||||||
params: {
|
|
||||||
...params,
|
|
||||||
},
|
|
||||||
...(options || {}),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 此处后端没有提供注释 GET /order/getOrderItems */
|
|
||||||
export async function ordercontrollerGetorderitems(
|
|
||||||
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
|
|
||||||
params: API.ordercontrollerGetorderitemsParams,
|
|
||||||
options?: { [key: string]: any },
|
|
||||||
) {
|
|
||||||
return request<any>('/order/getOrderItems', {
|
|
||||||
method: 'GET',
|
|
||||||
params: {
|
|
||||||
...params,
|
|
||||||
},
|
|
||||||
...(options || {}),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 此处后端没有提供注释 GET /order/getOrders */
|
/** 此处后端没有提供注释 GET /order/getOrders */
|
||||||
export async function ordercontrollerGetorders(
|
export async function ordercontrollerGetorders(
|
||||||
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
|
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
|
||||||
|
|
@ -240,21 +181,16 @@ export async function ordercontrollerChangestatus(
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 此处后端没有提供注释 POST /order/sync/${param0} */
|
/** 此处后端没有提供注释 POST /order/syncOrder/${param0} */
|
||||||
export async function ordercontrollerSyncorders(
|
export async function ordercontrollerSyncorder(
|
||||||
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
|
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
|
||||||
params: API.ordercontrollerSyncordersParams,
|
params: API.ordercontrollerSyncorderParams,
|
||||||
body: Record<string, any>,
|
|
||||||
options?: { [key: string]: any },
|
options?: { [key: string]: any },
|
||||||
) {
|
) {
|
||||||
const { siteId: param0, ...queryParams } = params;
|
const { siteId: param0, ...queryParams } = params;
|
||||||
return request<API.BooleanRes>(`/order/sync/${param0}`, {
|
return request<API.BooleanRes>(`/order/syncOrder/${param0}`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
|
||||||
'Content-Type': 'text/plain',
|
|
||||||
},
|
|
||||||
params: { ...queryParams },
|
params: { ...queryParams },
|
||||||
data: body,
|
|
||||||
...(options || {}),
|
...(options || {}),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -266,14 +202,11 @@ export async function ordercontrollerSyncorderbyid(
|
||||||
options?: { [key: string]: any },
|
options?: { [key: string]: any },
|
||||||
) {
|
) {
|
||||||
const { orderId: param0, siteId: param1, ...queryParams } = params;
|
const { orderId: param0, siteId: param1, ...queryParams } = params;
|
||||||
return request<API.SyncOperationResult>(
|
return request<API.BooleanRes>(`/order/syncOrder/${param1}/order/${param0}`, {
|
||||||
`/order/syncOrder/${param1}/order/${param0}`,
|
|
||||||
{
|
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
params: { ...queryParams },
|
params: { ...queryParams },
|
||||||
...(options || {}),
|
...(options || {}),
|
||||||
},
|
});
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 此处后端没有提供注释 POST /order/updateOrderItems/${param0} */
|
/** 此处后端没有提供注释 POST /order/updateOrderItems/${param0} */
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue