feat: 添加产品工具, 重构产品 #31
10
.umirc.ts
10
.umirc.ts
|
|
@ -1,10 +1,10 @@
|
|||
import { defineConfig } from '@umijs/max';
|
||||
import { codeInspectorPlugin } from 'code-inspector-plugin';
|
||||
|
||||
const isDev = process.env.NODE_ENV === 'development';
|
||||
const UMI_APP_API_URL = isDev
|
||||
? 'http://localhost:7001'
|
||||
: 'https://api.yoone.ca';
|
||||
import { codeInspectorPlugin } from 'code-inspector-plugin';
|
||||
|
||||
export default defineConfig({
|
||||
hash: true,
|
||||
|
|
@ -23,7 +23,7 @@ export default defineConfig({
|
|||
config.plugin('code-inspector-plugin').use(
|
||||
codeInspectorPlugin({
|
||||
bundler: 'webpack',
|
||||
})
|
||||
}),
|
||||
);
|
||||
},
|
||||
routes: [
|
||||
|
|
@ -93,9 +93,9 @@ export default defineConfig({
|
|||
component: './Product/List',
|
||||
},
|
||||
{
|
||||
name: '商品分类',
|
||||
path: '/product/category',
|
||||
component: './Product/Category',
|
||||
name: '品牌',
|
||||
path: '/product/brand',
|
||||
component: './Product/Brand',
|
||||
},
|
||||
{
|
||||
name: '强度',
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ export default (initialState: any) => {
|
|||
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);
|
||||
return {
|
||||
canSeeOrganiza,
|
||||
canSeeProduct,
|
||||
|
|
@ -22,5 +22,6 @@ export default (initialState: any) => {
|
|||
canSeeStatistics,
|
||||
canSeeSite,
|
||||
canSeeDict,
|
||||
canSeeTemplate,
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
|
||||
import { sitecontrollerAll } from '@/servers/api/site';
|
||||
import { SyncOutlined } from '@ant-design/icons';
|
||||
import {
|
||||
|
|
@ -7,13 +6,13 @@ import {
|
|||
ProForm,
|
||||
ProFormSelect,
|
||||
} from '@ant-design/pro-components';
|
||||
import { App, Button } from 'antd';
|
||||
import { Button } from 'antd';
|
||||
import React from 'react';
|
||||
|
||||
// 定义SyncForm组件的props类型
|
||||
interface SyncFormProps {
|
||||
tableRef: React.MutableRefObject<ActionType | undefined>;
|
||||
onFinish: (values:any) => Promise<void>;
|
||||
onFinish: (values: any) => Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -39,9 +39,9 @@ const ListPage: React.FC = () => {
|
|||
{
|
||||
title: '客户编号',
|
||||
dataIndex: 'customerId',
|
||||
render: (_, record) => {
|
||||
if(!record.customerId) return '-';
|
||||
return String(record.customerId).padStart(6,0)
|
||||
render: (_, record) => {
|
||||
if (!record.customerId) return '-';
|
||||
return String(record.customerId).padStart(6, 0);
|
||||
},
|
||||
sorter: true,
|
||||
},
|
||||
|
|
@ -95,31 +95,37 @@ const ListPage: React.FC = () => {
|
|||
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 '-'
|
||||
}
|
||||
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} />
|
||||
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}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,8 +1,18 @@
|
|||
import { PageContainer } from '@ant-design/pro-components';
|
||||
import { Input, Button, Table, Layout, Space, Modal, message, Form, Upload } from 'antd';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { request } from '@umijs/max';
|
||||
import { UploadOutlined } from '@ant-design/icons';
|
||||
import { PageContainer } from '@ant-design/pro-components';
|
||||
import { request } from '@umijs/max';
|
||||
import {
|
||||
Button,
|
||||
Form,
|
||||
Input,
|
||||
Layout,
|
||||
Modal,
|
||||
Space,
|
||||
Table,
|
||||
Upload,
|
||||
message,
|
||||
} from 'antd';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
const { Sider, Content } = Layout;
|
||||
|
||||
|
|
@ -26,13 +36,17 @@ const DictPage: React.FC = () => {
|
|||
// 控制字典项模态框的显示
|
||||
const [isDictItemModalVisible, setIsDictItemModalVisible] = useState(false);
|
||||
const [editingDictItem, setEditingDictItem] = useState<any>(null);
|
||||
const [dictItemForm, setDictItemForm] = useState({ name: '', title: '', value: '' });
|
||||
const [dictItemForm, setDictItemForm] = useState({
|
||||
name: '',
|
||||
title: '',
|
||||
value: '',
|
||||
});
|
||||
|
||||
// 获取字典列表
|
||||
const fetchDicts = async (title?: string) => {
|
||||
setLoadingDicts(true);
|
||||
try {
|
||||
const res = await request('/dict/list', { params: { title } });
|
||||
const res = await request('/dict/list', { params: { title } });
|
||||
if (res) {
|
||||
setDicts(res);
|
||||
}
|
||||
|
|
@ -46,7 +60,7 @@ const DictPage: React.FC = () => {
|
|||
const fetchDictItems = async (dictId?: number) => {
|
||||
setLoadingDictItems(true);
|
||||
try {
|
||||
const res = await request('/dict/items', { params: { dictId } });
|
||||
const res = await request('/dict/items', { params: { dictId } });
|
||||
if (res) {
|
||||
setDictItems(res);
|
||||
}
|
||||
|
|
@ -80,13 +94,13 @@ const DictPage: React.FC = () => {
|
|||
}
|
||||
try {
|
||||
if (editingDict) {
|
||||
await request(`/dict/${editingDict.id}`, {
|
||||
await request(`/dict/${editingDict.id}`, {
|
||||
method: 'PUT',
|
||||
data: { name: newDictName, title: newDictTitle },
|
||||
});
|
||||
message.success('更新成功');
|
||||
} else {
|
||||
await request('/dict', {
|
||||
await request('/dict', {
|
||||
method: 'POST',
|
||||
data: { name: newDictName, title: newDictTitle },
|
||||
});
|
||||
|
|
@ -132,14 +146,14 @@ const DictPage: React.FC = () => {
|
|||
try {
|
||||
if (editingDictItem) {
|
||||
// 编辑
|
||||
await request(`/dict/item/${editingDictItem.id}`, {
|
||||
await request(`/dict/item/${editingDictItem.id}`, {
|
||||
method: 'PUT',
|
||||
data: dictItemForm,
|
||||
});
|
||||
message.success('更新成功');
|
||||
} else {
|
||||
// 添加
|
||||
await request('/dict/item', {
|
||||
await request('/dict/item', {
|
||||
method: 'POST',
|
||||
data: { ...dictItemForm, dictId: selectedDict.id },
|
||||
});
|
||||
|
|
@ -155,7 +169,7 @@ const DictPage: React.FC = () => {
|
|||
// 处理删除字典项
|
||||
const handleDeleteDictItem = async (itemId: number) => {
|
||||
try {
|
||||
await request(`/dict/item/${itemId}`, { method: 'DELETE' });
|
||||
await request(`/dict/item/${itemId}`, { method: 'DELETE' });
|
||||
message.success('删除成功');
|
||||
fetchDictItems(selectedDict.id);
|
||||
} catch (error) {
|
||||
|
|
@ -166,7 +180,7 @@ const DictPage: React.FC = () => {
|
|||
// 处理删除字典
|
||||
const handleDeleteDict = async (dictId: number) => {
|
||||
try {
|
||||
await request(`/dict/${dictId}`, { method: 'DELETE' });
|
||||
await request(`/dict/${dictId}`, { method: 'DELETE' });
|
||||
message.success('删除成功');
|
||||
fetchDicts(); // 重新获取字典列表
|
||||
// 如果删除的是当前选中的字典,则清空右侧列表
|
||||
|
|
@ -181,13 +195,12 @@ const DictPage: React.FC = () => {
|
|||
|
||||
// 左侧字典列表的列定义
|
||||
const dictColumns = [
|
||||
|
||||
{
|
||||
title: '名称',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
},
|
||||
{
|
||||
{
|
||||
title: '标题',
|
||||
dataIndex: 'title',
|
||||
key: 'title',
|
||||
|
|
@ -197,10 +210,23 @@ const DictPage: React.FC = () => {
|
|||
key: 'action',
|
||||
render: (_: any, record: any) => (
|
||||
<Space>
|
||||
<Button type="link" onClick={(e) => { e.stopPropagation(); handleEditDict(record); }}>
|
||||
<Button
|
||||
type="link"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleEditDict(record);
|
||||
}}
|
||||
>
|
||||
编辑
|
||||
</Button>
|
||||
<Button type="link" danger onClick={(e) => { e.stopPropagation(); handleDeleteDict(record.id); }}>
|
||||
<Button
|
||||
type="link"
|
||||
danger
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleDeleteDict(record.id);
|
||||
}}
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
</Space>
|
||||
|
|
@ -225,8 +251,16 @@ const DictPage: React.FC = () => {
|
|||
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>
|
||||
<Button type="link" onClick={() => handleEditDictItem(record)}>
|
||||
编辑
|
||||
</Button>
|
||||
<Button
|
||||
type="link"
|
||||
danger
|
||||
onClick={() => handleDeleteDictItem(record.id)}
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
|
|
@ -235,18 +269,35 @@ const DictPage: React.FC = () => {
|
|||
return (
|
||||
<PageContainer>
|
||||
<Layout style={{ background: '#fff' }}>
|
||||
<Sider width={300} style={{ background: '#fff', padding: '16px', borderRight: '1px solid #f0f0f0' }}>
|
||||
<Sider
|
||||
width={300}
|
||||
style={{
|
||||
background: '#fff',
|
||||
padding: '16px',
|
||||
borderRight: '1px solid #f0f0f0',
|
||||
}}
|
||||
>
|
||||
<Space direction="vertical" style={{ width: '100%' }}>
|
||||
<Input.Search placeholder="搜索字典" onSearch={handleSearch} onChange={e => setSearchText(e.target.value)} enterButton allowClear />
|
||||
<Button type="primary" onClick={() => setIsAddDictModalVisible(true)} block>
|
||||
<Input.Search
|
||||
placeholder="搜索字典"
|
||||
onSearch={handleSearch}
|
||||
onChange={(e) => setSearchText(e.target.value)}
|
||||
enterButton
|
||||
allowClear
|
||||
/>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={() => setIsAddDictModalVisible(true)}
|
||||
block
|
||||
>
|
||||
添加字典
|
||||
</Button>
|
||||
<Space>
|
||||
<Upload
|
||||
name="file"
|
||||
action="/dict/import"
|
||||
action="/dict/import"
|
||||
showUploadList={false}
|
||||
onChange={info => {
|
||||
onChange={(info) => {
|
||||
if (info.file.status === 'done') {
|
||||
message.success(`${info.file.name} 文件上传成功`);
|
||||
fetchDicts();
|
||||
|
|
@ -257,14 +308,16 @@ const DictPage: React.FC = () => {
|
|||
>
|
||||
<Button icon={<UploadOutlined />}>导入字典</Button>
|
||||
</Upload>
|
||||
<Button onClick={() => window.open('/dict/template')}>下载模板</Button>
|
||||
<Button onClick={() => window.open('/dict/template')}>
|
||||
下载模板
|
||||
</Button>
|
||||
</Space>
|
||||
<Table
|
||||
dataSource={dicts}
|
||||
columns={dictColumns}
|
||||
rowKey="id"
|
||||
loading={loadingDicts}
|
||||
onRow={record => ({
|
||||
onRow={(record) => ({
|
||||
onClick: () => {
|
||||
// 如果点击的是当前已选中的行,则取消选择
|
||||
if (selectedDict?.id === record.id) {
|
||||
|
|
@ -274,24 +327,30 @@ const DictPage: React.FC = () => {
|
|||
}
|
||||
},
|
||||
})}
|
||||
rowClassName={record => (selectedDict?.id === record.id ? 'ant-table-row-selected' : '')}
|
||||
rowClassName={(record) =>
|
||||
selectedDict?.id === record.id ? 'ant-table-row-selected' : ''
|
||||
}
|
||||
pagination={false}
|
||||
/>
|
||||
</Space>
|
||||
</Sider>
|
||||
<Content style={{ padding: '16px' }}>
|
||||
<Space direction="vertical" style={{ width: '100%' }}>
|
||||
<Button type="primary" onClick={handleAddDictItem} disabled={!selectedDict}>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={handleAddDictItem}
|
||||
disabled={!selectedDict}
|
||||
>
|
||||
添加字典项
|
||||
</Button>
|
||||
<Space>
|
||||
<Upload
|
||||
name="file"
|
||||
action={`/dict/item/import`}
|
||||
action={`/dict/item/import`}
|
||||
data={{ dictId: selectedDict?.id }}
|
||||
showUploadList={false}
|
||||
disabled={!selectedDict}
|
||||
onChange={info => {
|
||||
onChange={(info) => {
|
||||
if (info.file.status === 'done') {
|
||||
message.success(`${info.file.name} 文件上传成功`);
|
||||
fetchDictItems(selectedDict.id);
|
||||
|
|
@ -300,11 +359,23 @@ const DictPage: React.FC = () => {
|
|||
}
|
||||
}}
|
||||
>
|
||||
<Button icon={<UploadOutlined />} disabled={!selectedDict}>导入字典项</Button>
|
||||
<Button icon={<UploadOutlined />} disabled={!selectedDict}>
|
||||
导入字典项
|
||||
</Button>
|
||||
</Upload>
|
||||
<Button onClick={() => window.open('/dict/item/template')} disabled={!selectedDict}>下载模板</Button>
|
||||
<Button
|
||||
onClick={() => window.open('/dict/item/template')}
|
||||
disabled={!selectedDict}
|
||||
>
|
||||
下载模板
|
||||
</Button>
|
||||
</Space>
|
||||
<Table dataSource={dictItems} columns={dictItemColumns} rowKey="id" loading={loadingDictItems} />
|
||||
<Table
|
||||
dataSource={dictItems}
|
||||
columns={dictItemColumns}
|
||||
rowKey="id"
|
||||
loading={loadingDictItems}
|
||||
/>
|
||||
</Space>
|
||||
</Content>
|
||||
</Layout>
|
||||
|
|
@ -317,13 +388,31 @@ const DictPage: React.FC = () => {
|
|||
>
|
||||
<Form layout="vertical">
|
||||
<Form.Item label="名称">
|
||||
<Input placeholder="名称 (e.g., zyn)" value={dictItemForm.name} onChange={e => setDictItemForm({ ...dictItemForm, name: e.target.value })} />
|
||||
<Input
|
||||
placeholder="名称 (e.g., zyn)"
|
||||
value={dictItemForm.name}
|
||||
onChange={(e) =>
|
||||
setDictItemForm({ ...dictItemForm, name: e.target.value })
|
||||
}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label="标题">
|
||||
<Input placeholder="标题 (e.g., ZYN)" value={dictItemForm.title} onChange={e => setDictItemForm({ ...dictItemForm, title: e.target.value })} />
|
||||
<Input
|
||||
placeholder="标题 (e.g., ZYN)"
|
||||
value={dictItemForm.title}
|
||||
onChange={(e) =>
|
||||
setDictItemForm({ ...dictItemForm, title: e.target.value })
|
||||
}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label="值 (可选)">
|
||||
<Input placeholder="值 (可选)" value={dictItemForm.value} onChange={e => setDictItemForm({ ...dictItemForm, value: e.target.value })} />
|
||||
<Input
|
||||
placeholder="值 (可选)"
|
||||
value={dictItemForm.value}
|
||||
onChange={(e) =>
|
||||
setDictItemForm({ ...dictItemForm, value: e.target.value })
|
||||
}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
|
@ -336,10 +425,18 @@ const DictPage: React.FC = () => {
|
|||
>
|
||||
<Form layout="vertical">
|
||||
<Form.Item label="字典名称">
|
||||
<Input placeholder="字典名称 (e.g., brand)" value={newDictName} onChange={e => setNewDictName(e.target.value)} />
|
||||
<Input
|
||||
placeholder="字典名称 (e.g., brand)"
|
||||
value={newDictName}
|
||||
onChange={(e) => setNewDictName(e.target.value)}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label="字典标题">
|
||||
<Input placeholder="字典标题 (e.g., 品牌)" value={newDictTitle} onChange={e => setNewDictTitle(e.target.value)} />
|
||||
<Input
|
||||
placeholder="字典标题 (e.g., 品牌)"
|
||||
value={newDictTitle}
|
||||
onChange={(e) => setNewDictTitle(e.target.value)}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { useDeviceFingerprint } from '@/hooks/useDeviceFingerprint';
|
||||
import { usercontrollerGetuser, usercontrollerLogin } from '@/servers/api/user';
|
||||
import { LockOutlined, UserOutlined } from '@ant-design/icons';
|
||||
import {
|
||||
|
|
@ -7,7 +8,6 @@ import {
|
|||
} from '@ant-design/pro-components';
|
||||
import { history, useModel } from '@umijs/max';
|
||||
import { App, theme } from 'antd';
|
||||
import {useDeviceFingerprint} from '@/hooks/useDeviceFingerprint';
|
||||
import { useState } from 'react';
|
||||
|
||||
const Page = () => {
|
||||
|
|
@ -15,28 +15,32 @@ const Page = () => {
|
|||
const { token } = theme.useToken();
|
||||
const { message } = App.useApp();
|
||||
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 }) => {
|
||||
try {
|
||||
const { data, success, code, message: msg } = await usercontrollerLogin({...values, deviceId});
|
||||
const {
|
||||
data,
|
||||
success,
|
||||
code,
|
||||
message: msg,
|
||||
} = await usercontrollerLogin({ ...values, deviceId });
|
||||
if (success) {
|
||||
message.success('登录成功');
|
||||
localStorage.setItem('token', data?.token as string);
|
||||
const { data: user } = await usercontrollerGetuser();
|
||||
setInitialState({ user });
|
||||
history.push('/');
|
||||
return
|
||||
return;
|
||||
}
|
||||
if(code === 10001){
|
||||
message.info("验证码已发送至管理邮箱")
|
||||
if (code === 10001) {
|
||||
message.info('验证码已发送至管理邮箱');
|
||||
setIsAuth(true);
|
||||
return;
|
||||
}
|
||||
message.error(msg);
|
||||
|
||||
} catch {
|
||||
message.error('登录失败');
|
||||
}
|
||||
|
|
@ -100,16 +104,17 @@ const Page = () => {
|
|||
},
|
||||
]}
|
||||
/>
|
||||
{
|
||||
isAuth?
|
||||
{isAuth ? (
|
||||
<ProFormText
|
||||
name="authCode"
|
||||
label="验证码"
|
||||
width="lg"
|
||||
placeholder="请输入验证码"
|
||||
rules={[{ required: true, message: '请输入验证码' }]}
|
||||
/>:<></>
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
{/* <div
|
||||
style={{
|
||||
marginBlockEnd: 24,
|
||||
|
|
|
|||
|
|
@ -1,12 +1,14 @@
|
|||
import { logisticscontrollerGetlist, logisticscontrollerGetshipmentlabel,
|
||||
import {
|
||||
logisticscontrollerDeleteshipment,
|
||||
logisticscontrollerUpdateshipmentstate
|
||||
} from '@/servers/api/logistics';
|
||||
logisticscontrollerGetlist,
|
||||
logisticscontrollerGetshipmentlabel,
|
||||
logisticscontrollerUpdateshipmentstate,
|
||||
} from '@/servers/api/logistics';
|
||||
import { sitecontrollerAll } from '@/servers/api/site';
|
||||
import { stockcontrollerGetallstockpoints } from '@/servers/api/stock';
|
||||
import { formatUniuniShipmentState } from '@/utils/format';
|
||||
import { printPDF } from '@/utils/util';
|
||||
import { CopyOutlined } from '@ant-design/icons';
|
||||
import { ToastContainer, toast } from 'react-toastify';
|
||||
import {
|
||||
ActionType,
|
||||
PageContainer,
|
||||
|
|
@ -15,7 +17,7 @@ import {
|
|||
} from '@ant-design/pro-components';
|
||||
import { App, Button, Divider, Popconfirm } from 'antd';
|
||||
import { useRef, useState } from 'react';
|
||||
import { sitecontrollerAll } from '@/servers/api/site';
|
||||
import { ToastContainer } from 'react-toastify';
|
||||
|
||||
const ListPage: React.FC = () => {
|
||||
const actionRef = useRef<ActionType>();
|
||||
|
|
@ -69,7 +71,9 @@ const ListPage: React.FC = () => {
|
|||
<CopyOutlined
|
||||
onClick={async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(record.return_tracking_number);
|
||||
await navigator.clipboard.writeText(
|
||||
record.return_tracking_number,
|
||||
);
|
||||
message.success('复制成功!');
|
||||
} catch (err) {
|
||||
message.error('复制失败!');
|
||||
|
|
@ -106,7 +110,9 @@ const ListPage: React.FC = () => {
|
|||
disabled={isLoading}
|
||||
onClick={async () => {
|
||||
setIsLoading(true);
|
||||
const { data } = await logisticscontrollerGetshipmentlabel({shipmentId:record.id});
|
||||
const { data } = await logisticscontrollerGetshipmentlabel({
|
||||
shipmentId: record.id,
|
||||
});
|
||||
const content = data.content;
|
||||
printPDF([content]);
|
||||
setIsLoading(false);
|
||||
|
|
@ -120,7 +126,9 @@ const ListPage: React.FC = () => {
|
|||
disabled={isLoading}
|
||||
onClick={async () => {
|
||||
setIsLoading(true);
|
||||
const res = await logisticscontrollerUpdateshipmentstate({shipmentId:record.id});
|
||||
const res = await logisticscontrollerUpdateshipmentstate({
|
||||
shipmentId: record.id,
|
||||
});
|
||||
console.log('res', res);
|
||||
|
||||
setIsLoading(false);
|
||||
|
|
@ -137,7 +145,7 @@ const ListPage: React.FC = () => {
|
|||
try {
|
||||
setIsLoading(true);
|
||||
const { success, message: errMsg } =
|
||||
await logisticscontrollerDeleteshipment({id:record.id});
|
||||
await logisticscontrollerDeleteshipment({ id: record.id });
|
||||
if (!success) {
|
||||
throw new Error(errMsg);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import {
|
||||
logisticscontrollerGetservicelist,
|
||||
logisticscontrollerSyncservices,
|
||||
logisticscontrollerToggleactive,
|
||||
} from '@/servers/api/logistics';
|
||||
import {
|
||||
|
|
@ -10,7 +9,7 @@ import {
|
|||
ProFormSwitch,
|
||||
ProTable,
|
||||
} from '@ant-design/pro-components';
|
||||
import { App, Button } from 'antd';
|
||||
import { App } from 'antd';
|
||||
import { useRef } from 'react';
|
||||
|
||||
const ListPage: React.FC = () => {
|
||||
|
|
|
|||
|
|
@ -1,11 +1,15 @@
|
|||
import React, { useRef } 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 } from 'antd';
|
||||
import dayjs from 'dayjs';
|
||||
import { ordercontrollerGetordersales } 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 } from 'antd';
|
||||
import dayjs from 'dayjs';
|
||||
import React, { useRef } from 'react';
|
||||
|
||||
// 列表行数据结构(订单商品聚合)
|
||||
interface OrderItemAggRow {
|
||||
|
|
@ -87,7 +91,10 @@ const OrderItemsPage: React.FC = () => {
|
|||
request: async () => {
|
||||
// 拉取站点列表(后台 /site/all)
|
||||
const { data = [] } = await sitecontrollerAll();
|
||||
return (data || []).map((item: any) => ({ label: item.siteName, value: item.id }));
|
||||
return (data || []).map((item: any) => ({
|
||||
label: item.siteName,
|
||||
value: item.id,
|
||||
}));
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
@ -104,7 +111,9 @@ const OrderItemsPage: React.FC = () => {
|
|||
];
|
||||
|
||||
// 表格请求方法:调用 /order/getOrderSales 接口并设置 isSource=true 获取订单项聚合
|
||||
const request: ProTableProps<OrderItemAggRow>['request'] = async (params:any) => {
|
||||
const request: ProTableProps<OrderItemAggRow>['request'] = async (
|
||||
params: any,
|
||||
) => {
|
||||
try {
|
||||
const { current = 1, pageSize = 10, siteId, name } = params as any;
|
||||
const [startDate, endDate] = (params as any).dateRange || [];
|
||||
|
|
@ -115,7 +124,9 @@ const OrderItemsPage: React.FC = () => {
|
|||
siteId,
|
||||
name,
|
||||
isSource: true as any,
|
||||
startDate: startDate ? (dayjs(startDate).toISOString() as any) : undefined,
|
||||
startDate: startDate
|
||||
? (dayjs(startDate).toISOString() as any)
|
||||
: undefined,
|
||||
endDate: endDate ? (dayjs(endDate).toISOString() as any) : undefined,
|
||||
} as any);
|
||||
const { success, data, message: errMsg } = resp as any;
|
||||
|
|
@ -132,10 +143,12 @@ const OrderItemsPage: React.FC = () => {
|
|||
};
|
||||
|
||||
return (
|
||||
<PageContainer title='订单商品概览'>
|
||||
<PageContainer title="订单商品概览">
|
||||
<ProTable<OrderItemAggRow>
|
||||
actionRef={actionRef}
|
||||
rowKey={(r) => `${r.externalProductId}-${r.externalVariationId}-${r.name}`}
|
||||
rowKey={(r) =>
|
||||
`${r.externalProductId}-${r.externalVariationId}-${r.name}`
|
||||
}
|
||||
columns={columns}
|
||||
request={request}
|
||||
pagination={{ showSizeChanger: true }}
|
||||
|
|
@ -146,4 +159,4 @@ const OrderItemsPage: React.FC = () => {
|
|||
);
|
||||
};
|
||||
|
||||
export default OrderItemsPage;
|
||||
export default OrderItemsPage;
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -86,15 +86,13 @@ const List: React.FC = () => {
|
|||
|
||||
return (
|
||||
<PageContainer header={{ title: '品牌列表' }}>
|
||||
<ProTable<API.Brand>
|
||||
<ProTable<any>
|
||||
headerTitle="查询表格"
|
||||
actionRef={actionRef}
|
||||
rowKey="id"
|
||||
toolBarRender={() => [<CreateForm tableRef={actionRef} />]}
|
||||
request={async (params) => {
|
||||
const { data, success } = await productcontrollerGetbrands(
|
||||
params,
|
||||
);
|
||||
const { data, success } = await productcontrollerGetbrands(params);
|
||||
return {
|
||||
total: data?.total || 0,
|
||||
data: data?.items || [],
|
||||
|
|
@ -26,24 +26,33 @@ const NameCn: React.FC<{
|
|||
id: number;
|
||||
value: string | undefined;
|
||||
tableRef: React.MutableRefObject<ActionType | undefined>;
|
||||
}> = ({value,tableRef, id}) => {
|
||||
}> = ({ value, tableRef, id }) => {
|
||||
const { message } = App.useApp();
|
||||
const [editable, setEditable] = React.useState<boolean>(false);
|
||||
if (!editable) return <div onClick={() => setEditable(true)}>{value||'-'}</div>;
|
||||
return <ProFormText initialValue={value} fieldProps={{autoFocus:true, onBlur:async(e: React.FocusEvent<HTMLInputElement>) => {
|
||||
if(!e.target.value) return setEditable(false)
|
||||
const { success, message: errMsg } =
|
||||
await productcontrollerUpdateproductnamecn({
|
||||
id,
|
||||
nameCn: e.target.value,
|
||||
})
|
||||
setEditable(false)
|
||||
if (!success) {
|
||||
return message.error(errMsg)
|
||||
}
|
||||
tableRef?.current?.reload()
|
||||
}}} />
|
||||
}
|
||||
if (!editable)
|
||||
return <div onClick={() => setEditable(true)}>{value || '-'}</div>;
|
||||
return (
|
||||
<ProFormText
|
||||
initialValue={value}
|
||||
fieldProps={{
|
||||
autoFocus: true,
|
||||
onBlur: async (e: React.FocusEvent<HTMLInputElement>) => {
|
||||
if (!e.target.value) return setEditable(false);
|
||||
const { success, message: errMsg } =
|
||||
await productcontrollerUpdateproductnamecn({
|
||||
id,
|
||||
nameCn: e.target.value,
|
||||
});
|
||||
setEditable(false);
|
||||
if (!success) {
|
||||
return message.error(errMsg);
|
||||
}
|
||||
tableRef?.current?.reload();
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
const List: React.FC = () => {
|
||||
const actionRef = useRef<ActionType>();
|
||||
// 状态:存储当前选中的行
|
||||
|
|
@ -58,10 +67,10 @@ const List: React.FC = () => {
|
|||
{
|
||||
title: '中文名',
|
||||
dataIndex: 'nameCn',
|
||||
render: (_, record) => {
|
||||
render: (_, record) => {
|
||||
return (
|
||||
<NameCn value={record.nameCn} id={record.id} tableRef={actionRef} />
|
||||
)
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
@ -149,7 +158,7 @@ const List: React.FC = () => {
|
|||
success,
|
||||
};
|
||||
}}
|
||||
columns={columns}
|
||||
columns={columns}
|
||||
editable={{
|
||||
type: 'single',
|
||||
onSave: async (key, record, originRow) => {
|
||||
|
|
@ -215,7 +224,7 @@ const CreateForm: React.FC<{
|
|||
width="lg"
|
||||
label="产品品牌"
|
||||
placeholder="请选择产品品牌"
|
||||
request={async () => {
|
||||
request={async () => {
|
||||
const { data = [] } = await productcontrollerGetbrandall();
|
||||
return data.map((item: API.Brand) => ({
|
||||
label: item.name,
|
||||
|
|
|
|||
|
|
@ -262,9 +262,9 @@ const UpdateStatus: React.FC<{
|
|||
{
|
||||
id: initialValues.id,
|
||||
},
|
||||
{
|
||||
{
|
||||
status,
|
||||
stock_status
|
||||
stock_status,
|
||||
},
|
||||
);
|
||||
if (!success) {
|
||||
|
|
@ -285,7 +285,7 @@ const UpdateStatus: React.FC<{
|
|||
name="status"
|
||||
valueEnum={PRODUCT_STATUS_ENUM}
|
||||
/>
|
||||
<ProFormSelect
|
||||
<ProFormSelect
|
||||
label="上下架状态"
|
||||
width="lg"
|
||||
name="stock_status"
|
||||
|
|
@ -296,7 +296,6 @@ const UpdateStatus: React.FC<{
|
|||
);
|
||||
};
|
||||
|
||||
|
||||
const UpdateForm: React.FC<{
|
||||
tableRef: React.MutableRefObject<ActionType | undefined>;
|
||||
values: API.WpProductDTO;
|
||||
|
|
|
|||
|
|
@ -1,12 +1,11 @@
|
|||
|
||||
import React, { useState } from 'react';
|
||||
import { UploadOutlined } from '@ant-design/icons';
|
||||
import {
|
||||
PageContainer,
|
||||
ProForm,
|
||||
ProFormSelect,
|
||||
} from '@ant-design/pro-components';
|
||||
import { Button, Card, Col, Input, message, Row, Upload } from 'antd';
|
||||
import { UploadOutlined } from '@ant-design/icons';
|
||||
import React, { useState } from 'react';
|
||||
import * as XLSX from 'xlsx';
|
||||
|
||||
// 定义配置接口
|
||||
|
|
@ -22,7 +21,10 @@ interface TagConfig {
|
|||
/**
|
||||
* @description 从产品名称中解析出品牌、口味、毫克含量和干燥度
|
||||
*/
|
||||
const parseName = (name: string, brands: string[]): [string, string, string, string] => {
|
||||
const parseName = (
|
||||
name: string,
|
||||
brands: string[],
|
||||
): [string, string, string, string] => {
|
||||
const nm = name.trim();
|
||||
const dryMatch = nm.match(/\(([^)]*)\)/);
|
||||
const dryness = dryMatch ? dryMatch[1].trim() : '';
|
||||
|
|
@ -75,11 +77,18 @@ const splitFlavorTokens = (flavorPart: string): string[] => {
|
|||
/**
|
||||
* @description 根据口味分类额外的标签(如 Fruit, Mint)
|
||||
*/
|
||||
const classifyExtraTags = (flavorPart: string, fruitKeys: string[], mintKeys: string[]): string[] => {
|
||||
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)) || tokens.some(t => fruitKeys.includes(t));
|
||||
const isMint = mintKeys.some(key => fLower.includes(key)) || tokens.includes('mint');
|
||||
const isFruit =
|
||||
fruitKeys.some((key) => fLower.includes(key)) ||
|
||||
tokens.some((t) => fruitKeys.includes(t));
|
||||
const isMint =
|
||||
mintKeys.some((key) => fLower.includes(key)) || tokens.includes('mint');
|
||||
|
||||
const extras: string[] = [];
|
||||
if (isFruit) extras.push('Fruit');
|
||||
|
|
@ -94,8 +103,12 @@ const computeTags = (name: string, sku: string, config: TagConfig): string => {
|
|||
const [brand, flavorPart, mg, dryness] = parseName(name, config.brands);
|
||||
const tokens = splitFlavorTokens(flavorPart);
|
||||
|
||||
const tokensForFlavor = tokens.filter(t => !config.nonFlavorTokens.includes(t));
|
||||
const flavorTag = tokensForFlavor.map(t => t.charAt(0).toUpperCase() + t.slice(1)).join('');
|
||||
const tokensForFlavor = tokens.filter(
|
||||
(t) => !config.nonFlavorTokens.includes(t),
|
||||
);
|
||||
const flavorTag = tokensForFlavor
|
||||
.map((t) => t.charAt(0).toUpperCase() + t.slice(1))
|
||||
.join('');
|
||||
|
||||
let tags: string[] = [];
|
||||
if (brand) tags.push(brand);
|
||||
|
|
@ -133,11 +146,13 @@ const computeTags = (name: string, sku: string, config: TagConfig): string => {
|
|||
}
|
||||
}
|
||||
|
||||
tags.push(...classifyExtraTags(flavorPart, config.fruitKeys, config.mintKeys));
|
||||
tags.push(
|
||||
...classifyExtraTags(flavorPart, config.fruitKeys, config.mintKeys),
|
||||
);
|
||||
|
||||
// 去重并保留顺序
|
||||
const seen = new Set<string>();
|
||||
const finalTags = tags.filter(t => {
|
||||
const finalTags = tags.filter((t) => {
|
||||
if (t && !seen.has(t)) {
|
||||
seen.add(t);
|
||||
return true;
|
||||
|
|
@ -152,9 +167,22 @@ const computeTags = (name: string, sku: string, config: TagConfig): string => {
|
|||
const DEFAULT_CONFIG = {
|
||||
brands: ['YOONE', 'ZYN', 'ZEX', 'JUX', 'WHITE FOX'],
|
||||
fruitKeys: [
|
||||
'apple', 'blueberry', 'citrus', 'mango', 'peach', 'grape', 'cherry',
|
||||
'strawberry', 'watermelon', 'orange', 'lemon', 'lemonade',
|
||||
'razz', 'pineapple', 'berry', 'fruit',
|
||||
'apple',
|
||||
'blueberry',
|
||||
'citrus',
|
||||
'mango',
|
||||
'peach',
|
||||
'grape',
|
||||
'cherry',
|
||||
'strawberry',
|
||||
'watermelon',
|
||||
'orange',
|
||||
'lemon',
|
||||
'lemonade',
|
||||
'razz',
|
||||
'pineapple',
|
||||
'berry',
|
||||
'fruit',
|
||||
],
|
||||
mintKeys: ['mint', 'wintergreen', 'peppermint', 'spearmint', 'menthol'],
|
||||
nonFlavorTokens: ['slim', 'pouches', 'pouch', 'mini', 'dry'],
|
||||
|
|
@ -242,7 +270,7 @@ const WpToolPage: React.FC = () => {
|
|||
try {
|
||||
// 获取表单中的最新配置
|
||||
const config = await form.validateFields();
|
||||
|
||||
|
||||
const { brands, fruitKeys, mintKeys, nonFlavorTokens } = config;
|
||||
|
||||
// 确保品牌按长度降序排序
|
||||
|
|
@ -266,10 +294,15 @@ const WpToolPage: React.FC = () => {
|
|||
});
|
||||
|
||||
setProcessedData(dataWithTags);
|
||||
message.success({ content: 'Tags 生成成功!现在可以下载了。', key: 'processing' });
|
||||
|
||||
message.success({
|
||||
content: 'Tags 生成成功!现在可以下载了。',
|
||||
key: 'processing',
|
||||
});
|
||||
} catch (error) {
|
||||
message.error({ content: '处理失败,请检查配置或文件。', key: 'processing' });
|
||||
message.error({
|
||||
content: '处理失败,请检查配置或文件。',
|
||||
key: 'processing',
|
||||
});
|
||||
console.error('Processing Error:', error);
|
||||
} finally {
|
||||
setIsProcessing(false);
|
||||
|
|
@ -368,7 +401,9 @@ const WpToolPage: React.FC = () => {
|
|||
<Button icon={<UploadOutlined />}>选择 CSV 文件</Button>
|
||||
</Upload>
|
||||
<div style={{ marginTop: 16 }}>
|
||||
<label style={{ display: 'block', marginBottom: 8 }}>已上传文件</label>
|
||||
<label style={{ display: 'block', marginBottom: 8 }}>
|
||||
已上传文件
|
||||
</label>
|
||||
<Input value={file ? file.name : '暂未选择文件'} readOnly />
|
||||
</div>
|
||||
<Button
|
||||
|
|
|
|||
|
|
@ -1,8 +1,16 @@
|
|||
import React, { useEffect, useRef, useState } from 'react';
|
||||
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 {
|
||||
ActionType,
|
||||
DrawerForm,
|
||||
ProColumns,
|
||||
ProFormInstance,
|
||||
ProFormSelect,
|
||||
ProFormSwitch,
|
||||
ProFormText,
|
||||
ProTable,
|
||||
} from '@ant-design/pro-components';
|
||||
import { request } from '@umijs/max';
|
||||
import { Button, message, Popconfirm, Space, Tag } from 'antd';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
|
||||
// 站点数据项类型(前端不包含密钥字段,后端列表不返回密钥)
|
||||
interface SiteItem {
|
||||
|
|
@ -58,10 +66,21 @@ const SiteList: React.FC = () => {
|
|||
|
||||
// 表格列定义
|
||||
const columns: ProColumns<SiteItem>[] = [
|
||||
{ title: 'ID', dataIndex: 'id', width: 80, sorter: true, hideInSearch: true },
|
||||
{
|
||||
title: 'ID',
|
||||
dataIndex: 'id',
|
||||
width: 80,
|
||||
sorter: true,
|
||||
hideInSearch: true,
|
||||
},
|
||||
{ title: '站点名称', dataIndex: 'siteName', width: 220 },
|
||||
{ title: 'API 地址', dataIndex: 'apiUrl', width: 280, hideInSearch: true },
|
||||
{ title: 'SKU 前缀', dataIndex: 'skuPrefix', width: 160, hideInSearch: true },
|
||||
{
|
||||
title: 'SKU 前缀',
|
||||
dataIndex: 'skuPrefix',
|
||||
width: 160,
|
||||
hideInSearch: true,
|
||||
},
|
||||
{
|
||||
title: '平台',
|
||||
dataIndex: 'type',
|
||||
|
|
@ -101,7 +120,9 @@ const SiteList: React.FC = () => {
|
|||
</Button>
|
||||
<Popconfirm
|
||||
title={row.isDisabled ? '启用站点' : '禁用站点'}
|
||||
description={row.isDisabled ? '确认启用该站点?' : '确认禁用该站点?'}
|
||||
description={
|
||||
row.isDisabled ? '确认启用该站点?' : '确认禁用该站点?'
|
||||
}
|
||||
onConfirm={async () => {
|
||||
try {
|
||||
await request(`/site/disable/${row.id}`, {
|
||||
|
|
@ -159,7 +180,9 @@ const SiteList: React.FC = () => {
|
|||
...(values.siteName ? { siteName: values.siteName } : {}),
|
||||
...(values.apiUrl ? { apiUrl: values.apiUrl } : {}),
|
||||
...(values.type ? { type: values.type } : {}),
|
||||
...(typeof values.isDisabled === 'boolean' ? { isDisabled: values.isDisabled } : {}),
|
||||
...(typeof values.isDisabled === 'boolean'
|
||||
? { isDisabled: values.isDisabled }
|
||||
: {}),
|
||||
...(values.skuPrefix ? { skuPrefix: values.skuPrefix } : {}),
|
||||
};
|
||||
// 仅当输入了新密钥时才提交,未输入则保持原本值
|
||||
|
|
@ -169,7 +192,10 @@ const SiteList: React.FC = () => {
|
|||
if (values.consumerSecret && values.consumerSecret.trim()) {
|
||||
payload.consumerSecret = values.consumerSecret.trim();
|
||||
}
|
||||
await request(`/site/update/${editing.id}`, { method: 'PUT', data: payload });
|
||||
await request(`/site/update/${editing.id}`, {
|
||||
method: 'PUT',
|
||||
data: payload,
|
||||
});
|
||||
} else {
|
||||
// 新增站点时要求填写 consumerKey 和 consumerSecret
|
||||
if (!values.consumerKey || !values.consumerSecret) {
|
||||
|
|
@ -218,7 +244,7 @@ const SiteList: React.FC = () => {
|
|||
</Button>,
|
||||
// 同步包括 orders subscriptions 等等
|
||||
// <Button key='new' type='primary' onClick={()=> {
|
||||
//
|
||||
//
|
||||
// }}}>
|
||||
// 同步站点数据
|
||||
// </Button>
|
||||
|
|
@ -233,9 +259,18 @@ const SiteList: React.FC = () => {
|
|||
onFinish={handleSubmit}
|
||||
>
|
||||
{/* 站点名称,必填 */}
|
||||
<ProFormText name="siteName" label="站点名称" placeholder="例如:本地商店" rules={[{ required: true, message: '站点名称为必填项' }]} />
|
||||
<ProFormText
|
||||
name="siteName"
|
||||
label="站点名称"
|
||||
placeholder="例如:本地商店"
|
||||
rules={[{ required: true, message: '站点名称为必填项' }]}
|
||||
/>
|
||||
{/* API 地址,可选 */}
|
||||
<ProFormText name="apiUrl" label="API 地址" placeholder="例如:https://shop.example.com" />
|
||||
<ProFormText
|
||||
name="apiUrl"
|
||||
label="API 地址"
|
||||
placeholder="例如:https://shop.example.com"
|
||||
/>
|
||||
{/* 平台类型选择 */}
|
||||
<ProFormSelect
|
||||
name="type"
|
||||
|
|
@ -247,14 +282,30 @@ const SiteList: React.FC = () => {
|
|||
/>
|
||||
{/* 是否禁用 */}
|
||||
<ProFormSwitch name="isDisabled" label="禁用" />
|
||||
<ProFormText name="skuPrefix" label="SKU 前缀" placeholder={editing ? '留空表示不修改' : '可选'} />
|
||||
<ProFormText
|
||||
name="skuPrefix"
|
||||
label="SKU 前缀"
|
||||
placeholder={editing ? '留空表示不修改' : '可选'}
|
||||
/>
|
||||
{/* WooCommerce REST consumer key;新增必填,编辑不填则保持原值 */}
|
||||
<ProFormText name="consumerKey" label="Key" placeholder={editing ? '留空表示不修改' : '必填'} rules={editing ? [] : [{ required: true, message: '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 为必填项' }]} />
|
||||
<ProFormText
|
||||
name="consumerSecret"
|
||||
label="Secret"
|
||||
placeholder={editing ? '留空表示不修改' : '必填'}
|
||||
rules={
|
||||
editing ? [] : [{ required: true, message: 'Secret 为必填项' }]
|
||||
}
|
||||
/>
|
||||
</DrawerForm>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default SiteList;
|
||||
export default SiteList;
|
||||
|
|
|
|||
|
|
@ -1,257 +1,279 @@
|
|||
import React, { useEffect, useState, useMemo, useRef } from "react"
|
||||
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||
|
||||
import {
|
||||
ActionType,
|
||||
PageContainer, ProColumns, ProTable,
|
||||
} from '@ant-design/pro-components';
|
||||
import { statisticscontrollerGetordersorce, statisticscontrollerGetinativeusersbymonth } from "@/servers/api/statistics";
|
||||
import ReactECharts from 'echarts-for-react';
|
||||
import { App, Button, Space, Tag } from 'antd';
|
||||
import { HistoryOrder } from "../Order";
|
||||
statisticscontrollerGetinativeusersbymonth,
|
||||
statisticscontrollerGetordersorce,
|
||||
} from '@/servers/api/statistics';
|
||||
import {
|
||||
ActionType,
|
||||
PageContainer,
|
||||
ProColumns,
|
||||
ProTable,
|
||||
} from '@ant-design/pro-components';
|
||||
import { Space, Tag } from 'antd';
|
||||
import dayjs from 'dayjs';
|
||||
import ReactECharts from 'echarts-for-react';
|
||||
import { HistoryOrder } from '../Order';
|
||||
const ListPage: React.FC = () => {
|
||||
const [data, setData] = useState({});
|
||||
|
||||
const [data, setData] = useState({});
|
||||
useEffect(() => {
|
||||
statisticscontrollerGetordersorce().then(({ data, success }) => {
|
||||
if (success) setData(data);
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
statisticscontrollerGetordersorce().then(({ data, success }) => {
|
||||
if(success) setData(data)
|
||||
});
|
||||
}, []);
|
||||
|
||||
const option = useMemo(() => {
|
||||
if(!data.inactiveRes) return {}
|
||||
const xAxisData = data?.inactiveRes?.map(v=> v.order_month)?.sort(_=>-1)
|
||||
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 = [
|
||||
{
|
||||
name: '新客户',
|
||||
type: 'bar',
|
||||
data: data?.inactiveRes?.map(v=> v.new_user_count)?.sort(_=>-1),
|
||||
label: {
|
||||
show: true,
|
||||
},
|
||||
emphasis: {
|
||||
focus: 'series'
|
||||
},
|
||||
xAxisIndex: 0,
|
||||
yAxisIndex: 0,
|
||||
},
|
||||
{
|
||||
name: '老客户',
|
||||
type: 'bar',
|
||||
data: data?.inactiveRes?.map(v=> v.old_user_count)?.sort(_=>-1),
|
||||
label: {
|
||||
show: true,
|
||||
},
|
||||
emphasis: {
|
||||
focus: 'series'
|
||||
},
|
||||
xAxisIndex: 0,
|
||||
yAxisIndex: 0,
|
||||
},
|
||||
...uniqueArr?.map(v => {
|
||||
data?.res?.filter(item => item.order_month === v)
|
||||
return {
|
||||
name: v,
|
||||
type: "bar",
|
||||
stack: "total",
|
||||
label: {
|
||||
"show": true,
|
||||
formatter: function(params) {
|
||||
if(!params.value) return ''
|
||||
return Math.abs(params.value)
|
||||
},
|
||||
color: '#fff'
|
||||
},
|
||||
"data": xAxisData.map(month => {
|
||||
return (data?.res?.find(item => item.order_month === month && item.first_order_month_group === v)?.order_count || 0)
|
||||
}),
|
||||
xAxisIndex: 0,
|
||||
yAxisIndex: 0,
|
||||
}
|
||||
}),
|
||||
{
|
||||
name: '未复购客户',
|
||||
type: 'bar',
|
||||
data: data?.inactiveRes?.map(v=> -v.inactive_user_count)?.sort(_=>-1),
|
||||
stack: "total",
|
||||
label: {
|
||||
show: true,
|
||||
},
|
||||
emphasis: {
|
||||
focus: 'series'
|
||||
},
|
||||
xAxisIndex: 1,
|
||||
yAxisIndex: 1,
|
||||
barWidth: "60%",
|
||||
|
||||
itemStyle: {
|
||||
color: '#f44336'
|
||||
}
|
||||
},
|
||||
]
|
||||
return {
|
||||
grid: [
|
||||
{ top: '10%', height: '70%' },
|
||||
{ bottom: '10%', height: '10%' }
|
||||
],
|
||||
legend: {
|
||||
selectedMode: false
|
||||
},
|
||||
xAxis: [{
|
||||
type: 'category',
|
||||
data: xAxisData,
|
||||
gridIndex: 0,
|
||||
},{
|
||||
type: 'category',
|
||||
data: xAxisData,
|
||||
gridIndex: 1,
|
||||
}],
|
||||
yAxis: [{
|
||||
type: 'value',
|
||||
gridIndex: 0,
|
||||
},{
|
||||
type: 'value',
|
||||
gridIndex: 1,
|
||||
}],
|
||||
series,
|
||||
}
|
||||
}, [data])
|
||||
|
||||
const [tableData, setTableData] = useState<any[]>([])
|
||||
const actionRef = useRef<ActionType>();
|
||||
const columns: ProColumns[] = [
|
||||
const option = useMemo(() => {
|
||||
if (!data.inactiveRes) return {};
|
||||
const xAxisData = data?.inactiveRes
|
||||
?.map((v) => v.order_month)
|
||||
?.sort((_) => -1);
|
||||
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 = [
|
||||
{
|
||||
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;
|
||||
name: '新客户',
|
||||
type: 'bar',
|
||||
data: data?.inactiveRes?.map((v) => v.new_user_count)?.sort((_) => -1),
|
||||
label: {
|
||||
show: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '邮箱',
|
||||
dataIndex: 'email',
|
||||
},
|
||||
{
|
||||
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')
|
||||
: '-',
|
||||
},
|
||||
{
|
||||
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: '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: '标签',
|
||||
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>
|
||||
);
|
||||
emphasis: {
|
||||
focus: 'series',
|
||||
},
|
||||
xAxisIndex: 0,
|
||||
yAxisIndex: 0,
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
dataIndex: 'option',
|
||||
valueType: 'option',
|
||||
render: (_, record) => {
|
||||
return (
|
||||
<HistoryOrder
|
||||
email={record.email}
|
||||
tags={record.tags}
|
||||
tableRef={actionRef}
|
||||
/>
|
||||
);
|
||||
name: '老客户',
|
||||
type: 'bar',
|
||||
data: data?.inactiveRes?.map((v) => v.old_user_count)?.sort((_) => -1),
|
||||
label: {
|
||||
show: true,
|
||||
},
|
||||
emphasis: {
|
||||
focus: 'series',
|
||||
},
|
||||
xAxisIndex: 0,
|
||||
yAxisIndex: 0,
|
||||
},
|
||||
...uniqueArr?.map((v) => {
|
||||
data?.res?.filter((item) => item.order_month === v);
|
||||
return {
|
||||
name: v,
|
||||
type: 'bar',
|
||||
stack: 'total',
|
||||
label: {
|
||||
show: true,
|
||||
formatter: function (params) {
|
||||
if (!params.value) return '';
|
||||
return Math.abs(params.value);
|
||||
},
|
||||
color: '#fff',
|
||||
},
|
||||
data: xAxisData.map((month) => {
|
||||
return (
|
||||
data?.res?.find(
|
||||
(item) =>
|
||||
item.order_month === month &&
|
||||
item.first_order_month_group === v,
|
||||
)?.order_count || 0
|
||||
);
|
||||
}),
|
||||
xAxisIndex: 0,
|
||||
yAxisIndex: 0,
|
||||
};
|
||||
}),
|
||||
{
|
||||
name: '未复购客户',
|
||||
type: 'bar',
|
||||
data: data?.inactiveRes
|
||||
?.map((v) => -v.inactive_user_count)
|
||||
?.sort((_) => -1),
|
||||
stack: 'total',
|
||||
label: {
|
||||
show: true,
|
||||
},
|
||||
emphasis: {
|
||||
focus: 'series',
|
||||
},
|
||||
xAxisIndex: 1,
|
||||
yAxisIndex: 1,
|
||||
barWidth: '60%',
|
||||
|
||||
itemStyle: {
|
||||
color: '#f44336',
|
||||
},
|
||||
},
|
||||
];
|
||||
return(
|
||||
<PageContainer ghost>
|
||||
<ReactECharts
|
||||
option={option}
|
||||
style={{ height: 1050 }}
|
||||
onEvents={{
|
||||
click: async (params) => {
|
||||
if (params.componentType === 'series') {
|
||||
setTableData([])
|
||||
const {success, data} = await statisticscontrollerGetinativeusersbymonth({
|
||||
month: params.name
|
||||
})
|
||||
if(success) setTableData(data)
|
||||
}
|
||||
},
|
||||
}}
|
||||
/>
|
||||
{
|
||||
tableData?.length ?
|
||||
<ProTable
|
||||
search={false}
|
||||
headerTitle="查询表格"
|
||||
actionRef={actionRef}
|
||||
rowKey="id"
|
||||
dataSource={tableData}
|
||||
columns={columns}
|
||||
/>
|
||||
:<></>
|
||||
|
||||
}
|
||||
</PageContainer>
|
||||
)
|
||||
}
|
||||
return {
|
||||
grid: [
|
||||
{ top: '10%', height: '70%' },
|
||||
{ bottom: '10%', height: '10%' },
|
||||
],
|
||||
legend: {
|
||||
selectedMode: false,
|
||||
},
|
||||
xAxis: [
|
||||
{
|
||||
type: 'category',
|
||||
data: xAxisData,
|
||||
gridIndex: 0,
|
||||
},
|
||||
{
|
||||
type: 'category',
|
||||
data: xAxisData,
|
||||
gridIndex: 1,
|
||||
},
|
||||
],
|
||||
yAxis: [
|
||||
{
|
||||
type: 'value',
|
||||
gridIndex: 0,
|
||||
},
|
||||
{
|
||||
type: 'value',
|
||||
gridIndex: 1,
|
||||
},
|
||||
],
|
||||
series,
|
||||
};
|
||||
}, [data]);
|
||||
|
||||
export default ListPage;
|
||||
const [tableData, setTableData] = useState<any[]>([]);
|
||||
const actionRef = useRef<ActionType>();
|
||||
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: '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')
|
||||
: '-',
|
||||
},
|
||||
{
|
||||
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: '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: '标签',
|
||||
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',
|
||||
render: (_, record) => {
|
||||
return (
|
||||
<HistoryOrder
|
||||
email={record.email}
|
||||
tags={record.tags}
|
||||
tableRef={actionRef}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
return (
|
||||
<PageContainer ghost>
|
||||
<ReactECharts
|
||||
option={option}
|
||||
style={{ height: 1050 }}
|
||||
onEvents={{
|
||||
click: async (params) => {
|
||||
if (params.componentType === 'series') {
|
||||
setTableData([]);
|
||||
const { success, data } =
|
||||
await statisticscontrollerGetinativeusersbymonth({
|
||||
month: params.name,
|
||||
});
|
||||
if (success) setTableData(data);
|
||||
}
|
||||
},
|
||||
}}
|
||||
/>
|
||||
{tableData?.length ? (
|
||||
<ProTable
|
||||
search={false}
|
||||
headerTitle="查询表格"
|
||||
actionRef={actionRef}
|
||||
rowKey="id"
|
||||
dataSource={tableData}
|
||||
columns={columns}
|
||||
/>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</PageContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default ListPage;
|
||||
|
|
|
|||
|
|
@ -223,14 +223,17 @@ const ListPage: React.FC = () => {
|
|||
(yooneTotal.yoone12Quantity || 0) +
|
||||
(yooneTotal.yoone15Quantity || 0) +
|
||||
(yooneTotal.yoone18Quantity || 0) +
|
||||
(yooneTotal.zexQuantity || 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 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>
|
||||
|
|
|
|||
|
|
@ -427,9 +427,7 @@ const UpdateForm: React.FC<{
|
|||
<ProFormTextArea label="备注" name="note" width={'lg'} />
|
||||
<ProFormDependency name={['items']}>
|
||||
{({ items }) => {
|
||||
return (
|
||||
'数量:' + items?.reduce((acc, cur) => acc + cur.quantity, 0)
|
||||
);
|
||||
return '数量:' + items?.reduce((acc, cur) => acc + cur.quantity, 0);
|
||||
}}
|
||||
</ProFormDependency>
|
||||
<ProFormList<API.PurchaseOrderItem>
|
||||
|
|
@ -459,7 +457,7 @@ const UpdateForm: React.FC<{
|
|||
return (
|
||||
data?.map((item) => {
|
||||
return {
|
||||
label: `${item.name} - ${item.nameCn}`,
|
||||
label: `${item.name} - ${item.nameCn}`,
|
||||
value: item.sku,
|
||||
};
|
||||
}) || []
|
||||
|
|
@ -624,7 +622,7 @@ const DetailForm: React.FC<{
|
|||
return (
|
||||
data?.map((item) => {
|
||||
return {
|
||||
label: `${item.name} - ${item.nameCn}`,
|
||||
label: `${item.name} - ${item.nameCn}`,
|
||||
value: item.sku,
|
||||
};
|
||||
}) || []
|
||||
|
|
|
|||
|
|
@ -31,12 +31,12 @@ const ListPage: React.FC = () => {
|
|||
dataIndex: 'operationType',
|
||||
valueType: 'select',
|
||||
valueEnum: {
|
||||
'in': {
|
||||
text: '入库'
|
||||
in: {
|
||||
text: '入库',
|
||||
},
|
||||
out: {
|
||||
text: '出库',
|
||||
},
|
||||
"out": {
|
||||
text: '出库'
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -207,7 +207,7 @@ const CreateForm: React.FC<{
|
|||
drawerProps={{
|
||||
destroyOnHidden: true,
|
||||
}}
|
||||
onFinish={async ({orderNumber,...values}) => {
|
||||
onFinish={async ({ orderNumber, ...values }) => {
|
||||
try {
|
||||
const { success, message: errMsg } =
|
||||
await stockcontrollerCreatetransfer(values);
|
||||
|
|
@ -272,24 +272,38 @@ const CreateForm: React.FC<{
|
|||
rules={[{ required: true, message: '请选择源目标仓库' }]}
|
||||
/>
|
||||
<ProFormTextArea name="note" label="备注" />
|
||||
<ProFormText name={'orderNumber'} addonAfter={<Button onClick={async () => {
|
||||
const orderNumber = await form.getFieldValue('orderNumber')
|
||||
const { data } = await stockcontrollerGetpurchaseorder({orderNumber})
|
||||
form.setFieldsValue({
|
||||
items: data?.map(
|
||||
(item: { productName: string; productSku: string }) => ({
|
||||
...item,
|
||||
productSku: {
|
||||
label: item.productName,
|
||||
value: item.productSku,
|
||||
},
|
||||
}),
|
||||
),
|
||||
})
|
||||
}}>引用</Button>} />
|
||||
<ProFormText
|
||||
name={'orderNumber'}
|
||||
addonAfter={
|
||||
<Button
|
||||
onClick={async () => {
|
||||
const orderNumber = await form.getFieldValue('orderNumber');
|
||||
const { data } = await stockcontrollerGetpurchaseorder({
|
||||
orderNumber,
|
||||
});
|
||||
form.setFieldsValue({
|
||||
items: data?.map(
|
||||
(item: { productName: string; productSku: string }) => ({
|
||||
...item,
|
||||
productSku: {
|
||||
label: item.productName,
|
||||
value: item.productSku,
|
||||
},
|
||||
}),
|
||||
),
|
||||
});
|
||||
}}
|
||||
>
|
||||
引用
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<ProFormDependency name={['items']}>
|
||||
{({ items }) => {
|
||||
return '数量:' + (items?.reduce?.((acc, cur) => acc + cur.quantity, 0)||0);
|
||||
return (
|
||||
'数量:' +
|
||||
(items?.reduce?.((acc, cur) => acc + cur.quantity, 0) || 0)
|
||||
);
|
||||
}}
|
||||
</ProFormDependency>
|
||||
<ProFormList
|
||||
|
|
@ -653,7 +667,7 @@ const DetailForm: React.FC<{
|
|||
return (
|
||||
data?.map((item) => {
|
||||
return {
|
||||
label: `${item.name} - ${item.nameCn}`,
|
||||
label: `${item.name} - ${item.nameCn}`,
|
||||
value: item.sku,
|
||||
};
|
||||
}) || []
|
||||
|
|
|
|||
|
|
@ -1,4 +1,8 @@
|
|||
import React, { useRef, useState } from 'react';
|
||||
import { sitecontrollerAll } from '@/servers/api/site';
|
||||
import {
|
||||
subscriptioncontrollerList,
|
||||
subscriptioncontrollerSync,
|
||||
} from '@/servers/api/subscription';
|
||||
import {
|
||||
ActionType,
|
||||
DrawerForm,
|
||||
|
|
@ -7,14 +11,10 @@ import {
|
|||
ProFormSelect,
|
||||
ProTable,
|
||||
} from '@ant-design/pro-components';
|
||||
import { App, Button, Tag, Drawer, List } from 'antd';
|
||||
import { App, Button, Drawer, List, Tag } from 'antd';
|
||||
import dayjs from 'dayjs';
|
||||
import React, { useRef, useState } from 'react';
|
||||
import { request } from 'umi';
|
||||
import {
|
||||
subscriptioncontrollerList,
|
||||
subscriptioncontrollerSync,
|
||||
} from '@/servers/api/subscription';
|
||||
import { sitecontrollerAll } from '@/servers/api/site';
|
||||
|
||||
/**
|
||||
* 订阅状态枚举(用于筛选与展示)
|
||||
|
|
@ -77,7 +77,11 @@ const ListPage: React.FC = () => {
|
|||
valueEnum: SUBSCRIPTION_STATUS_ENUM,
|
||||
// 以 Tag 形式展示,更易辨识
|
||||
render: (_, row) =>
|
||||
row?.status ? <Tag>{SUBSCRIPTION_STATUS_ENUM[row.status]?.text || row.status}</Tag> : '-',
|
||||
row?.status ? (
|
||||
<Tag>{SUBSCRIPTION_STATUS_ENUM[row.status]?.text || row.status}</Tag>
|
||||
) : (
|
||||
'-'
|
||||
),
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
|
|
@ -152,9 +156,9 @@ const ListPage: React.FC = () => {
|
|||
const { success, data, message: errMsg } = resp as any;
|
||||
if (!success) throw new Error(errMsg || '获取失败');
|
||||
// 仅保留与父订单号完全一致的订单(避免模糊匹配误入)
|
||||
const candidates: any[] = (Array.isArray(data) ? data : []).filter(
|
||||
(c: any) => String(c?.externalOrderId) === parentNumber
|
||||
);
|
||||
const candidates: any[] = (
|
||||
Array.isArray(data) ? data : []
|
||||
).filter((c: any) => String(c?.externalOrderId) === parentNumber);
|
||||
// 拉取详情,补充状态、金额、时间
|
||||
const details = [] as any[];
|
||||
for (const c of candidates) {
|
||||
|
|
@ -230,14 +234,22 @@ const ListPage: React.FC = () => {
|
|||
<List.Item>
|
||||
<List.Item.Meta
|
||||
title={`#${item?.externalOrderId || '-'}`}
|
||||
description={`关系:${item?.relationship || '-'},站点:${item?.siteName || '-'}`}
|
||||
description={`关系:${item?.relationship || '-'},站点:${
|
||||
item?.siteName || '-'
|
||||
}`}
|
||||
/>
|
||||
<div style={{ display: 'flex', gap: 12, alignItems: 'center' }}>
|
||||
<span>{item?.date_created ? dayjs(item.date_created).format('YYYY-MM-DD HH:mm') : '-'}</span>
|
||||
<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 ?? '-'}
|
||||
{typeof item?.total === 'number'
|
||||
? item.total.toFixed(2)
|
||||
: item?.total ?? '-'}
|
||||
</span>
|
||||
</div>
|
||||
</List.Item>
|
||||
|
|
@ -274,7 +286,9 @@ const SyncForm: React.FC<{
|
|||
*/
|
||||
onFinish={async (values) => {
|
||||
try {
|
||||
const { success, message: errMsg } = await subscriptioncontrollerSync(values);
|
||||
const { success, message: errMsg } = await subscriptioncontrollerSync(
|
||||
values,
|
||||
);
|
||||
if (!success) {
|
||||
throw new Error(errMsg);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,31 +1,21 @@
|
|||
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 { CopyOutlined, DeleteFilled } from '@ant-design/icons';
|
||||
import { ActionType, ProDescriptions } from '@ant-design/pro-components';
|
||||
import { App, Button, Card, Divider, Drawer, Empty, Popconfirm } from 'antd';
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
|
||||
// 服务器 API 引用(保持与原 index.tsx 一致)
|
||||
import { logisticscontrollerDelshipment } from '@/servers/api/logistics';
|
||||
import {
|
||||
ordercontrollerChangestatus,
|
||||
ordercontrollerGetorderdetail,
|
||||
ordercontrollerSyncorderbyid,
|
||||
} from '@/servers/api/order';
|
||||
import { logisticscontrollerDelshipment } from '@/servers/api/logistics';
|
||||
import { sitecontrollerAll } from '@/servers/api/site';
|
||||
|
||||
// 工具与子组件
|
||||
import { ORDER_STATUS_ENUM } from '@/constants';
|
||||
import { formatShipmentState, formatSource } from '@/utils/format';
|
||||
import RelatedOrders from './RelatedOrders';
|
||||
import { ORDER_STATUS_ENUM } from '@/constants';
|
||||
|
||||
// 为保持原文件结构简单,此处从 index.tsx 引入的子组件仍由原文件导出或保持原状
|
||||
// 若后续需要彻底解耦,可将 OrderNote / Shipping / SalesChange 也独立到文件
|
||||
|
|
@ -35,13 +25,13 @@ type OrderRecord = API.Order;
|
|||
|
||||
interface OrderDetailDrawerProps {
|
||||
tableRef: React.MutableRefObject<ActionType | undefined>; // 列表刷新引用
|
||||
orderId: number; // 订单主键 ID
|
||||
record: OrderRecord; // 订单行记录
|
||||
open: boolean; // 是否打开抽屉
|
||||
onClose: () => void; // 关闭抽屉回调
|
||||
setActiveLine: (id: number) => void; // 高亮当前行
|
||||
OrderNoteComponent: React.ComponentType<any>; // 备注组件(从外部注入)
|
||||
SalesChangeComponent: React.ComponentType<any>; // 换货组件(从外部注入)
|
||||
orderId: number; // 订单主键 ID
|
||||
record: OrderRecord; // 订单行记录
|
||||
open: boolean; // 是否打开抽屉
|
||||
onClose: () => void; // 关闭抽屉回调
|
||||
setActiveLine: (id: number) => void; // 高亮当前行
|
||||
OrderNoteComponent: React.ComponentType<any>; // 备注组件(从外部注入)
|
||||
SalesChangeComponent: React.ComponentType<any>; // 换货组件(从外部注入)
|
||||
}
|
||||
|
||||
const OrderDetailDrawer: React.FC<OrderDetailDrawerProps> = ({
|
||||
|
|
@ -59,14 +49,18 @@ const OrderDetailDrawer: React.FC<OrderDetailDrawerProps> = ({
|
|||
|
||||
// 加载详情数据(与 index.tsx 中完全保持一致)
|
||||
const initRequest = async () => {
|
||||
const { data, success }: API.OrderDetailRes = await ordercontrollerGetorderdetail({ orderId });
|
||||
const { data, success }: API.OrderDetailRes =
|
||||
await ordercontrollerGetorderdetail({ orderId });
|
||||
if (!success || !data) return { data: {} } as any;
|
||||
data.sales = data.sales?.reduce((acc: API.OrderSale[], cur: API.OrderSale) => {
|
||||
const 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 = data.sales?.reduce(
|
||||
(acc: API.OrderSale[], cur: API.OrderSale) => {
|
||||
const idx = acc.findIndex((v: any) => v.productId === cur.productId);
|
||||
if (idx === -1) acc.push(cur);
|
||||
else acc[idx].quantity += cur.quantity;
|
||||
return acc;
|
||||
},
|
||||
[],
|
||||
);
|
||||
return { data } as any;
|
||||
};
|
||||
|
||||
|
|
@ -220,108 +214,273 @@ const OrderDetailDrawer: React.FC<OrderDetailDrawerProps> = ({
|
|||
: []),
|
||||
]}
|
||||
>
|
||||
<ProDescriptions labelStyle={{ width: '100px' }} actionRef={ref} request={initRequest}>
|
||||
<ProDescriptions.Item label="站点" dataIndex="siteId" valueType="select" request={async () => {
|
||||
const { data = [] } = await sitecontrollerAll();
|
||||
return data.map((item) => ({ label: item.siteName, 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
|
||||
labelStyle={{ width: '100px' }}
|
||||
actionRef={ref}
|
||||
request={initRequest}
|
||||
>
|
||||
<ProDescriptions.Item
|
||||
label="站点"
|
||||
dataIndex="siteId"
|
||||
valueType="select"
|
||||
request={async () => {
|
||||
const { data = [] } = await sitecontrollerAll();
|
||||
return data.map((item) => ({
|
||||
label: item.siteName,
|
||||
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="total" />
|
||||
<ProDescriptions.Item label="客户邮箱" dataIndex="customer_email" />
|
||||
<ProDescriptions.Item label="联系电话" span={3} render={(_, r: any) => (
|
||||
<div><span>{r?.shipping?.phone || r?.billing?.phone || '-'}</span></div>
|
||||
)} />
|
||||
<ProDescriptions.Item
|
||||
label="联系电话"
|
||||
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="IP" dataIndex="customer_id_address" />
|
||||
<ProDescriptions.Item label="设备" dataIndex="device_type" />
|
||||
<ProDescriptions.Item label="来源" render={(_, r: any) => formatSource(r.source_type, r.utm_source)} />
|
||||
<ProDescriptions.Item 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>company:<span>{r?.shipping?.company || r?.billing?.company || '-'}</span></div>
|
||||
<div>first_name:<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>
|
||||
{(r?.items || []).map((item: any) => (
|
||||
<li key={item.id}>{item.name}:{item.quantity}</li>
|
||||
))}
|
||||
</ul>
|
||||
)} />
|
||||
<ProDescriptions.Item label="关联" span={3} render={(_, r: any) => (
|
||||
<RelatedOrders data={r?.related} />
|
||||
)} />
|
||||
<ProDescriptions.Item label="订单内容" span={3} render={(_, r: any) => (
|
||||
<ul>
|
||||
{(r?.sales || []).map((item: any) => (
|
||||
<li key={item.id}>{item.name}:{item.quantity}</li>
|
||||
))}
|
||||
</ul>
|
||||
)} />
|
||||
<ProDescriptions.Item label="换货" span={3} render={(_, r: any) => (
|
||||
<SalesChangeComponent detailRef={ref} id={r.id as number} />
|
||||
)} />
|
||||
<ProDescriptions.Item label="备注" span={3} render={(_, r: any) => {
|
||||
if (!r.notes || r.notes.length === 0) return (<Empty description="暂无备注" />);
|
||||
return (
|
||||
<div style={{ width: '100%' }}>
|
||||
{r.notes.map((note: any) => (
|
||||
<div style={{ marginBottom: 10 }} key={note.id}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||
<span>{note.username}</span>
|
||||
<span>{note.createdAt}</span>
|
||||
<ProDescriptions.Item
|
||||
label="来源"
|
||||
render={(_, r: any) => formatSource(r.source_type, r.utm_source)}
|
||||
/>
|
||||
<ProDescriptions.Item
|
||||
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>
|
||||
company:
|
||||
<span>
|
||||
{r?.shipping?.company || r?.billing?.company || '-'}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
first_name:
|
||||
<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>
|
||||
{(r?.items || []).map((item: any) => (
|
||||
<li key={item.id}>
|
||||
{item.name}:{item.quantity}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
/>
|
||||
<ProDescriptions.Item
|
||||
label="关联"
|
||||
span={3}
|
||||
render={(_, r: any) => <RelatedOrders data={r?.related} />}
|
||||
/>
|
||||
<ProDescriptions.Item
|
||||
label="订单内容"
|
||||
span={3}
|
||||
render={(_, r: any) => (
|
||||
<ul>
|
||||
{(r?.sales || []).map((item: any) => (
|
||||
<li key={item.id}>
|
||||
{item.name}:{item.quantity}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
/>
|
||||
<ProDescriptions.Item
|
||||
label="换货"
|
||||
span={3}
|
||||
render={(_, r: any) => (
|
||||
<SalesChangeComponent detailRef={ref} id={r.id as number} />
|
||||
)}
|
||||
/>
|
||||
<ProDescriptions.Item
|
||||
label="备注"
|
||||
span={3}
|
||||
render={(_, r: any) => {
|
||||
if (!r.notes || r.notes.length === 0)
|
||||
return <Empty description="暂无备注" />;
|
||||
return (
|
||||
<div style={{ width: '100%' }}>
|
||||
{r.notes.map((note: any) => (
|
||||
<div style={{ marginBottom: 10 }} key={note.id}>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
}}
|
||||
>
|
||||
<span>{note.username}</span>
|
||||
<span>{note.createdAt}</span>
|
||||
</div>
|
||||
<div>{note.content}</div>
|
||||
</div>
|
||||
<div>{note.content}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}} />
|
||||
<ProDescriptions.Item label="物流信息" span={3} render={(_, r: any) => {
|
||||
if (!r.shipment || r.shipment.length === 0) return (<Empty description="暂无物流信息" />);
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', width: '100%' }}>
|
||||
{r.shipment.map((v: any) => (
|
||||
<Card key={v.id} style={{ marginBottom: '10px' }} extra={formatShipmentState(v.state)} title={<>
|
||||
{v.tracking_provider}
|
||||
{v.primary_tracking_number}
|
||||
<CopyOutlined onClick={async () => {
|
||||
try { await navigator.clipboard.writeText(v.tracking_url); message.success('复制成功!'); }
|
||||
catch { message.error('复制失败!'); }
|
||||
}} />
|
||||
</>}
|
||||
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 />取消运单
|
||||
</Popconfirm>
|
||||
] : [] }
|
||||
>
|
||||
<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>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<ProDescriptions.Item
|
||||
label="物流信息"
|
||||
span={3}
|
||||
render={(_, r: any) => {
|
||||
if (!r.shipment || r.shipment.length === 0)
|
||||
return <Empty description="暂无物流信息" />;
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
{r.shipment.map((v: any) => (
|
||||
<Card
|
||||
key={v.id}
|
||||
style={{ marginBottom: '10px' }}
|
||||
extra={formatShipmentState(v.state)}
|
||||
title={
|
||||
<>
|
||||
{v.tracking_provider}
|
||||
{v.primary_tracking_number}
|
||||
<CopyOutlined
|
||||
onClick={async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(
|
||||
v.tracking_url,
|
||||
);
|
||||
message.success('复制成功!');
|
||||
} catch {
|
||||
message.error('复制失败!');
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
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 />
|
||||
取消运单
|
||||
</Popconfirm>,
|
||||
]
|
||||
: []
|
||||
}
|
||||
>
|
||||
<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>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</ProDescriptions>
|
||||
</Drawer>
|
||||
);
|
||||
};
|
||||
|
||||
export default OrderDetailDrawer;
|
||||
export default OrderDetailDrawer;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import React from 'react';
|
||||
import { Empty, Tag } from 'antd';
|
||||
import dayjs from 'dayjs';
|
||||
import relativeTime from 'dayjs/plugin/relativeTime';
|
||||
import React from 'react';
|
||||
|
||||
dayjs.extend(relativeTime);
|
||||
|
||||
|
|
@ -12,17 +12,36 @@ dayjs.extend(relativeTime);
|
|||
*/
|
||||
const RelatedOrders: React.FC<{ data?: any[] }> = ({ data = [] }) => {
|
||||
const rows = (Array.isArray(data) ? data : []).map((it: any) => {
|
||||
const isSubscription = !!it?.externalSubscriptionId || !!it?.billing_period || !!it?.line_items;
|
||||
const number = isSubscription ? `#${it?.externalSubscriptionId || it?.id}` : `#${it?.externalOrderId || it?.id}`;
|
||||
const isSubscription =
|
||||
!!it?.externalSubscriptionId || !!it?.billing_period || !!it?.line_items;
|
||||
const number = isSubscription
|
||||
? `#${it?.externalSubscriptionId || it?.id}`
|
||||
: `#${it?.externalOrderId || it?.id}`;
|
||||
const relationship = isSubscription ? 'Subscription' : 'Order';
|
||||
const dateRaw = it?.start_date || it?.date_created || it?.createdAt || it?.updatedAt;
|
||||
const dateRaw =
|
||||
it?.start_date || it?.date_created || it?.createdAt || it?.updatedAt;
|
||||
const dateText = dateRaw ? dayjs(dateRaw).fromNow() : '-';
|
||||
const status = (isSubscription ? it?.status : it?.orderStatus) || '-';
|
||||
const statusLower = String(status).toLowerCase();
|
||||
const color = statusLower === 'active' ? 'green' : statusLower === 'cancelled' ? 'red' : 'default';
|
||||
const color =
|
||||
statusLower === 'active'
|
||||
? 'green'
|
||||
: statusLower === 'cancelled'
|
||||
? 'red'
|
||||
: 'default';
|
||||
const totalNum = Number(it?.total || 0);
|
||||
const totalText = isSubscription ? `$${totalNum.toFixed(2)} / ${it?.billing_period || 'period'}` : `$${totalNum.toFixed(2)}`;
|
||||
return { key: `${isSubscription ? 'sub' : 'order'}-${it?.id}`, number, relationship, dateText, status, color, totalText };
|
||||
const totalText = isSubscription
|
||||
? `$${totalNum.toFixed(2)} / ${it?.billing_period || 'period'}`
|
||||
: `$${totalNum.toFixed(2)}`;
|
||||
return {
|
||||
key: `${isSubscription ? 'sub' : 'order'}-${it?.id}`,
|
||||
number,
|
||||
relationship,
|
||||
dateText,
|
||||
status,
|
||||
color,
|
||||
totalText,
|
||||
};
|
||||
});
|
||||
|
||||
if (rows.length === 0) return <Empty description="暂无关联" />;
|
||||
|
|
@ -30,7 +49,14 @@ const RelatedOrders: React.FC<{ data?: any[] }> = ({ data = [] }) => {
|
|||
return (
|
||||
<div style={{ width: '100%' }}>
|
||||
{/* 表头(英文文案,符合国际化默认英文的要求) */}
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1.5fr 1fr 1fr 1fr 1fr', padding: '8px 0', fontWeight: 600 }}>
|
||||
<div
|
||||
style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '1.5fr 1fr 1fr 1fr 1fr',
|
||||
padding: '8px 0',
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
<div>订单编号</div>
|
||||
<div>关系</div>
|
||||
<div>日期</div>
|
||||
|
|
@ -39,11 +65,23 @@ const RelatedOrders: React.FC<{ data?: any[] }> = ({ data = [] }) => {
|
|||
</div>
|
||||
<div>
|
||||
{rows.map((r) => (
|
||||
<div key={r.key} style={{ display: 'grid', gridTemplateColumns: '1.5fr 1fr 1fr 1fr 1fr', padding: '6px 0', borderTop: '1px solid #f0f0f0' }}>
|
||||
<div><a>{r.number}</a></div>
|
||||
<div
|
||||
key={r.key}
|
||||
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 style={{ color: '#1677ff' }}>{r.dateText}</div>
|
||||
<div><Tag color={r.color}>{r.status}</Tag></div>
|
||||
<div>
|
||||
<Tag color={r.color}>{r.status}</Tag>
|
||||
</div>
|
||||
<div>{r.totalText}</div>
|
||||
</div>
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,16 @@
|
|||
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 { 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 OrderDetailDrawer from './OrderDetailDrawer';
|
||||
|
||||
interface OrderItemRow {
|
||||
id: number;
|
||||
|
|
@ -43,7 +47,10 @@ const OrdersPage: React.FC = () => {
|
|||
valueType: 'select',
|
||||
request: async () => {
|
||||
const { data = [] } = await sitecontrollerAll();
|
||||
return (data || []).map((item: any) => ({ label: item.siteName, value: item.id }));
|
||||
return (data || []).map((item: any) => ({
|
||||
label: item.siteName,
|
||||
value: item.id,
|
||||
}));
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
@ -51,7 +58,10 @@ const OrdersPage: React.FC = () => {
|
|||
dataIndex: 'date_created',
|
||||
width: 180,
|
||||
hideInSearch: true,
|
||||
render: (_, row) => (row?.date_created ? dayjs(row.date_created).format('YYYY-MM-DD HH:mm') : '-'),
|
||||
render: (_, row) =>
|
||||
row?.date_created
|
||||
? dayjs(row.date_created).format('YYYY-MM-DD HH:mm')
|
||||
: '-',
|
||||
},
|
||||
{
|
||||
title: '邮箱',
|
||||
|
|
@ -109,7 +119,14 @@ const OrdersPage: React.FC = () => {
|
|||
|
||||
const request: ProTableProps<OrderItemRow>['request'] = async (params) => {
|
||||
try {
|
||||
const { current = 1, pageSize = 10, siteId, keyword, customer_email, payment_method } = params as any;
|
||||
const {
|
||||
current = 1,
|
||||
pageSize = 10,
|
||||
siteId,
|
||||
keyword,
|
||||
customer_email,
|
||||
payment_method,
|
||||
} = params as any;
|
||||
const [startDate, endDate] = (params as any).dateRange || [];
|
||||
const resp = await ordercontrollerGetorders({
|
||||
current,
|
||||
|
|
@ -119,7 +136,9 @@ const OrdersPage: React.FC = () => {
|
|||
customer_email,
|
||||
payment_method,
|
||||
isSubscriptionOnly: true as any,
|
||||
startDate: startDate ? (dayjs(startDate).toISOString() as any) : undefined,
|
||||
startDate: startDate
|
||||
? (dayjs(startDate).toISOString() as any)
|
||||
: undefined,
|
||||
endDate: endDate ? (dayjs(endDate).toISOString() as any) : undefined,
|
||||
} as any);
|
||||
const { success, data, message: errMsg } = resp as any;
|
||||
|
|
@ -136,10 +155,10 @@ const OrdersPage: React.FC = () => {
|
|||
};
|
||||
|
||||
return (
|
||||
<PageContainer title='订阅订单'>
|
||||
<PageContainer title="订阅订单">
|
||||
<ProTable<OrderItemRow>
|
||||
actionRef={actionRef}
|
||||
rowKey='id'
|
||||
rowKey="id"
|
||||
columns={columns}
|
||||
request={request}
|
||||
pagination={{ showSizeChanger: true }}
|
||||
|
|
|
|||
|
|
@ -87,7 +87,9 @@ const List: React.FC = () => {
|
|||
rowKey="id"
|
||||
toolBarRender={() => [<CreateForm tableRef={actionRef} />]}
|
||||
request={async (params) => {
|
||||
const response = await templatecontrollerGettemplatelist(params as any) as any;
|
||||
const response = (await templatecontrollerGettemplatelist(
|
||||
params as any,
|
||||
)) as any;
|
||||
return {
|
||||
data: response.items || [],
|
||||
total: response.total || 0,
|
||||
|
|
@ -202,5 +204,4 @@ const UpdateForm: React.FC<{
|
|||
);
|
||||
};
|
||||
|
||||
|
||||
export default List;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import {
|
||||
logisticscontrollerGetlistbyorderid,
|
||||
logisticscontrollerGetorderlist,
|
||||
logisticscontrollerGetlistbyorderid
|
||||
} from '@/servers/api/logistics';
|
||||
import { SearchOutlined } from '@ant-design/icons';
|
||||
import { PageContainer, ProFormSelect } from '@ant-design/pro-components';
|
||||
|
|
@ -16,8 +16,9 @@ const TrackPage: React.FC = () => {
|
|||
debounceTime={500}
|
||||
request={async ({ keyWords }) => {
|
||||
if (!keyWords || keyWords.length < 3) return [];
|
||||
const { data: trackList } =
|
||||
await logisticscontrollerGetorderlist({ number: keyWords });
|
||||
const { data: trackList } = await logisticscontrollerGetorderlist({
|
||||
number: keyWords,
|
||||
});
|
||||
return trackList?.map((v) => {
|
||||
return {
|
||||
label: v.siteName + ' ' + v.externalOrderId,
|
||||
|
|
@ -29,8 +30,8 @@ const TrackPage: React.FC = () => {
|
|||
prefix: '订单号',
|
||||
async onChange(value: string) {
|
||||
setId(value);
|
||||
setData({})
|
||||
|
||||
setData({});
|
||||
|
||||
const { data } = await logisticscontrollerGetlistbyorderid({
|
||||
id,
|
||||
});
|
||||
|
|
@ -53,32 +54,34 @@ const TrackPage: React.FC = () => {
|
|||
),
|
||||
}}
|
||||
/>
|
||||
{
|
||||
data?.item ?
|
||||
{data?.item ? (
|
||||
<div>
|
||||
<div>
|
||||
<div>
|
||||
<h4>原订单内容</h4>
|
||||
{data?.item?.map((item) => (
|
||||
<div style={{ paddingLeft: 20, color: 'blue' }}>
|
||||
{item.name} * {item.quantity}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div> : <></>
|
||||
}
|
||||
{
|
||||
data?.saleItem ?
|
||||
<h4>原订单内容</h4>
|
||||
{data?.item?.map((item) => (
|
||||
<div style={{ paddingLeft: 20, color: 'blue' }}>
|
||||
{item.name} * {item.quantity}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
{data?.saleItem ? (
|
||||
<div>
|
||||
<div>
|
||||
<div>
|
||||
<h4>订单内容</h4>
|
||||
{data?.saleItem?.map((item) => (
|
||||
<div style={{ paddingLeft: 20, color: 'blue' }}>
|
||||
{item.name} * {item.quantity}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div> : <></>
|
||||
}
|
||||
<h4>订单内容</h4>
|
||||
{data?.saleItem?.map((item) => (
|
||||
<div style={{ paddingLeft: 20, color: 'blue' }}>
|
||||
{item.name} * {item.quantity}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</PageContainer>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
.selected-line-order-protable {
|
||||
background-color: #add8e6;
|
||||
}
|
||||
background-color: #add8e6;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,16 +1,14 @@
|
|||
import '@umijs/max/typings';
|
||||
|
||||
declare namespace BaseType {
|
||||
|
||||
type EnumTransformOptions {
|
||||
type EnumTransformOptions = {
|
||||
value: string; // 用于作为 value 的字段名
|
||||
label: string; // 用于作为 text 的字段名
|
||||
status?: string | undefined; // 可选:用于设置状态的字段名
|
||||
color?: string | undefined; // 可选:用于设置颜色的字段名
|
||||
}
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
declare global {
|
||||
const UMI_APP_API_URL: string;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue