refactor: 优化代码格式和结构,修复类型定义和样式问题

feat(access): 添加模板权限控制
feat(product): 新增品牌管理页面
fix(typings): 修正EnumTransformOptions类型定义
style: 统一代码缩进和格式
refactor(login): 调整设备指纹hook导入顺序
refactor(order): 优化订单商品聚合页面代码结构
refactor(subscription): 重构订阅订单相关组件
refactor(track): 调整物流跟踪页面代码结构
refactor(stock): 优化库存调拨表单逻辑
refactor(customer): 重构客户列表评分组件
refactor(product): 优化产品列表中文名编辑组件
refactor(logistics): 调整物流服务列表代码结构
refactor(site): 优化站点列表表单和操作逻辑
refactor(dict): 重构字典管理页面代码结构
This commit is contained in:
tikkhun 2025-11-28 00:08:31 +08:00
parent 7dfbc30e94
commit bd4096258e
29 changed files with 1501 additions and 990 deletions

View File

@ -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,
@ -23,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: [
@ -93,9 +93,9 @@ export default defineConfig({
component: './Product/List', component: './Product/List',
}, },
{ {
name: '商品分类', name: '品牌',
path: '/product/category', path: '/product/brand',
component: './Product/Category', component: './Product/Brand',
}, },
{ {
name: '强度', name: '强度',

View File

@ -1,2 +1 @@
# WEB # WEB

View File

@ -11,7 +11,7 @@ export default (initialState: any) => {
const canSeeStatistics = (isSuper || isAdmin) || (initialState?.user?.permissions?.includes('statistics') ?? false); const canSeeStatistics = (isSuper || isAdmin) || (initialState?.user?.permissions?.includes('statistics') ?? false);
const canSeeSite = (isSuper || isAdmin) || (initialState?.user?.permissions?.includes('site') ?? false); const canSeeSite = (isSuper || isAdmin) || (initialState?.user?.permissions?.includes('site') ?? false);
const canSeeDict = (isSuper || isAdmin) || (initialState?.user?.permissions?.includes('dict') ?? false); const canSeeDict = (isSuper || isAdmin) || (initialState?.user?.permissions?.includes('dict') ?? false);
const canSeeTemplate = (isSuper || isAdmin) || (initialState?.user?.permissions?.includes('template') ?? false);
return { return {
canSeeOrganiza, canSeeOrganiza,
canSeeProduct, canSeeProduct,
@ -22,5 +22,6 @@ export default (initialState: any) => {
canSeeStatistics, canSeeStatistics,
canSeeSite, canSeeSite,
canSeeDict, canSeeDict,
canSeeTemplate,
}; };
}; };

View File

@ -1,4 +1,3 @@
import { sitecontrollerAll } from '@/servers/api/site'; import { sitecontrollerAll } from '@/servers/api/site';
import { SyncOutlined } from '@ant-design/icons'; import { SyncOutlined } from '@ant-design/icons';
import { import {
@ -7,13 +6,13 @@ import {
ProForm, ProForm,
ProFormSelect, ProFormSelect,
} from '@ant-design/pro-components'; } from '@ant-design/pro-components';
import { App, Button } from 'antd'; import { Button } from 'antd';
import React from 'react'; import React from 'react';
// 定义SyncForm组件的props类型 // 定义SyncForm组件的props类型
interface SyncFormProps { interface SyncFormProps {
tableRef: React.MutableRefObject<ActionType | undefined>; tableRef: React.MutableRefObject<ActionType | undefined>;
onFinish: (values:any) => Promise<void>; onFinish: (values: any) => Promise<void>;
} }
/** /**

View File

@ -39,9 +39,9 @@ const ListPage: React.FC = () => {
{ {
title: '客户编号', title: '客户编号',
dataIndex: 'customerId', dataIndex: 'customerId',
render: (_, record) => { render: (_, record) => {
if(!record.customerId) return '-'; if (!record.customerId) return '-';
return String(record.customerId).padStart(6,0) return String(record.customerId).padStart(6, 0);
}, },
sorter: true, sorter: true,
}, },
@ -95,31 +95,37 @@ const ListPage: React.FC = () => {
title: '等级', title: '等级',
hideInSearch: true, hideInSearch: true,
render: (_, record) => { render: (_, record) => {
if(!record.yoone_orders || !record.yoone_total) return '-' if (!record.yoone_orders || !record.yoone_total) return '-';
if(Number(record.yoone_orders) === 1 && Number(record.yoone_total) > 0 ) return 'B' if (Number(record.yoone_orders) === 1 && Number(record.yoone_total) > 0)
return '-' return 'B';
} return '-';
},
}, },
{ {
title: '评星', title: '评星',
dataIndex: 'rate', dataIndex: 'rate',
width: 200, width: 200,
render: (_, record) => { render: (_, record) => {
return <Rate onChange={async(val)=>{ return (
try{ <Rate
const { success, message: msg } = await customercontrollerSetrate({ onChange={async (val) => {
id: record.customerId, try {
rate: val const { success, message: msg } =
}); await customercontrollerSetrate({
if (success) { id: record.customerId,
message.success(msg); rate: val,
actionRef.current?.reload(); });
} if (success) {
}catch(e){ message.success(msg);
message.error(e.message); actionRef.current?.reload();
}
} } catch (e) {
}} value={record.rate} /> message.error(e.message);
}
}}
value={record.rate}
/>
);
}, },
}, },
{ {

View File

@ -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 { 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; const { Sider, Content } = Layout;
@ -26,13 +36,17 @@ const DictPage: React.FC = () => {
// 控制字典项模态框的显示 // 控制字典项模态框的显示
const [isDictItemModalVisible, setIsDictItemModalVisible] = useState(false); const [isDictItemModalVisible, setIsDictItemModalVisible] = useState(false);
const [editingDictItem, setEditingDictItem] = useState<any>(null); 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) => { const fetchDicts = async (title?: string) => {
setLoadingDicts(true); setLoadingDicts(true);
try { try {
const res = await request('/dict/list', { params: { title } }); const res = await request('/dict/list', { params: { title } });
if (res) { if (res) {
setDicts(res); setDicts(res);
} }
@ -46,7 +60,7 @@ const DictPage: React.FC = () => {
const fetchDictItems = async (dictId?: number) => { const fetchDictItems = async (dictId?: number) => {
setLoadingDictItems(true); setLoadingDictItems(true);
try { try {
const res = await request('/dict/items', { params: { dictId } }); const res = await request('/dict/items', { params: { dictId } });
if (res) { if (res) {
setDictItems(res); setDictItems(res);
} }
@ -80,13 +94,13 @@ const DictPage: React.FC = () => {
} }
try { try {
if (editingDict) { if (editingDict) {
await request(`/dict/${editingDict.id}`, { await request(`/dict/${editingDict.id}`, {
method: 'PUT', method: 'PUT',
data: { name: newDictName, title: newDictTitle }, data: { name: newDictName, title: newDictTitle },
}); });
message.success('更新成功'); message.success('更新成功');
} else { } else {
await request('/dict', { await request('/dict', {
method: 'POST', method: 'POST',
data: { name: newDictName, title: newDictTitle }, data: { name: newDictName, title: newDictTitle },
}); });
@ -132,14 +146,14 @@ const DictPage: React.FC = () => {
try { try {
if (editingDictItem) { if (editingDictItem) {
// 编辑 // 编辑
await request(`/dict/item/${editingDictItem.id}`, { await request(`/dict/item/${editingDictItem.id}`, {
method: 'PUT', method: 'PUT',
data: dictItemForm, data: dictItemForm,
}); });
message.success('更新成功'); message.success('更新成功');
} else { } else {
// 添加 // 添加
await request('/dict/item', { await request('/dict/item', {
method: 'POST', method: 'POST',
data: { ...dictItemForm, dictId: selectedDict.id }, data: { ...dictItemForm, dictId: selectedDict.id },
}); });
@ -155,7 +169,7 @@ const DictPage: React.FC = () => {
// 处理删除字典项 // 处理删除字典项
const handleDeleteDictItem = async (itemId: number) => { const handleDeleteDictItem = async (itemId: number) => {
try { try {
await request(`/dict/item/${itemId}`, { method: 'DELETE' }); await request(`/dict/item/${itemId}`, { method: 'DELETE' });
message.success('删除成功'); message.success('删除成功');
fetchDictItems(selectedDict.id); fetchDictItems(selectedDict.id);
} catch (error) { } catch (error) {
@ -166,7 +180,7 @@ const DictPage: React.FC = () => {
// 处理删除字典 // 处理删除字典
const handleDeleteDict = async (dictId: number) => { const handleDeleteDict = async (dictId: number) => {
try { try {
await request(`/dict/${dictId}`, { method: 'DELETE' }); await request(`/dict/${dictId}`, { method: 'DELETE' });
message.success('删除成功'); message.success('删除成功');
fetchDicts(); // 重新获取字典列表 fetchDicts(); // 重新获取字典列表
// 如果删除的是当前选中的字典,则清空右侧列表 // 如果删除的是当前选中的字典,则清空右侧列表
@ -181,13 +195,12 @@ const DictPage: React.FC = () => {
// 左侧字典列表的列定义 // 左侧字典列表的列定义
const dictColumns = [ const dictColumns = [
{ {
title: '名称', title: '名称',
dataIndex: 'name', dataIndex: 'name',
key: 'name', key: 'name',
}, },
{ {
title: '标题', title: '标题',
dataIndex: 'title', dataIndex: 'title',
key: 'title', key: 'title',
@ -197,10 +210,23 @@ const DictPage: React.FC = () => {
key: 'action', key: 'action',
render: (_: any, record: any) => ( render: (_: any, record: any) => (
<Space> <Space>
<Button type="link" onClick={(e) => { e.stopPropagation(); handleEditDict(record); }}> <Button
type="link"
onClick={(e) => {
e.stopPropagation();
handleEditDict(record);
}}
>
</Button> </Button>
<Button type="link" danger onClick={(e) => { e.stopPropagation(); handleDeleteDict(record.id); }}> <Button
type="link"
danger
onClick={(e) => {
e.stopPropagation();
handleDeleteDict(record.id);
}}
>
</Button> </Button>
</Space> </Space>
@ -225,8 +251,16 @@ const DictPage: React.FC = () => {
key: 'action', key: 'action',
render: (_: any, record: any) => ( render: (_: any, record: any) => (
<Space size="middle"> <Space size="middle">
<Button type="link" onClick={() => handleEditDictItem(record)}></Button> <Button type="link" onClick={() => handleEditDictItem(record)}>
<Button type="link" danger onClick={() => handleDeleteDictItem(record.id)}></Button>
</Button>
<Button
type="link"
danger
onClick={() => handleDeleteDictItem(record.id)}
>
</Button>
</Space> </Space>
), ),
}, },
@ -235,18 +269,35 @@ const DictPage: React.FC = () => {
return ( return (
<PageContainer> <PageContainer>
<Layout style={{ background: '#fff' }}> <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%' }}> <Space direction="vertical" style={{ width: '100%' }}>
<Input.Search placeholder="搜索字典" onSearch={handleSearch} onChange={e => setSearchText(e.target.value)} enterButton allowClear /> <Input.Search
<Button type="primary" onClick={() => setIsAddDictModalVisible(true)} block> placeholder="搜索字典"
onSearch={handleSearch}
onChange={(e) => setSearchText(e.target.value)}
enterButton
allowClear
/>
<Button
type="primary"
onClick={() => setIsAddDictModalVisible(true)}
block
>
</Button> </Button>
<Space> <Space>
<Upload <Upload
name="file" name="file"
action="/dict/import" action="/dict/import"
showUploadList={false} showUploadList={false}
onChange={info => { onChange={(info) => {
if (info.file.status === 'done') { if (info.file.status === 'done') {
message.success(`${info.file.name} 文件上传成功`); message.success(`${info.file.name} 文件上传成功`);
fetchDicts(); fetchDicts();
@ -257,14 +308,16 @@ const DictPage: React.FC = () => {
> >
<Button icon={<UploadOutlined />}></Button> <Button icon={<UploadOutlined />}></Button>
</Upload> </Upload>
<Button onClick={() => window.open('/dict/template')}></Button> <Button onClick={() => window.open('/dict/template')}>
</Button>
</Space> </Space>
<Table <Table
dataSource={dicts} dataSource={dicts}
columns={dictColumns} columns={dictColumns}
rowKey="id" rowKey="id"
loading={loadingDicts} loading={loadingDicts}
onRow={record => ({ onRow={(record) => ({
onClick: () => { onClick: () => {
// 如果点击的是当前已选中的行,则取消选择 // 如果点击的是当前已选中的行,则取消选择
if (selectedDict?.id === record.id) { 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} pagination={false}
/> />
</Space> </Space>
</Sider> </Sider>
<Content style={{ padding: '16px' }}> <Content style={{ padding: '16px' }}>
<Space direction="vertical" style={{ width: '100%' }}> <Space direction="vertical" style={{ width: '100%' }}>
<Button type="primary" onClick={handleAddDictItem} disabled={!selectedDict}> <Button
type="primary"
onClick={handleAddDictItem}
disabled={!selectedDict}
>
</Button> </Button>
<Space> <Space>
<Upload <Upload
name="file" name="file"
action={`/dict/item/import`} action={`/dict/item/import`}
data={{ dictId: selectedDict?.id }} data={{ dictId: selectedDict?.id }}
showUploadList={false} showUploadList={false}
disabled={!selectedDict} disabled={!selectedDict}
onChange={info => { onChange={(info) => {
if (info.file.status === 'done') { if (info.file.status === 'done') {
message.success(`${info.file.name} 文件上传成功`); message.success(`${info.file.name} 文件上传成功`);
fetchDictItems(selectedDict.id); fetchDictItems(selectedDict.id);
@ -300,11 +359,23 @@ const DictPage: React.FC = () => {
} }
}} }}
> >
<Button icon={<UploadOutlined />} disabled={!selectedDict}></Button> <Button icon={<UploadOutlined />} disabled={!selectedDict}>
</Button>
</Upload> </Upload>
<Button onClick={() => window.open('/dict/item/template')} disabled={!selectedDict}></Button> <Button
onClick={() => window.open('/dict/item/template')}
disabled={!selectedDict}
>
</Button>
</Space> </Space>
<Table dataSource={dictItems} columns={dictItemColumns} rowKey="id" loading={loadingDictItems} /> <Table
dataSource={dictItems}
columns={dictItemColumns}
rowKey="id"
loading={loadingDictItems}
/>
</Space> </Space>
</Content> </Content>
</Layout> </Layout>
@ -317,13 +388,31 @@ const DictPage: React.FC = () => {
> >
<Form layout="vertical"> <Form layout="vertical">
<Form.Item label="名称"> <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>
<Form.Item label="标题"> <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>
<Form.Item label="值 (可选)"> <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.Item>
</Form> </Form>
</Modal> </Modal>
@ -336,10 +425,18 @@ const DictPage: React.FC = () => {
> >
<Form layout="vertical"> <Form layout="vertical">
<Form.Item label="字典名称"> <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>
<Form.Item label="字典标题"> <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.Item>
</Form> </Form>
</Modal> </Modal>

View File

@ -1,3 +1,4 @@
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 {
@ -7,7 +8,6 @@ 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,28 +15,32 @@ 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 { data, success, code, message: msg } = await usercontrollerLogin({...values, deviceId}); const {
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('登录失败');
} }
@ -100,16 +104,17 @@ const Page = () => {
}, },
]} ]}
/> />
{ {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,

View File

@ -1,12 +1,14 @@
import { logisticscontrollerGetlist, logisticscontrollerGetshipmentlabel, import {
logisticscontrollerDeleteshipment, logisticscontrollerDeleteshipment,
logisticscontrollerUpdateshipmentstate logisticscontrollerGetlist,
} from '@/servers/api/logistics'; logisticscontrollerGetshipmentlabel,
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,
@ -15,7 +17,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 { sitecontrollerAll } from '@/servers/api/site'; import { ToastContainer } from 'react-toastify';
const ListPage: React.FC = () => { const ListPage: React.FC = () => {
const actionRef = useRef<ActionType>(); const actionRef = useRef<ActionType>();
@ -69,7 +71,9 @@ const ListPage: React.FC = () => {
<CopyOutlined <CopyOutlined
onClick={async () => { onClick={async () => {
try { try {
await navigator.clipboard.writeText(record.return_tracking_number); await navigator.clipboard.writeText(
record.return_tracking_number,
);
message.success('复制成功!'); message.success('复制成功!');
} catch (err) { } catch (err) {
message.error('复制失败!'); message.error('复制失败!');
@ -106,7 +110,9 @@ const ListPage: React.FC = () => {
disabled={isLoading} disabled={isLoading}
onClick={async () => { onClick={async () => {
setIsLoading(true); setIsLoading(true);
const { data } = await logisticscontrollerGetshipmentlabel({shipmentId:record.id}); const { data } = await logisticscontrollerGetshipmentlabel({
shipmentId: record.id,
});
const content = data.content; const content = data.content;
printPDF([content]); printPDF([content]);
setIsLoading(false); setIsLoading(false);
@ -120,7 +126,9 @@ const ListPage: React.FC = () => {
disabled={isLoading} disabled={isLoading}
onClick={async () => { onClick={async () => {
setIsLoading(true); setIsLoading(true);
const res = await logisticscontrollerUpdateshipmentstate({shipmentId:record.id}); const res = await logisticscontrollerUpdateshipmentstate({
shipmentId: record.id,
});
console.log('res', res); console.log('res', res);
setIsLoading(false); setIsLoading(false);
@ -137,7 +145,7 @@ const ListPage: React.FC = () => {
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);
} }

View File

@ -1,6 +1,5 @@
import { import {
logisticscontrollerGetservicelist, logisticscontrollerGetservicelist,
logisticscontrollerSyncservices,
logisticscontrollerToggleactive, logisticscontrollerToggleactive,
} from '@/servers/api/logistics'; } from '@/servers/api/logistics';
import { import {
@ -10,7 +9,7 @@ import {
ProFormSwitch, ProFormSwitch,
ProTable, ProTable,
} from '@ant-design/pro-components'; } from '@ant-design/pro-components';
import { App, Button } from 'antd'; import { App } from 'antd';
import { useRef } from 'react'; import { useRef } from 'react';
const ListPage: React.FC = () => { const ListPage: React.FC = () => {

View File

@ -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 { ordercontrollerGetordersales } from '@/servers/api/order';
import { sitecontrollerAll } from '@/servers/api/site'; 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 { interface OrderItemAggRow {
@ -87,7 +91,10 @@ 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) => ({ 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 获取订单项聚合 // 表格请求方法:调用 /order/getOrderSales 接口并设置 isSource=true 获取订单项聚合
const request: ProTableProps<OrderItemAggRow>['request'] = async (params:any) => { const request: ProTableProps<OrderItemAggRow>['request'] = async (
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 || [];
@ -115,7 +124,9 @@ const OrderItemsPage: React.FC = () => {
siteId, siteId,
name, name,
isSource: true as any, 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, 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;
@ -132,10 +143,12 @@ const OrderItemsPage: React.FC = () => {
}; };
return ( return (
<PageContainer title='订单商品概览'> <PageContainer title="订单商品概览">
<ProTable<OrderItemAggRow> <ProTable<OrderItemAggRow>
actionRef={actionRef} actionRef={actionRef}
rowKey={(r) => `${r.externalProductId}-${r.externalVariationId}-${r.name}`} rowKey={(r) =>
`${r.externalProductId}-${r.externalVariationId}-${r.name}`
}
columns={columns} columns={columns}
request={request} request={request}
pagination={{ showSizeChanger: true }} 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

View File

@ -86,15 +86,13 @@ const List: React.FC = () => {
return ( return (
<PageContainer header={{ title: '品牌列表' }}> <PageContainer header={{ title: '品牌列表' }}>
<ProTable<API.Brand> <ProTable<any>
headerTitle="查询表格" headerTitle="查询表格"
actionRef={actionRef} actionRef={actionRef}
rowKey="id" rowKey="id"
toolBarRender={() => [<CreateForm tableRef={actionRef} />]} toolBarRender={() => [<CreateForm tableRef={actionRef} />]}
request={async (params) => { request={async (params) => {
const { data, success } = await productcontrollerGetbrands( const { data, success } = await productcontrollerGetbrands(params);
params,
);
return { return {
total: data?.total || 0, total: data?.total || 0,
data: data?.items || [], data: data?.items || [],

View File

@ -26,24 +26,33 @@ const NameCn: React.FC<{
id: number; id: number;
value: string | undefined; value: string | undefined;
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) return <div onClick={() => setEditable(true)}>{value||'-'}</div>; if (!editable)
return <ProFormText initialValue={value} fieldProps={{autoFocus:true, onBlur:async(e: React.FocusEvent<HTMLInputElement>) => { return <div onClick={() => setEditable(true)}>{value || '-'}</div>;
if(!e.target.value) return setEditable(false) return (
const { success, message: errMsg } = <ProFormText
await productcontrollerUpdateproductnamecn({ initialValue={value}
id, fieldProps={{
nameCn: e.target.value, autoFocus: true,
}) onBlur: async (e: React.FocusEvent<HTMLInputElement>) => {
setEditable(false) if (!e.target.value) return setEditable(false);
if (!success) { const { success, message: errMsg } =
return message.error(errMsg) await productcontrollerUpdateproductnamecn({
} id,
tableRef?.current?.reload() nameCn: e.target.value,
}}} /> });
} setEditable(false);
if (!success) {
return message.error(errMsg);
}
tableRef?.current?.reload();
},
}}
/>
);
};
const List: React.FC = () => { const List: React.FC = () => {
const actionRef = useRef<ActionType>(); const actionRef = useRef<ActionType>();
// 状态:存储当前选中的行 // 状态:存储当前选中的行
@ -58,10 +67,10 @@ const List: React.FC = () => {
{ {
title: '中文名', title: '中文名',
dataIndex: 'nameCn', dataIndex: 'nameCn',
render: (_, record) => { render: (_, record) => {
return ( return (
<NameCn value={record.nameCn} id={record.id} tableRef={actionRef} /> <NameCn value={record.nameCn} id={record.id} tableRef={actionRef} />
) );
}, },
}, },
{ {
@ -149,7 +158,7 @@ const List: React.FC = () => {
success, success,
}; };
}} }}
columns={columns} columns={columns}
editable={{ editable={{
type: 'single', type: 'single',
onSave: async (key, record, originRow) => { onSave: async (key, record, originRow) => {
@ -215,7 +224,7 @@ const CreateForm: React.FC<{
width="lg" width="lg"
label="产品品牌" label="产品品牌"
placeholder="请选择产品品牌" placeholder="请选择产品品牌"
request={async () => { request={async () => {
const { data = [] } = await productcontrollerGetbrandall(); const { data = [] } = await productcontrollerGetbrandall();
return data.map((item: API.Brand) => ({ return data.map((item: API.Brand) => ({
label: item.name, label: item.name,

View File

@ -262,9 +262,9 @@ const UpdateStatus: React.FC<{
{ {
id: initialValues.id, id: initialValues.id,
}, },
{ {
status, status,
stock_status stock_status,
}, },
); );
if (!success) { if (!success) {
@ -285,7 +285,7 @@ const UpdateStatus: React.FC<{
name="status" name="status"
valueEnum={PRODUCT_STATUS_ENUM} valueEnum={PRODUCT_STATUS_ENUM}
/> />
<ProFormSelect <ProFormSelect
label="上下架状态" label="上下架状态"
width="lg" width="lg"
name="stock_status" name="stock_status"
@ -296,7 +296,6 @@ const UpdateStatus: React.FC<{
); );
}; };
const UpdateForm: React.FC<{ const UpdateForm: React.FC<{
tableRef: React.MutableRefObject<ActionType | undefined>; tableRef: React.MutableRefObject<ActionType | undefined>;
values: API.WpProductDTO; values: API.WpProductDTO;

View File

@ -1,12 +1,11 @@
import { UploadOutlined } from '@ant-design/icons';
import React, { useState } from 'react';
import { import {
PageContainer, PageContainer,
ProForm, ProForm,
ProFormSelect, ProFormSelect,
} from '@ant-design/pro-components'; } from '@ant-design/pro-components';
import { Button, Card, Col, Input, message, Row, Upload } from 'antd'; 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'; import * as XLSX from 'xlsx';
// 定义配置接口 // 定义配置接口
@ -22,7 +21,10 @@ interface TagConfig {
/** /**
* @description * @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 nm = name.trim();
const dryMatch = nm.match(/\(([^)]*)\)/); const dryMatch = nm.match(/\(([^)]*)\)/);
const dryness = dryMatch ? dryMatch[1].trim() : ''; const dryness = dryMatch ? dryMatch[1].trim() : '';
@ -75,11 +77,18 @@ const splitFlavorTokens = (flavorPart: string): string[] => {
/** /**
* @description Fruit, Mint * @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 tokens = splitFlavorTokens(flavorPart);
const fLower = flavorPart.toLowerCase(); const fLower = flavorPart.toLowerCase();
const isFruit = fruitKeys.some(key => fLower.includes(key)) || tokens.some(t => fruitKeys.includes(t)); const isFruit =
const isMint = mintKeys.some(key => fLower.includes(key)) || tokens.includes('mint'); 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[] = []; const extras: string[] = [];
if (isFruit) extras.push('Fruit'); 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 [brand, flavorPart, mg, dryness] = parseName(name, config.brands);
const tokens = splitFlavorTokens(flavorPart); const tokens = splitFlavorTokens(flavorPart);
const tokensForFlavor = tokens.filter(t => !config.nonFlavorTokens.includes(t)); const tokensForFlavor = tokens.filter(
const flavorTag = tokensForFlavor.map(t => t.charAt(0).toUpperCase() + t.slice(1)).join(''); (t) => !config.nonFlavorTokens.includes(t),
);
const flavorTag = tokensForFlavor
.map((t) => t.charAt(0).toUpperCase() + t.slice(1))
.join('');
let tags: string[] = []; let tags: string[] = [];
if (brand) tags.push(brand); 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 seen = new Set<string>();
const finalTags = tags.filter(t => { const finalTags = tags.filter((t) => {
if (t && !seen.has(t)) { if (t && !seen.has(t)) {
seen.add(t); seen.add(t);
return true; return true;
@ -152,9 +167,22 @@ const computeTags = (name: string, sku: string, config: TagConfig): string => {
const DEFAULT_CONFIG = { const DEFAULT_CONFIG = {
brands: ['YOONE', 'ZYN', 'ZEX', 'JUX', 'WHITE FOX'], brands: ['YOONE', 'ZYN', 'ZEX', 'JUX', 'WHITE FOX'],
fruitKeys: [ fruitKeys: [
'apple', 'blueberry', 'citrus', 'mango', 'peach', 'grape', 'cherry', 'apple',
'strawberry', 'watermelon', 'orange', 'lemon', 'lemonade', 'blueberry',
'razz', 'pineapple', 'berry', 'fruit', 'citrus',
'mango',
'peach',
'grape',
'cherry',
'strawberry',
'watermelon',
'orange',
'lemon',
'lemonade',
'razz',
'pineapple',
'berry',
'fruit',
], ],
mintKeys: ['mint', 'wintergreen', 'peppermint', 'spearmint', 'menthol'], mintKeys: ['mint', 'wintergreen', 'peppermint', 'spearmint', 'menthol'],
nonFlavorTokens: ['slim', 'pouches', 'pouch', 'mini', 'dry'], nonFlavorTokens: ['slim', 'pouches', 'pouch', 'mini', 'dry'],
@ -242,7 +270,7 @@ const WpToolPage: React.FC = () => {
try { try {
// 获取表单中的最新配置 // 获取表单中的最新配置
const config = await form.validateFields(); const config = await form.validateFields();
const { brands, fruitKeys, mintKeys, nonFlavorTokens } = config; const { brands, fruitKeys, mintKeys, nonFlavorTokens } = config;
// 确保品牌按长度降序排序 // 确保品牌按长度降序排序
@ -266,10 +294,15 @@ const WpToolPage: React.FC = () => {
}); });
setProcessedData(dataWithTags); setProcessedData(dataWithTags);
message.success({ content: 'Tags 生成成功!现在可以下载了。', key: 'processing' }); message.success({
content: 'Tags 生成成功!现在可以下载了。',
key: 'processing',
});
} catch (error) { } catch (error) {
message.error({ content: '处理失败,请检查配置或文件。', key: 'processing' }); message.error({
content: '处理失败,请检查配置或文件。',
key: 'processing',
});
console.error('Processing Error:', error); console.error('Processing Error:', error);
} finally { } finally {
setIsProcessing(false); setIsProcessing(false);
@ -368,7 +401,9 @@ const WpToolPage: React.FC = () => {
<Button icon={<UploadOutlined />}> CSV </Button> <Button icon={<UploadOutlined />}> CSV </Button>
</Upload> </Upload>
<div style={{ marginTop: 16 }}> <div style={{ marginTop: 16 }}>
<label style={{ display: 'block', marginBottom: 8 }}></label> <label style={{ display: 'block', marginBottom: 8 }}>
</label>
<Input value={file ? file.name : '暂未选择文件'} readOnly /> <Input value={file ? file.name : '暂未选择文件'} readOnly />
</div> </div>
<Button <Button

View File

@ -1,8 +1,16 @@
import React, { useEffect, useRef, useState } from 'react'; import {
import { ActionType, ProColumns, ProTable, ProFormInstance } from '@ant-design/pro-components'; ActionType,
import { DrawerForm, ProFormText, ProFormSelect, ProFormSwitch } from '@ant-design/pro-components'; DrawerForm,
import { Button, message, Popconfirm, Space, Tag } from 'antd'; ProColumns,
ProFormInstance,
ProFormSelect,
ProFormSwitch,
ProFormText,
ProTable,
} from '@ant-design/pro-components';
import { request } from '@umijs/max'; import { request } from '@umijs/max';
import { Button, message, Popconfirm, Space, Tag } from 'antd';
import React, { useEffect, useRef, useState } from 'react';
// 站点数据项类型(前端不包含密钥字段,后端列表不返回密钥) // 站点数据项类型(前端不包含密钥字段,后端列表不返回密钥)
interface SiteItem { interface SiteItem {
@ -58,10 +66,21 @@ const SiteList: React.FC = () => {
// 表格列定义 // 表格列定义
const columns: ProColumns<SiteItem>[] = [ 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: '站点名称', dataIndex: 'siteName', width: 220 },
{ 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: 'SKU 前缀',
dataIndex: 'skuPrefix',
width: 160,
hideInSearch: true,
},
{ {
title: '平台', title: '平台',
dataIndex: 'type', dataIndex: 'type',
@ -101,7 +120,9 @@ 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 request(`/site/disable/${row.id}`, { await request(`/site/disable/${row.id}`, {
@ -159,7 +180,9 @@ const SiteList: React.FC = () => {
...(values.siteName ? { siteName: values.siteName } : {}), ...(values.siteName ? { siteName: values.siteName } : {}),
...(values.apiUrl ? { apiUrl: values.apiUrl } : {}), ...(values.apiUrl ? { apiUrl: values.apiUrl } : {}),
...(values.type ? { type: values.type } : {}), ...(values.type ? { type: values.type } : {}),
...(typeof values.isDisabled === 'boolean' ? { isDisabled: values.isDisabled } : {}), ...(typeof values.isDisabled === 'boolean'
? { isDisabled: values.isDisabled }
: {}),
...(values.skuPrefix ? { skuPrefix: values.skuPrefix } : {}), ...(values.skuPrefix ? { skuPrefix: values.skuPrefix } : {}),
}; };
// 仅当输入了新密钥时才提交,未输入则保持原本值 // 仅当输入了新密钥时才提交,未输入则保持原本值
@ -169,7 +192,10 @@ const SiteList: React.FC = () => {
if (values.consumerSecret && values.consumerSecret.trim()) { if (values.consumerSecret && values.consumerSecret.trim()) {
payload.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 { } else {
// 新增站点时要求填写 consumerKey 和 consumerSecret // 新增站点时要求填写 consumerKey 和 consumerSecret
if (!values.consumerKey || !values.consumerSecret) { if (!values.consumerKey || !values.consumerSecret) {
@ -218,7 +244,7 @@ const SiteList: React.FC = () => {
</Button>, </Button>,
// 同步包括 orders subscriptions 等等 // 同步包括 orders subscriptions 等等
// <Button key='new' type='primary' onClick={()=> { // <Button key='new' type='primary' onClick={()=> {
// //
// }}}> // }}}>
// 同步站点数据 // 同步站点数据
// </Button> // </Button>
@ -233,9 +259,18 @@ const SiteList: React.FC = () => {
onFinish={handleSubmit} onFinish={handleSubmit}
> >
{/* 站点名称,必填 */} {/* 站点名称,必填 */}
<ProFormText name="siteName" label="站点名称" placeholder="例如:本地商店" rules={[{ required: true, message: '站点名称为必填项' }]} /> <ProFormText
name="siteName"
label="站点名称"
placeholder="例如:本地商店"
rules={[{ required: true, message: '站点名称为必填项' }]}
/>
{/* API 地址,可选 */} {/* API 地址,可选 */}
<ProFormText name="apiUrl" label="API 地址" placeholder="例如https://shop.example.com" /> <ProFormText
name="apiUrl"
label="API 地址"
placeholder="例如https://shop.example.com"
/>
{/* 平台类型选择 */} {/* 平台类型选择 */}
<ProFormSelect <ProFormSelect
name="type" name="type"
@ -247,14 +282,30 @@ const SiteList: React.FC = () => {
/> />
{/* 是否禁用 */} {/* 是否禁用 */}
<ProFormSwitch name="isDisabled" label="禁用" /> <ProFormSwitch name="isDisabled" label="禁用" />
<ProFormText name="skuPrefix" label="SKU 前缀" placeholder={editing ? '留空表示不修改' : '可选'} /> <ProFormText
name="skuPrefix"
label="SKU 前缀"
placeholder={editing ? '留空表示不修改' : '可选'}
/>
{/* WooCommerce REST consumer key新增必填编辑不填则保持原值 */} {/* 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新增必填编辑不填则保持原值 */} {/* 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> </DrawerForm>
</> </>
); );
}; };
export default SiteList; export default SiteList;

View File

@ -1,257 +1,279 @@
import React, { useEffect, useState, useMemo, useRef } from "react" import React, { useEffect, useMemo, useRef, useState } from 'react';
import { import {
ActionType, statisticscontrollerGetinativeusersbymonth,
PageContainer, ProColumns, ProTable, statisticscontrollerGetordersorce,
} from '@ant-design/pro-components'; } from '@/servers/api/statistics';
import { statisticscontrollerGetordersorce, statisticscontrollerGetinativeusersbymonth } from "@/servers/api/statistics"; import {
import ReactECharts from 'echarts-for-react'; ActionType,
import { App, Button, Space, Tag } from 'antd'; PageContainer,
import { HistoryOrder } from "../Order"; ProColumns,
ProTable,
} from '@ant-design/pro-components';
import { Space, Tag } from 'antd';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import ReactECharts from 'echarts-for-react';
import { HistoryOrder } from '../Order';
const ListPage: React.FC = () => { const ListPage: React.FC = () => {
const [data, setData] = useState({});
const [data, setData] = useState({}); useEffect(() => {
statisticscontrollerGetordersorce().then(({ data, success }) => {
if (success) setData(data);
});
}, []);
useEffect(() => { const option = useMemo(() => {
statisticscontrollerGetordersorce().then(({ data, success }) => { if (!data.inactiveRes) return {};
if(success) setData(data) const xAxisData = data?.inactiveRes
}); ?.map((v) => v.order_month)
}, []); ?.sort((_) => -1);
const arr = data?.res?.map((v) => v.first_order_month_group);
const option = useMemo(() => { const uniqueArr = arr
if(!data.inactiveRes) return {} .filter((item, index) => arr.indexOf(item) === index)
const xAxisData = data?.inactiveRes?.map(v=> v.order_month)?.sort(_=>-1) .sort((a, b) => a.localeCompare(b));
const arr = data?.res?.map(v=>v.first_order_month_group) const series = [
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[] = [
{ {
title: '用户名', name: '新客户',
dataIndex: 'username', type: 'bar',
hideInSearch: true, data: data?.inactiveRes?.map((v) => v.new_user_count)?.sort((_) => -1),
render: (_, record) => { label: {
if (record.billing.first_name || record.billing.last_name) show: true,
return record.billing.first_name + ' ' + record.billing.last_name;
return record.shipping.first_name + ' ' + record.shipping.last_name;
}, },
}, emphasis: {
{ focus: 'series',
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>
);
}, },
xAxisIndex: 0,
yAxisIndex: 0,
}, },
{ {
title: '操作', name: '老客户',
dataIndex: 'option', type: 'bar',
valueType: 'option', data: data?.inactiveRes?.map((v) => v.old_user_count)?.sort((_) => -1),
render: (_, record) => { label: {
return ( show: true,
<HistoryOrder },
email={record.email} emphasis: {
tags={record.tags} focus: 'series',
tableRef={actionRef} },
/> 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( return {
<PageContainer ghost> grid: [
<ReactECharts { top: '10%', height: '70%' },
option={option} { bottom: '10%', height: '10%' },
style={{ height: 1050 }} ],
onEvents={{ legend: {
click: async (params) => { selectedMode: false,
if (params.componentType === 'series') { },
setTableData([]) xAxis: [
const {success, data} = await statisticscontrollerGetinativeusersbymonth({ {
month: params.name type: 'category',
}) data: xAxisData,
if(success) setTableData(data) gridIndex: 0,
} },
}, {
}} type: 'category',
/> data: xAxisData,
{ gridIndex: 1,
tableData?.length ? },
<ProTable ],
search={false} yAxis: [
headerTitle="查询表格" {
actionRef={actionRef} type: 'value',
rowKey="id" gridIndex: 0,
dataSource={tableData} },
columns={columns} {
/> type: 'value',
:<></> gridIndex: 1,
},
} ],
</PageContainer> 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;

View File

@ -223,14 +223,17 @@ 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>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 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>

View File

@ -427,9 +427,7 @@ 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 ( return '数量:' + items?.reduce((acc, cur) => acc + cur.quantity, 0);
'数量:' + items?.reduce((acc, cur) => acc + cur.quantity, 0)
);
}} }}
</ProFormDependency> </ProFormDependency>
<ProFormList<API.PurchaseOrderItem> <ProFormList<API.PurchaseOrderItem>
@ -459,7 +457,7 @@ const UpdateForm: React.FC<{
return ( return (
data?.map((item) => { data?.map((item) => {
return { return {
label: `${item.name} - ${item.nameCn}`, label: `${item.name} - ${item.nameCn}`,
value: item.sku, value: item.sku,
}; };
}) || [] }) || []
@ -624,7 +622,7 @@ const DetailForm: React.FC<{
return ( return (
data?.map((item) => { data?.map((item) => {
return { return {
label: `${item.name} - ${item.nameCn}`, label: `${item.name} - ${item.nameCn}`,
value: item.sku, value: item.sku,
}; };
}) || [] }) || []

View File

@ -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: '出库'
}
}, },
}, },
{ {

View File

@ -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,24 +272,38 @@ const CreateForm: React.FC<{
rules={[{ required: true, message: '请选择源目标仓库' }]} rules={[{ required: true, message: '请选择源目标仓库' }]}
/> />
<ProFormTextArea name="note" label="备注" /> <ProFormTextArea name="note" label="备注" />
<ProFormText name={'orderNumber'} addonAfter={<Button onClick={async () => { <ProFormText
const orderNumber = await form.getFieldValue('orderNumber') name={'orderNumber'}
const { data } = await stockcontrollerGetpurchaseorder({orderNumber}) addonAfter={
form.setFieldsValue({ <Button
items: data?.map( onClick={async () => {
(item: { productName: string; productSku: string }) => ({ const orderNumber = await form.getFieldValue('orderNumber');
...item, const { data } = await stockcontrollerGetpurchaseorder({
productSku: { orderNumber,
label: item.productName, });
value: item.productSku, form.setFieldsValue({
}, items: data?.map(
}), (item: { productName: string; productSku: string }) => ({
), ...item,
}) productSku: {
}}></Button>} /> label: item.productName,
value: item.productSku,
},
}),
),
});
}}
>
</Button>
}
/>
<ProFormDependency name={['items']}> <ProFormDependency name={['items']}>
{({ items }) => { {({ items }) => {
return '数量:' + (items?.reduce?.((acc, cur) => acc + cur.quantity, 0)||0); return (
'数量:' +
(items?.reduce?.((acc, cur) => acc + cur.quantity, 0) || 0)
);
}} }}
</ProFormDependency> </ProFormDependency>
<ProFormList <ProFormList
@ -653,7 +667,7 @@ const DetailForm: React.FC<{
return ( return (
data?.map((item) => { data?.map((item) => {
return { return {
label: `${item.name} - ${item.nameCn}`, label: `${item.name} - ${item.nameCn}`,
value: item.sku, value: item.sku,
}; };
}) || [] }) || []

View File

@ -1,4 +1,8 @@
import React, { useRef, useState } from 'react'; import { sitecontrollerAll } from '@/servers/api/site';
import {
subscriptioncontrollerList,
subscriptioncontrollerSync,
} from '@/servers/api/subscription';
import { import {
ActionType, ActionType,
DrawerForm, DrawerForm,
@ -7,14 +11,10 @@ import {
ProFormSelect, ProFormSelect,
ProTable, ProTable,
} from '@ant-design/pro-components'; } 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 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';
/** /**
* () * ()
@ -77,7 +77,11 @@ const ListPage: React.FC = () => {
valueEnum: SUBSCRIPTION_STATUS_ENUM, valueEnum: SUBSCRIPTION_STATUS_ENUM,
// 以 Tag 形式展示,更易辨识 // 以 Tag 形式展示,更易辨识
render: (_, row) => 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, width: 120,
}, },
{ {
@ -152,9 +156,9 @@ 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[] = (Array.isArray(data) ? data : []).filter( const candidates: any[] = (
(c: any) => String(c?.externalOrderId) === parentNumber Array.isArray(data) ? data : []
); ).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) {
@ -230,14 +234,22 @@ const ListPage: React.FC = () => {
<List.Item> <List.Item>
<List.Item.Meta <List.Item.Meta
title={`#${item?.externalOrderId || '-'}`} title={`#${item?.externalOrderId || '-'}`}
description={`关系:${item?.relationship || '-'},站点:${item?.siteName || '-'}`} description={`关系:${item?.relationship || '-'},站点:${
item?.siteName || '-'
}`}
/> />
<div style={{ display: 'flex', gap: 12, alignItems: 'center' }}> <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> <Tag>{item?.status || '-'}</Tag>
<span> <span>
{item?.currency_symbol || ''} {item?.currency_symbol || ''}
{typeof item?.total === 'number' ? item.total.toFixed(2) : item?.total ?? '-'} {typeof item?.total === 'number'
? item.total.toFixed(2)
: item?.total ?? '-'}
</span> </span>
</div> </div>
</List.Item> </List.Item>
@ -274,7 +286,9 @@ const SyncForm: React.FC<{
*/ */
onFinish={async (values) => { onFinish={async (values) => {
try { try {
const { success, message: errMsg } = await subscriptioncontrollerSync(values); const { success, message: errMsg } = await subscriptioncontrollerSync(
values,
);
if (!success) { if (!success) {
throw new Error(errMsg); throw new Error(errMsg);
} }

View File

@ -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 { 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 一致) // 服务器 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 也独立到文件
@ -35,13 +25,13 @@ type OrderRecord = API.Order;
interface OrderDetailDrawerProps { interface OrderDetailDrawerProps {
tableRef: React.MutableRefObject<ActionType | undefined>; // 列表刷新引用 tableRef: React.MutableRefObject<ActionType | undefined>; // 列表刷新引用
orderId: number; // 订单主键 ID orderId: number; // 订单主键 ID
record: OrderRecord; // 订单行记录 record: OrderRecord; // 订单行记录
open: boolean; // 是否打开抽屉 open: boolean; // 是否打开抽屉
onClose: () => void; // 关闭抽屉回调 onClose: () => void; // 关闭抽屉回调
setActiveLine: (id: number) => void; // 高亮当前行 setActiveLine: (id: number) => void; // 高亮当前行
OrderNoteComponent: React.ComponentType<any>; // 备注组件(从外部注入) OrderNoteComponent: React.ComponentType<any>; // 备注组件(从外部注入)
SalesChangeComponent: React.ComponentType<any>; // 换货组件(从外部注入) SalesChangeComponent: React.ComponentType<any>; // 换货组件(从外部注入)
} }
const OrderDetailDrawer: React.FC<OrderDetailDrawerProps> = ({ const OrderDetailDrawer: React.FC<OrderDetailDrawerProps> = ({
@ -59,14 +49,18 @@ const OrderDetailDrawer: React.FC<OrderDetailDrawerProps> = ({
// 加载详情数据(与 index.tsx 中完全保持一致) // 加载详情数据(与 index.tsx 中完全保持一致)
const initRequest = async () => { 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; if (!success || !data) return { data: {} } as any;
data.sales = data.sales?.reduce((acc: API.OrderSale[], cur: API.OrderSale) => { data.sales = data.sales?.reduce(
const idx = acc.findIndex((v: any) => v.productId === cur.productId); (acc: API.OrderSale[], cur: API.OrderSale) => {
if (idx === -1) acc.push(cur); const idx = acc.findIndex((v: any) => v.productId === cur.productId);
else acc[idx].quantity += cur.quantity; if (idx === -1) acc.push(cur);
return acc; else acc[idx].quantity += cur.quantity;
}, []); return acc;
},
[],
);
return { data } as any; return { data } as any;
}; };
@ -220,108 +214,273 @@ const OrderDetailDrawer: React.FC<OrderDetailDrawerProps> = ({
: []), : []),
]} ]}
> >
<ProDescriptions labelStyle={{ width: '100px' }} actionRef={ref} request={initRequest}> <ProDescriptions
<ProDescriptions.Item label="站点" dataIndex="siteId" valueType="select" request={async () => { labelStyle={{ width: '100px' }}
const { data = [] } = await sitecontrollerAll(); actionRef={ref}
return data.map((item) => ({ label: item.siteName, value: item.id })); request={initRequest}
}} /> >
<ProDescriptions.Item label="订单日期" dataIndex="date_created" valueType="dateTime" /> <ProDescriptions.Item
<ProDescriptions.Item label="订单状态" dataIndex="orderStatus" valueType="select" valueEnum={ORDER_STATUS_ENUM as any} /> 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="total" />
<ProDescriptions.Item label="客户邮箱" dataIndex="customer_email" /> <ProDescriptions.Item label="客户邮箱" dataIndex="customer_email" />
<ProDescriptions.Item label="联系电话" span={3} render={(_, r: any) => ( <ProDescriptions.Item
<div><span>{r?.shipping?.phone || r?.billing?.phone || '-'}</span></div> 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="交易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 label="来源" render={(_, r: any) => formatSource(r.source_type, r.utm_source)} /> <ProDescriptions.Item
<ProDescriptions.Item label="原订单状态" dataIndex="status" valueType="select" valueEnum={ORDER_STATUS_ENUM as any} /> label="来源"
<ProDescriptions.Item label="支付链接" dataIndex="payment_url" span={3} copyable /> render={(_, r: any) => formatSource(r.source_type, r.utm_source)}
<ProDescriptions.Item label="客户备注" dataIndex="customer_note" span={3} /> />
<ProDescriptions.Item label="发货信息" span={3} render={(_, r: any) => ( <ProDescriptions.Item
<div> label="原订单状态"
<div>company:<span>{r?.shipping?.company || r?.billing?.company || '-'}</span></div> dataIndex="status"
<div>first_name:<span>{r?.shipping?.first_name || r?.billing?.first_name || '-'}</span></div> valueType="select"
<div>last_name:<span>{r?.shipping?.last_name || r?.billing?.last_name || '-'}</span></div> valueEnum={ORDER_STATUS_ENUM as any}
<div>country:<span>{r?.shipping?.country || r?.billing?.country || '-'}</span></div> />
<div>state:<span>{r?.shipping?.state || r?.billing?.state || '-'}</span></div> <ProDescriptions.Item
<div>city:<span>{r?.shipping?.city || r?.billing?.city || '-'}</span></div> label="支付链接"
<div>postcode:<span>{r?.shipping?.postcode || r?.billing?.postcode || '-'}</span></div> dataIndex="payment_url"
<div>phone:<span>{r?.shipping?.phone || r?.billing?.phone || '-'}</span></div> span={3}
<div>address_1:<span>{r?.shipping?.address_1 || r?.billing?.address_1 || '-'}</span></div> copyable
</div> />
)} /> <ProDescriptions.Item
<ProDescriptions.Item label="原始订单" span={3} render={(_, r: any) => ( label="客户备注"
<ul> dataIndex="customer_note"
{(r?.items || []).map((item: any) => ( span={3}
<li key={item.id}>{item.name}:{item.quantity}</li> />
))} <ProDescriptions.Item
</ul> label="发货信息"
)} /> span={3}
<ProDescriptions.Item label="关联" span={3} render={(_, r: any) => ( render={(_, r: any) => (
<RelatedOrders data={r?.related} /> <div>
)} /> <div>
<ProDescriptions.Item label="订单内容" span={3} render={(_, r: any) => ( company:
<ul> <span>
{(r?.sales || []).map((item: any) => ( {r?.shipping?.company || r?.billing?.company || '-'}
<li key={item.id}>{item.name}:{item.quantity}</li> </span>
))} </div>
</ul> <div>
)} /> first_name:
<ProDescriptions.Item label="换货" span={3} render={(_, r: any) => ( <span>
<SalesChangeComponent detailRef={ref} id={r.id as number} /> {r?.shipping?.first_name || r?.billing?.first_name || '-'}
)} /> </span>
<ProDescriptions.Item label="备注" span={3} render={(_, r: any) => { </div>
if (!r.notes || r.notes.length === 0) return (<Empty description="暂无备注" />); <div>
return ( last_name:
<div style={{ width: '100%' }}> <span>
{r.notes.map((note: any) => ( {r?.shipping?.last_name || r?.billing?.last_name || '-'}
<div style={{ marginBottom: 10 }} key={note.id}> </span>
<div style={{ display: 'flex', justifyContent: 'space-between' }}> </div>
<span>{note.username}</span> <div>
<span>{note.createdAt}</span> 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>
<div>{note.content}</div> ))}
</div> </div>
))} );
</div> }}
); />
}} /> <ProDescriptions.Item
<ProDescriptions.Item label="物流信息" span={3} render={(_, r: any) => { label="物流信息"
if (!r.shipment || r.shipment.length === 0) return (<Empty description="暂无物流信息" />); span={3}
return ( render={(_, r: any) => {
<div style={{ display: 'flex', flexDirection: 'column', width: '100%' }}> if (!r.shipment || r.shipment.length === 0)
{r.shipment.map((v: any) => ( return <Empty description="暂无物流信息" />;
<Card key={v.id} style={{ marginBottom: '10px' }} extra={formatShipmentState(v.state)} title={<> return (
{v.tracking_provider} <div
{v.primary_tracking_number} style={{
<CopyOutlined onClick={async () => { display: 'flex',
try { await navigator.clipboard.writeText(v.tracking_url); message.success('复制成功!'); } flexDirection: 'column',
catch { message.error('复制失败!'); } width: '100%',
}} /> }}
</>} >
actions={ (v.state === 'waiting-for-scheduling' || v.state === 'waiting-for-transit') ? [ {r.shipment.map((v: any) => (
<Popconfirm key="action-cancel" title="取消运单" description="确认取消运单?" onConfirm={async () => { <Card
try { const { success, message: errMsg } = await logisticscontrollerDelshipment({ id: v.id }); if (!success) throw new Error(errMsg); tableRef.current?.reload(); ref.current?.reload?.(); } key={v.id}
catch (error: any) { message.error(error.message); } style={{ marginBottom: '10px' }}
}}> extra={formatShipmentState(v.state)}
<DeleteFilled /> title={
</Popconfirm> <>
] : [] } {v.tracking_provider}
> {v.primary_tracking_number}
<div>: {Array.isArray(v?.orderIds) ? v.orderIds.join(',') : '-'}</div> <CopyOutlined
{Array.isArray(v?.items) && v.items.map((item: any) => ( onClick={async () => {
<div key={item.id}>{item.name}: {item.quantity}</div> try {
))} await navigator.clipboard.writeText(
</Card> v.tracking_url,
))} );
</div> 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> </ProDescriptions>
</Drawer> </Drawer>
); );
}; };
export default OrderDetailDrawer; export default OrderDetailDrawer;

View File

@ -1,7 +1,7 @@
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);
@ -12,17 +12,36 @@ dayjs.extend(relativeTime);
*/ */
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 = !!it?.externalSubscriptionId || !!it?.billing_period || !!it?.line_items; const isSubscription =
const number = isSubscription ? `#${it?.externalSubscriptionId || it?.id}` : `#${it?.externalOrderId || it?.id}`; !!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 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 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 = statusLower === 'active' ? 'green' : statusLower === 'cancelled' ? 'red' : 'default'; const color =
statusLower === 'active'
? 'green'
: statusLower === 'cancelled'
? 'red'
: 'default';
const totalNum = Number(it?.total || 0); const totalNum = Number(it?.total || 0);
const totalText = isSubscription ? `$${totalNum.toFixed(2)} / ${it?.billing_period || 'period'}` : `$${totalNum.toFixed(2)}`; const totalText = isSubscription
return { key: `${isSubscription ? 'sub' : 'order'}-${it?.id}`, number, relationship, dateText, status, color, totalText }; ? `$${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="暂无关联" />; if (rows.length === 0) return <Empty description="暂无关联" />;
@ -30,7 +49,14 @@ const RelatedOrders: React.FC<{ data?: any[] }> = ({ data = [] }) => {
return ( return (
<div style={{ width: '100%' }}> <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> <div></div>
<div></div> <div></div>
@ -39,11 +65,23 @@ const RelatedOrders: React.FC<{ data?: any[] }> = ({ data = [] }) => {
</div> </div>
<div> <div>
{rows.map((r) => ( {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
<div><a>{r.number}</a></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>{r.relationship}</div>
<div style={{ color: '#1677ff' }}>{r.dateText}</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>{r.totalText}</div>
</div> </div>
))} ))}

View File

@ -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 { ordercontrollerGetorders } from '@/servers/api/order';
import OrderDetailDrawer from './OrderDetailDrawer';
import { sitecontrollerAll } from '@/servers/api/site'; 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 { interface OrderItemRow {
id: number; id: number;
@ -43,7 +47,10 @@ 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) => ({ 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', dataIndex: 'date_created',
width: 180, width: 180,
hideInSearch: true, 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: '邮箱', title: '邮箱',
@ -109,7 +119,14 @@ const OrdersPage: React.FC = () => {
const request: ProTableProps<OrderItemRow>['request'] = async (params) => { const request: ProTableProps<OrderItemRow>['request'] = async (params) => {
try { 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 [startDate, endDate] = (params as any).dateRange || [];
const resp = await ordercontrollerGetorders({ const resp = await ordercontrollerGetorders({
current, current,
@ -119,7 +136,9 @@ const OrdersPage: React.FC = () => {
customer_email, customer_email,
payment_method, payment_method,
isSubscriptionOnly: true as any, 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, 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;
@ -136,10 +155,10 @@ 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={{ showSizeChanger: true }} pagination={{ showSizeChanger: true }}

View File

@ -87,7 +87,9 @@ const List: React.FC = () => {
rowKey="id" rowKey="id"
toolBarRender={() => [<CreateForm tableRef={actionRef} />]} toolBarRender={() => [<CreateForm tableRef={actionRef} />]}
request={async (params) => { request={async (params) => {
const response = await templatecontrollerGettemplatelist(params as any) as any; const response = (await templatecontrollerGettemplatelist(
params as any,
)) as any;
return { return {
data: response.items || [], data: response.items || [],
total: response.total || 0, total: response.total || 0,
@ -202,5 +204,4 @@ const UpdateForm: React.FC<{
); );
}; };
export default List; export default List;

View File

@ -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,8 +16,9 @@ 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 } = const { data: trackList } = await logisticscontrollerGetorderlist({
await logisticscontrollerGetorderlist({ number: keyWords }); number: keyWords,
});
return trackList?.map((v) => { return trackList?.map((v) => {
return { return {
label: v.siteName + ' ' + v.externalOrderId, label: v.siteName + ' ' + v.externalOrderId,
@ -29,8 +30,8 @@ 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,
}); });
@ -53,32 +54,34 @@ const TrackPage: React.FC = () => {
), ),
}} }}
/> />
{ {data?.item ? (
data?.item ? <div>
<div> <div>
<div> <h4></h4>
<h4></h4> {data?.item?.map((item) => (
{data?.item?.map((item) => ( <div style={{ paddingLeft: 20, color: 'blue' }}>
<div style={{ paddingLeft: 20, color: 'blue' }}> {item.name} * {item.quantity}
{item.name} * {item.quantity} </div>
</div> ))}
))} </div>
</div> </div>
</div> : <></> ) : (
} <></>
{ )}
data?.saleItem ? {data?.saleItem ? (
<div>
<div> <div>
<div> <h4></h4>
<h4></h4> {data?.saleItem?.map((item) => (
{data?.saleItem?.map((item) => ( <div style={{ paddingLeft: 20, color: 'blue' }}>
<div style={{ paddingLeft: 20, color: 'blue' }}> {item.name} * {item.quantity}
{item.name} * {item.quantity} </div>
</div> ))}
))} </div>
</div> </div>
</div> : <></> ) : (
} <></>
)}
</PageContainer> </PageContainer>
); );
}; };

View File

@ -1,3 +1,3 @@
.selected-line-order-protable { .selected-line-order-protable {
background-color: #add8e6; background-color: #add8e6;
} }

8
typings.d.ts vendored
View File

@ -1,16 +1,14 @@
import '@umijs/max/typings'; import '@umijs/max/typings';
declare namespace BaseType { declare namespace BaseType {
type EnumTransformOptions = {
type EnumTransformOptions {
value: string; // 用于作为 value 的字段名 value: string; // 用于作为 value 的字段名
label: string; // 用于作为 text 的字段名 label: string; // 用于作为 text 的字段名
status?: string | undefined; // 可选:用于设置状态的字段名 status?: string | undefined; // 可选:用于设置状态的字段名
color?: string | undefined; // 可选:用于设置颜色的字段名 color?: string | undefined; // 可选:用于设置颜色的字段名
} };
} }
declare global { declare global {
const UMI_APP_API_URL: string; const UMI_APP_API_URL: string;
} }