6 Commits

Author SHA1 Message Date
88aae37295 Merge branch 'wuxichen' of http://gitea.tashowz.com/tashow/tashow-manager into qianpw 2025-11-19 10:08:47 +08:00
607b292f36 feat: 售后列表文件 2025-11-19 10:05:22 +08:00
a15bda739a feat: 取消订单 2025-10-31 16:04:17 +08:00
85e1696101 feat: 菜单添加图标 2025-10-30 16:32:03 +08:00
2ad04594fe fix: 合并冲突 2025-10-30 16:27:19 +08:00
126ee7b50a feat: 系统管理 2025-10-30 16:12:02 +08:00
38 changed files with 1113 additions and 131 deletions

View File

@@ -39,6 +39,7 @@
"@ant-design/icons": "^5.6.1", "@ant-design/icons": "^5.6.1",
"@ant-design/pro-components": "^2.8.9", "@ant-design/pro-components": "^2.8.9",
"@ant-design/v5-patch-for-react-19": "^1.0.3", "@ant-design/v5-patch-for-react-19": "^1.0.3",
"@icon-park/react": "^1.4.2",
"@dnd-kit/core": "^6.3.1", "@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0", "@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2", "@dnd-kit/utilities": "^3.2.2",
@@ -48,6 +49,7 @@
"browser-id3-writer": "^6.3.1", "browser-id3-writer": "^6.3.1",
"classnames": "^2.5.1", "classnames": "^2.5.1",
"dayjs": "^1.11.13", "dayjs": "^1.11.13",
"dompurify": "^3.3.0",
"jsencrypt": "^3.5.4", "jsencrypt": "^3.5.4",
"rc-resize-observer": "^1.4.3", "rc-resize-observer": "^1.4.3",
"react": "^19.1.0", "react": "^19.1.0",

31
pnpm-lock.yaml generated
View File

@@ -17,6 +17,9 @@ importers:
'@ant-design/v5-patch-for-react-19': '@ant-design/v5-patch-for-react-19':
specifier: ^1.0.3 specifier: ^1.0.3
version: 1.0.3(antd@5.27.3(date-fns@2.30.0)(moment@2.30.1)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(react-dom@19.1.1(react@19.1.1))(react@19.1.1) version: 1.0.3(antd@5.27.3(date-fns@2.30.0)(moment@2.30.1)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
'@icon-park/react':
specifier: ^1.4.2
version: 1.4.2(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
'@dnd-kit/core': '@dnd-kit/core':
specifier: ^6.3.1 specifier: ^6.3.1
version: 6.3.1(react-dom@19.1.1(react@19.1.1))(react@19.1.1) version: 6.3.1(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
@@ -44,6 +47,9 @@ importers:
dayjs: dayjs:
specifier: ^1.11.13 specifier: ^1.11.13
version: 1.11.18 version: 1.11.18
dompurify:
specifier: ^3.3.0
version: 3.3.0
jsencrypt: jsencrypt:
specifier: ^3.5.4 specifier: ^3.5.4
version: 3.5.4 version: 3.5.4
@@ -1890,6 +1896,13 @@ packages:
resolution: {integrity: sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==} resolution: {integrity: sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==}
deprecated: Use @eslint/object-schema instead deprecated: Use @eslint/object-schema instead
'@icon-park/react@1.4.2':
resolution: {integrity: sha512-+MtQLjNiRuia3fC/NfpSCTIy5KH5b+NkMB9zYd7p3R4aAIK61AjK0OSraaICJdkKooU9jpzk8m0fY4g9A3JqhQ==}
engines: {node: '>= 8.0.0', npm: '>= 5.0.0'}
peerDependencies:
react: '>=16.9'
react-dom: '>=16.9'
'@iconify/types@2.0.0': '@iconify/types@2.0.0':
resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==} resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==}
@@ -2818,6 +2831,9 @@ packages:
'@types/tough-cookie@4.0.5': '@types/tough-cookie@4.0.5':
resolution: {integrity: sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==} resolution: {integrity: sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==}
'@types/trusted-types@2.0.7':
resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==}
'@types/unist@2.0.11': '@types/unist@2.0.11':
resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==} resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==}
@@ -4807,6 +4823,9 @@ packages:
resolution: {integrity: sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==} resolution: {integrity: sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==}
engines: {node: '>= 4'} engines: {node: '>= 4'}
dompurify@3.3.0:
resolution: {integrity: sha512-r+f6MYR1gGN1eJv0TVQbhA7if/U7P87cdPl3HN5rikqaBSBxLiCb/b9O+2eG0cxz0ghyU+mU1QkbsOwERMYlWQ==}
domutils@1.7.0: domutils@1.7.0:
resolution: {integrity: sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==} resolution: {integrity: sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==}
@@ -13828,6 +13847,11 @@ snapshots:
'@humanwhocodes/object-schema@2.0.3': {} '@humanwhocodes/object-schema@2.0.3': {}
'@icon-park/react@1.4.2(react-dom@19.1.1(react@19.1.1))(react@19.1.1)':
dependencies:
react: 19.1.1
react-dom: 19.1.1(react@19.1.1)
'@iconify/types@2.0.0': {} '@iconify/types@2.0.0': {}
'@iconify/utils@2.1.1': '@iconify/utils@2.1.1':
@@ -15041,6 +15065,9 @@ snapshots:
'@types/tough-cookie@4.0.5': {} '@types/tough-cookie@4.0.5': {}
'@types/trusted-types@2.0.7':
optional: true
'@types/unist@2.0.11': {} '@types/unist@2.0.11': {}
'@types/use-sync-external-store@0.0.3': {} '@types/use-sync-external-store@0.0.3': {}
@@ -17939,6 +17966,10 @@ snapshots:
dependencies: dependencies:
domelementtype: 2.3.0 domelementtype: 2.3.0
dompurify@3.3.0:
optionalDependencies:
'@types/trusted-types': 2.0.7
domutils@1.7.0: domutils@1.7.0:
dependencies: dependencies:
dom-serializer: 0.2.2 dom-serializer: 0.2.2

View File

@@ -0,0 +1,73 @@
/* index.less */
.productImages {
padding: 16px;
.mainImage {
margin-bottom: 16px;
text-align: center;
.largeImage {
border: 1px solid #f0f0f0;
border-radius: 4px;
}
}
.thumbnailList {
.thumbnail {
padding: 2px;
border: 2px solid transparent;
border-radius: 4px;
cursor: pointer;
transition: all 0.3s;
&:hover {
border-color: #d9d9d9;
}
&.active {
border-color: #ff4d4f;
}
}
}
}
.advancedGallery {
.mainImageContainer {
margin-bottom: 16px;
text-align: center;
}
.thumbnailContainer {
display: flex;
align-items: center;
gap: 8px;
.scrollBtn {
flex-shrink: 0;
}
.thumbnailWrapper {
display: flex;
gap: 8px;
overflow-x: auto;
scroll-behavior: smooth;
&::-webkit-scrollbar {
display: none;
}
}
.thumbnailItem {
flex-shrink: 0;
padding: 2px;
border: 2px solid transparent;
border-radius: 4px;
cursor: pointer;
&.active {
border-color: #1890ff;
}
}
}
}

View File

@@ -0,0 +1,71 @@
import { LeftOutlined, RightOutlined } from '@ant-design/icons';
import { Button, Image } from 'antd';
import React, { useRef, useState } from 'react';
import styles from './index.module.less';
const AdvancedImageGallery: React.FC<{ images: string[] }> = ({ images }) => {
const [currentIndex, setCurrentIndex] = useState(0);
const thumbnailRef = useRef<HTMLDivElement>(null);
// 缩略图滚动
const scrollThumbnails = (direction: 'left' | 'right') => {
if (thumbnailRef.current) {
const scrollAmount = direction === 'left' ? -100 : 100;
thumbnailRef.current.scrollLeft += scrollAmount;
}
};
return (
<div className={styles.advancedGallery}>
{/* 大图区域 */}
<div className={styles.mainImageContainer}>
<Image
src={images[currentIndex]}
alt="主图"
width={'100%'}
height={376}
style={{ objectFit: 'cover' }}
/>
</div>
{/* 缩略图区域 */}
<div className={styles.thumbnailContainer}>
<Button
icon={<LeftOutlined />}
size="small"
onClick={() => scrollThumbnails('left')}
className={styles.scrollBtn}
/>
<div ref={thumbnailRef} className={styles.thumbnailWrapper}>
{images.map((img, index) => (
<div
key={`${index}${Math.random()}`}
className={`${styles.thumbnailItem} ${
index === currentIndex ? styles.active : ''
}`}
onClick={() => setCurrentIndex(index)}
>
<Image
src={img}
width={80}
height={80}
preview={false}
alt={`缩略图-${index}`}
/>
</div>
))}
</div>
<Button
icon={<RightOutlined />}
size="small"
onClick={() => scrollThumbnails('right')}
className={styles.scrollBtn}
/>
</div>
</div>
);
};
export default React.memo(AdvancedImageGallery);

View File

@@ -0,0 +1,7 @@
.dangerouslySetInnerHTML {
:global {
p {
margin-bottom: 12px;
}
}
}

View File

@@ -0,0 +1,16 @@
import DOMPurify from 'dompurify';
import React from 'react';
import styles from './index.module.less';
const DangerouslySetInnerHTML = (props: { content?: string }) => {
const { content = '' } = props;
return (
<div
className={styles.dangerouslySetInnerHTML}
// biome-ignore lint/security/noDangerouslySetInnerHtml: 已使用 DOMPurify 进行 HTML 净化
dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(content) }}
/>
);
};
export default React.memo(DangerouslySetInnerHTML);

View File

@@ -22,6 +22,7 @@ function EnhancedProTable<T extends BaseRecord, U extends ParamsType = any>(
toolbarActions, toolbarActions,
rowKey = 'id', rowKey = 'id',
pagination = true, pagination = true,
scroll,
...restProps ...restProps
} = props; } = props;
@@ -55,7 +56,7 @@ function EnhancedProTable<T extends BaseRecord, U extends ParamsType = any>(
manualRequest={false} manualRequest={false}
showSorterTooltip showSorterTooltip
// scroll={{ x: "max-content" }} // scroll={{ x: "max-content" }}
scroll={{ x: 1200 }} scroll={scroll ? scroll : { x: 1200 }}
components={components} components={components}
search={ search={
search search

View File

@@ -35,7 +35,15 @@ const UploadImages: React.FC<{
const [uploading, setUploading] = useState(false); const [uploading, setUploading] = useState(false);
useEffect(() => { useEffect(() => {
if (value) { if (value) {
setFileList([{ uid: '-1', url: value, status: 'done', name: value }]); console.log(value.split(','));
const list = value.split(',').map((item, index) => ({
uid: index.toString(),
url: item,
thumbUrl: item,
status: 'done',
name: item,
}));
setFileList(list as UploadFile[]);
} else { } else {
setFileList([]); setFileList([]);
} }
@@ -63,7 +71,7 @@ const UploadImages: React.FC<{
const handleRemove = (file: UploadFile): boolean => { const handleRemove = (file: UploadFile): boolean => {
const newFileList = fileList.filter((item) => item.uid !== file.uid); const newFileList = fileList.filter((item) => item.uid !== file.uid);
const newUrl = newFileList.map((item) => item.url) as string[]; const newUrl = newFileList.map((item) => item.url) as string[];
onChange?.(newUrl[0]); onChange?.(newUrl.join(','));
return true; return true;
}; };
@@ -101,8 +109,15 @@ const UploadImages: React.FC<{
const url = await uploadFile(file as File); const url = await uploadFile(file as File);
onProgress?.({ percent: 100 }); onProgress?.({ percent: 100 });
if (url) { if (url) {
if (maxCount === 1) {
onChange?.(url); onChange?.(url);
onSuccess?.({ url }); onSuccess?.({ url });
} else {
const res = value?.split(',') || [];
console.log([...res, url].join(','));
onChange?.([...res, url].join(','));
}
message.success('上传成功'); message.success('上传成功');
} else { } else {
throw new Error('上传失败'); throw new Error('上传失败');

View File

View File

View File

@@ -74,7 +74,7 @@ const ProdInfo = <T extends Record<string, any>>(props: ProdInfoProps<T>) => {
extra="仅支持.jpg .png 格式建议图片比例1:1限7张" extra="仅支持.jpg .png 格式建议图片比例1:1限7张"
// getValueFromEvent={(e) => e.fileList} // getValueFromEvent={(e) => e.fileList}
> >
<UploadImages /> <UploadImages maxCount={7} />
</ProForm.Item> </ProForm.Item>
<ProForm.Item <ProForm.Item
style={{ width: '100%' }} style={{ width: '100%' }}

View File

@@ -5,8 +5,9 @@ import type {
import { Tag } from 'antd'; import { Tag } from 'antd';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { tenantStatus } from '@/constants'; import { tenantStatus } from '@/constants';
import type { DeptVO } from '@/services/system/dept'; import { type DeptVO, getSimpleDeptList } from '@/services/system/dept';
import { getStatusLabel } from '@/utils/constant'; import { getStatusLabel } from '@/utils/constant';
import { handleTree } from '@/utils/tree';
export const baseDeptColumns: ProColumns<DeptVO>[] = [ export const baseDeptColumns: ProColumns<DeptVO>[] = [
{ {
@@ -84,11 +85,28 @@ export const formColumns = (_type: string): ProFormColumnsType[] => [
valueType: 'treeSelect', valueType: 'treeSelect',
fieldProps: () => { fieldProps: () => {
return { return {
multiple: true,
placeholder: '请选择上级部门', placeholder: '请选择上级部门',
options: [{ label: '11', value: 5016 }],
}; };
}, },
request: async () => {
// 调用getSimplePostList方法获取数据
const res = await getSimpleDeptList();
const deptTree = handleTree(res);
const formatToOptions = (nodes: Tree[]): any[] =>
nodes.map((node) => ({
label: node.name,
value: node.id,
children: node.children ? formatToOptions(node.children) : undefined,
}));
return [
{
label: '顶级部门',
value: 0,
children: formatToOptions(deptTree),
},
];
},
}, },
{ {
title: '部门名称', title: '部门名称',
@@ -150,12 +168,12 @@ export const formColumns = (_type: string): ProFormColumnsType[] => [
fieldProps: { fieldProps: {
options: [ options: [
{ {
label: '正常', label: '开启',
value: 1, value: 0,
}, },
{ {
label: '禁用', label: '关闭',
value: 2, value: 1,
}, },
], ],
}, },

View File

@@ -5,15 +5,12 @@ import {
ProFormRadio, ProFormRadio,
ProFormText, ProFormText,
} from '@ant-design/pro-components'; } from '@ant-design/pro-components';
import { Switch } from 'antd'; import { Select, Switch } from 'antd';
import { CommonStatusEnum } from '@/constants'; import { CommonStatusEnum } from '@/constants';
import { useMessage } from '@/hooks/antd/useMessage'; import { useMessage } from '@/hooks/antd/useMessage';
import { import { getMenuList, type MenuVO, updateMenu } from '@/services/system/menu';
getSimpleMenusList,
type MenuVO,
updateMenu,
} from '@/services/system/menu';
import { handleTree } from '@/utils/tree'; import { handleTree } from '@/utils/tree';
import IconSelector from './icon';
const handleStatus = async (record: MenuVO) => { const handleStatus = async (record: MenuVO) => {
const message = useMessage(); // 消息弹窗 const message = useMessage(); // 消息弹窗
@@ -86,7 +83,7 @@ export const formColumns = (_type: string): ProFormColumnsType[] => [
dataIndex: 'parentId', dataIndex: 'parentId',
valueType: 'treeSelect', valueType: 'treeSelect',
request: async () => { request: async () => {
const res = await getSimpleMenusList(); const res = await getMenuList({});
console.log(res); console.log(res);
const menu: Tree = { id: 0, name: '主类目', children: [] }; const menu: Tree = { id: 0, name: '主类目', children: [] };
menu.children = handleTree(res); menu.children = handleTree(res);
@@ -130,24 +127,44 @@ export const formColumns = (_type: string): ProFormColumnsType[] => [
], ],
}, },
}, },
// {
// title: "菜单图标",
// dataIndex: "icon",
// fieldProps: {
// placeholder: "请选择图标",
// },
// dependencies: ["type"],
// renderFormItem: (_schema, _config, form) => {
// const type = form.getFieldValue("type");
// if (type === 3) return null;
// return (
// <ProFormText
// formItemProps={{
// style: {
// marginBottom: 0,
// },
// }}
// />
// );
// },
// },
{ {
title: '菜单图标', title: '菜单图标',
dataIndex: 'icon', dataIndex: 'icon',
fieldProps: {
placeholder: '请选择图标',
},
dependencies: ['type'], dependencies: ['type'],
renderFormItem: (_schema, _config, form) => { renderFormItem: (_schema, _config, form) => {
const type = form.getFieldValue('type'); const type = form.getFieldValue('type');
if (type === 3) return null; if (type === 3) return null;
return (
<ProFormText return <IconSelector />;
formItemProps={{
style: {
marginBottom: 0,
}, },
}} render: (_, record: MenuVO) => {
/> if (!record.icon) return '—';
// 保持原有渲染逻辑
return (
<span>
{record.icon ? <i className={`anticon ${record.icon}`} /> : '—'}
</span>
); );
}, },
}, },

View File

@@ -0,0 +1,187 @@
import * as IconPark from '@icon-park/react';
import { Input, List, Modal, Pagination } from 'antd';
import { useState } from 'react';
const allIcons = Object.keys(IconPark);
// 动态构造 iconMap
const iconMap: Record<string, React.ReactNode> = {};
allIcons.forEach((iconName) => {
// 排除不需要的属性(如默认导出等)
if (
iconName !== 'default' &&
typeof IconPark[iconName as keyof typeof IconPark] === 'function'
) {
const IconComponent = IconPark[
iconName as keyof typeof IconPark
] as React.ComponentType<any>;
iconMap[iconName] = <IconComponent theme="outline" size="16" />;
}
});
// 图标选项列表
const iconOptions = Object.keys(iconMap).map((key) => ({
label: (
<span>
{iconMap[key]} {key}
</span>
),
value: key,
}));
interface IconSelectorProps {
value?: string;
onChange?: (value: string) => void;
}
const IconSelector: React.FC<IconSelectorProps> = ({ value, onChange }) => {
const [open, setOpen] = useState(false);
const [searchValue, setSearchValue] = useState('');
const [currentPage, setCurrentPage] = useState(1);
const pageSize = 48;
// 过滤图标
const filteredIcons = iconOptions.filter(
(option) =>
option.value.toLowerCase().includes(searchValue.toLowerCase()) ||
option.value.toLowerCase().includes(searchValue.toLowerCase()),
);
// 分页数据
const paginatedIcons = filteredIcons.slice(
(currentPage - 1) * pageSize,
currentPage * pageSize,
);
const handleSelect = (iconName: string) => {
onChange?.(iconName);
setOpen(false);
setCurrentPage(1);
setSearchValue('');
};
const handleClear = () => {
onChange?.('');
setOpen(false);
};
return (
<>
<div
onClick={() => setOpen(true)}
style={{
border: '1px solid #d9d9d9',
borderRadius: 6,
padding: '4px 11px',
cursor: 'pointer',
minHeight: 32,
display: 'flex',
alignItems: 'center',
}}
>
{value ? (
<span>
{iconMap[value as keyof typeof iconMap]}
<span style={{ marginLeft: 8 }}>{value}</span>
</span>
) : (
<span style={{ color: '#bfbfbf' }}></span>
)}
</div>
<Modal
title="选择图标"
open={open}
onCancel={() => {
setOpen(false);
setCurrentPage(1);
setSearchValue('');
}}
onOk={() => setOpen(false)}
width={800}
styles={{ body: { padding: 0 } }}
okText="确定"
cancelText="取消"
>
<div style={{ padding: 16 }}>
<Input.Search
placeholder="搜索图标..."
value={searchValue}
onChange={(e) => {
setSearchValue(e.target.value);
setCurrentPage(1);
}}
style={{ marginBottom: 16 }}
/>
{filteredIcons.length === 0 ? (
<IconPark.Empty title="未找到相关图标" />
) : (
<>
<List
grid={{ gutter: 16, column: 8 }}
dataSource={paginatedIcons}
renderItem={(item) => (
<List.Item
onClick={() => handleSelect(item.value)}
style={{
cursor: 'pointer',
textAlign: 'center',
border:
value === item.value
? '1px solid #1890ff'
: '1px solid transparent',
borderRadius: 6,
padding: 8,
}}
>
<div>
<div style={{ fontSize: 20, marginBottom: 4 }}>
{iconMap[item.value as keyof typeof iconMap]}
</div>
<div
style={{
fontSize: 12,
overflow: 'hidden',
textOverflow: 'ellipsis',
}}
>
{item.value}
</div>
</div>
</List.Item>
)}
/>
<div
style={{
display: 'flex',
justifyContent: 'space-between',
marginTop: 16,
}}
>
<Pagination
current={currentPage}
pageSize={pageSize}
total={filteredIcons.length}
onChange={setCurrentPage}
showSizeChanger={false}
size="small"
/>
<div>
<a onClick={handleClear} style={{ marginRight: 16 }}>
</a>
</div>
</div>
</>
)}
</div>
</Modal>
</>
);
};
export default IconSelector;

View File

@@ -55,6 +55,14 @@ const SystemMenu = () => {
configurableDrawerRef.current?.open({ type: 1 }); configurableDrawerRef.current?.open({ type: 1 });
}; };
const handleAddChild = (record: MenuVO) => {
setType('create');
configurableDrawerRef.current?.open({
type: 2,
parentId: record.id,
});
};
const handleReload = async () => { const handleReload = async () => {
try { try {
await modal.confirm({ await modal.confirm({
@@ -127,6 +135,9 @@ const SystemMenu = () => {
> >
<a></a> <a></a>
</Popconfirm>, </Popconfirm>,
<a key="add" onClick={() => handleAddChild(record)}>
</a>,
], ],
}; };
const columns = [...baseMenuColumns, actionColumns]; const columns = [...baseMenuColumns, actionColumns];

View File

@@ -5,8 +5,11 @@ import type {
} from '@ant-design/pro-components'; } from '@ant-design/pro-components';
import { Modal, message, Switch } from 'antd'; import { Modal, message, Switch } from 'antd';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { getSimpleDeptList } from '@/services/system/dept';
import { getSimplePostList } from '@/services/system/post';
import { updateUserStatus } from '@/services/system/user'; import { updateUserStatus } from '@/services/system/user';
import type { UserVO } from '@/services/system/user/index'; import type { UserVO } from '@/services/system/user/index';
import { handleTree } from '@/utils/tree';
export const baseTenantColumns: ProColumns<UserVO>[] = [ export const baseTenantColumns: ProColumns<UserVO>[] = [
{ {
@@ -99,6 +102,20 @@ export const baseTenantColumns: ProColumns<UserVO>[] = [
]; ];
export const formColumns = (type: string): ProFormColumnsType[] => [ export const formColumns = (type: string): ProFormColumnsType[] => [
{
title: '用户账号',
dataIndex: 'username',
fieldProps: {
style: { display: 'none' },
},
formItemProps: {
style: {
display: 'none',
margin: 0,
padding: 0,
},
},
},
{ {
title: '用户昵称', title: '用户昵称',
dataIndex: 'nickname', dataIndex: 'nickname',
@@ -115,10 +132,20 @@ export const formColumns = (type: string): ProFormColumnsType[] => [
title: '归属部门', title: '归属部门',
dataIndex: 'deptId', dataIndex: 'deptId',
valueType: 'treeSelect', valueType: 'treeSelect',
fieldProps: { fieldProps: {
multiple: true,
placeholder: '请选择归属部门', placeholder: '请选择归属部门',
options: [{ lable: '11', value: 5016 }], fieldNames: {
label: 'name',
value: 'id',
children: 'children',
},
},
request: async () => {
const res = await getSimpleDeptList();
const data = handleTree(res);
console.log('data', data);
return data;
}, },
}, },
{ {
@@ -199,18 +226,19 @@ export const formColumns = (type: string): ProFormColumnsType[] => [
fieldProps: { fieldProps: {
mode: 'multiple', mode: 'multiple',
placeholder: '请选择岗位', placeholder: '请选择岗位',
options: [
{
label: '管理员',
value: 1,
}, },
{ request: async () => {
label: '普通用户', // 调用getSimplePostList方法获取数据
value: 2, const res = await getSimplePostList();
const data =
res.map((item: any) => ({
label: item.label || item.name,
value: item.value || item.id,
})) || [];
return data;
}, },
],
},
formItemProps: {},
}, },
{ {
title: '备注', title: '备注',

View File

@@ -0,0 +1,83 @@
import {
BetaSchemaForm,
type ProFormColumnsType,
} from '@ant-design/pro-components';
import { type FormInstance, Modal, message } from 'antd';
import React, { useCallback, useRef, useState } from 'react';
import { cancelOrder } from '@/services/trade/order';
const CancleOrderModal: React.FC<{
open?: boolean;
id?: number;
onClose?: () => void;
}> = (props) => {
const formRef = useRef<FormInstance>(null);
const [loading, setLoading] = useState<boolean>(false);
const columns: ProFormColumnsType[] = [
{
title: '',
dataIndex: 'cancelReason',
valueType: 'radio',
fieldProps: {
options: [
{ value: '用户主动取消', label: '用户主动取消' },
{ value: '选项二', label: '选项二' },
{ value: '选项三', label: '选项三' },
{ value: '选项四', label: '选项四' },
{ value: '选项五', label: '选项五' },
{ value: '选项六', label: '选项六' },
{ value: '选项七', label: '选项七' },
],
style: {
display: 'flex',
flexDirection: 'column',
gap: '8px',
marginTop: '6px',
background: '#f0f0f0',
padding: '8px',
borderRadius: '8px',
},
},
},
{
title: '取消备注',
dataIndex: 'cancelRemark',
valueType: 'textarea',
},
];
const handleOk = useCallback(async () => {
try {
setLoading(true);
const values = formRef.current?.getFieldsValue();
console.log(values, props.id, 'ok');
await cancelOrder({ id: props.id, ...values });
message.success('取消成功');
props.onClose?.();
} finally {
setLoading(false);
}
}, [open, props.id]);
return (
<Modal
title="取消订单"
open={props.open}
onOk={handleOk}
onCancel={props?.onClose}
confirmLoading={loading}
>
<span></span>
<BetaSchemaForm
layoutType="Form"
formRef={formRef}
columns={columns || []}
layout="horizontal"
submitter={false}
labelCol={{ span: 4 }}
wrapperCol={{ span: 20 }}
/>
</Modal>
);
};
export default React.memo(CancleOrderModal);

View File

@@ -14,10 +14,10 @@ export const baseOrderColumns: ProColumns<TradeOrderPageRespVO>[] = [
if (!record.items) { if (!record.items) {
return _; return _;
} }
return record.items.map((item) => ( return record.items.map((item, index) => (
<div <div
style={{ width: '100%', display: 'flex', gap: '8px' }} style={{ width: '100%', display: 'flex', gap: '8px' }}
key={item.id} key={`${index}${Math.random()}`}
> >
<Image src={item.picUrl} width={64} height={64} /> <Image src={item.picUrl} width={64} height={64} />
<div style={{ flex: '1', overflow: 'hidden' }}> <div style={{ flex: '1', overflow: 'hidden' }}>

View File

@@ -21,7 +21,7 @@ const DetailCom: React.FC<{ data?: TradeOrderPageRespVO }> = (props) => {
children: '商品配送', children: '商品配送',
}, },
]; ];
return <Tabs defaultActiveKey="1" items={items} />; return <Tabs defaultActiveKey="1" items={items} destroyOnHidden />;
}; };
export default React.memo(DetailCom); export default React.memo(DetailCom);

View File

@@ -5,16 +5,17 @@ import {
type TradeOrderDetailRespVO, type TradeOrderDetailRespVO,
type TradeOrderPageRespVO, type TradeOrderPageRespVO,
} from '@/services/trade/order'; } from '@/services/trade/order';
import BasicInfo from './component/info/basic-info'; //基本信息(通版) import BasicInfo from './src/info/basic-info'; //基本信息(通版)
import ExtendCostInfo from './component/info/extend-cost'; //服务附加费(殡葬专属字段) import ExtendCostInfo from './src/info/extend-cost'; //服务附加费(殡葬专属字段)
import ExtendService from './component/info/extend-service'; //可选服务(殡葬专属字段) import ExtendService from './src/info/extend-service'; //可选服务(殡葬专属字段)
import ProdInfo from './component/info/prod-info'; //商品信息(通版) import ProdInfo from './src/info/prod-info'; //商品信息(通版)
import ServiceInfo from './component/info/service-info'; import ServiceInfo from './src/info/service-info';
export interface ItemConfig<T> { export interface ItemConfig<T> {
data?: T; data?: T;
loading?: boolean; loading?: boolean;
orderCategoryId?: number; orderCategoryId?: number;
id?: number;
} }
const OrderDetail: React.FC<{ data?: TradeOrderPageRespVO }> = (props) => { const OrderDetail: React.FC<{ data?: TradeOrderPageRespVO }> = (props) => {
const { data } = props; const { data } = props;
@@ -43,6 +44,7 @@ const OrderDetail: React.FC<{ data?: TradeOrderPageRespVO }> = (props) => {
<ServiceInfo <ServiceInfo
data={detais.tradeServeInfo} data={detais.tradeServeInfo}
orderCategoryId={detais?.orderCategoryId} orderCategoryId={detais?.orderCategoryId}
id={detais?.id}
/> />
)} )}
{detais?.tradeExtendServeInfo && ( {detais?.tradeExtendServeInfo && (

View File

@@ -73,10 +73,13 @@ export const renderBaseInfoOrder = (
> >
{data?.orderStatus && orderStatusObj.label} {data?.orderStatus && orderStatusObj.label}
</Title> </Title>
{data.orderStatus === 10 && (
<span> <span>
<Text type="secondary"></Text> <Text type="secondary"></Text>
<Text>430</Text> <Text>430</Text>
</span> </span>
)}
{data.orderStatus && {data.orderStatus &&
Number(data.orderStatus) === OrderStatus.Refunded && ( Number(data.orderStatus) === OrderStatus.Refunded && (
<Paragraph> <Paragraph>
@@ -121,18 +124,18 @@ export const renderBaseInfoOrder = (
{data.orderStatus && {data.orderStatus &&
Number(data.orderStatus) === OrderStatus.Cancelled && ( Number(data.orderStatus) === OrderStatus.Cancelled && (
<> <>
<Paragraph> <div>
<Text type="secondary"></Text> <Text type="secondary"></Text>
<Text></Text> <Text>{data.cancelTime}</Text>
</Paragraph> </div>
<Paragraph> <div>
<Text type="secondary"></Text> <Text type="secondary"></Text>
<Text>{data.cancelReason}</Text> <Text>{data.cancelReason}</Text>
</Paragraph> </div>
<Paragraph> <div>
<Text type="secondary"></Text> <Text type="secondary"></Text>
<Text>{data?.merchantRemark}</Text> <Text>{data?.cancelRemark}</Text>
</Paragraph> </div>
</> </>
)} )}
{data.orderStatus && {data.orderStatus &&

View File

@@ -1,14 +1,22 @@
import { ProCard } from '@ant-design/pro-components'; import { ProCard } from '@ant-design/pro-components';
import { Button, Card, Image, Space, Tag, Typography } from 'antd'; import { Button, Card, Image, Modal, Space, Tag, Typography } from 'antd';
import React, { useCallback } from 'react'; import React, { useCallback, useEffect, useState } from 'react';
import AdvancedImageGallery from '@/components/AdvancedImageGallery';
import DangerouslySetInnerHTML from '@/components/DangerouslySetInnerHTML';
import { fallback } from '@/constants/antd/image'; import { fallback } from '@/constants/antd/image';
import type { Item } from '@/services/trade/order/detail'; import { getfastPhoto } from '@/services/trade/order';
import type {
Item,
TradeOrderFastPhotoRespVo,
} from '@/services/trade/order/detail';
import type { ItemConfig } from '../../order-info'; import type { ItemConfig } from '../../order-info';
import styles from './index.module.less'; import styles from './index.module.less';
const { Text, Paragraph } = Typography; const { Text, Paragraph, Title } = Typography;
const ProdInfo: React.FC<ItemConfig<Item[]>> = (props) => { const ProdInfo: React.FC<ItemConfig<Item[]>> = (props) => {
const { data = [] } = props; const { data = [] } = props;
const [visible, setVisible] = useState(false);
const [item, setItem] = useState<Item>();
const renderTitle = useCallback((item: Item) => { const renderTitle = useCallback((item: Item) => {
return ( return (
<Space style={{ height: '100%' }} size={16}> <Space style={{ height: '100%' }} size={16}>
@@ -24,6 +32,14 @@ const ProdInfo: React.FC<ItemConfig<Item[]>> = (props) => {
</Space> </Space>
); );
}, []); }, []);
const onPhoto = useCallback(
async (item: Item) => {
setVisible(true);
setItem(item);
},
[item, visible],
);
return ( return (
<div className={styles['order-info']}> <div className={styles['order-info']}>
<Card title="商品信息"> <Card title="商品信息">
@@ -35,7 +51,11 @@ const ProdInfo: React.FC<ItemConfig<Item[]>> = (props) => {
bordered bordered
headerBordered headerBordered
gutter={8} gutter={8}
extra={<Button size="small"></Button>} extra={
<Button size="small" onClick={() => onPhoto(item)}>
</Button>
}
> >
<ProCard <ProCard
layout="default" layout="default"
@@ -74,14 +94,12 @@ const ProdInfo: React.FC<ItemConfig<Item[]>> = (props) => {
> >
<Text type="secondary"></Text> <Text type="secondary"></Text>
<Text> <Text>
{' '}
¥{item.handedPrice} {item.unit} ¥{item.handedPrice} {item.unit}
</Text> </Text>
</Paragraph> </Paragraph>
<Paragraph className="order-paragraph"> <Paragraph className="order-paragraph">
<Text type="secondary"></Text> <Text type="secondary"></Text>
<Text> <Text>
{' '}
¥{item.expensePrice} {item.unit}/- ¥{item.expensePrice} {item.unit}/-
</Text> </Text>
</Paragraph> </Paragraph>
@@ -141,8 +159,94 @@ const ProdInfo: React.FC<ItemConfig<Item[]>> = (props) => {
</ProCard> </ProCard>
))} ))}
</Card> </Card>
<PhotoModal
open={visible}
onCancel={() => setVisible(false)}
item={item}
/>
</div> </div>
); );
}; };
// 交易快照
const PhotoModal: React.FC<{
open: boolean;
onCancel: () => void;
item?: Item;
}> = (props) => {
const [photo, setPhoto] = useState<TradeOrderFastPhotoRespVo>();
const { item } = props;
console.log(item, 'item');
const onPhoto = useCallback(async () => {
const res = await getfastPhoto({
itemId: item?.id as number,
spuId: item?.spuId as number,
});
setPhoto(res);
}, [item]);
useEffect(() => {
if (props.open && item) {
onPhoto();
}
}, [props.open, item]);
return (
<Modal
title="交易快照"
open={props.open}
onCancel={props.onCancel}
footer={null}
width={820}
>
<div
style={{
height: '480px',
width: '100%',
overflow: 'hidden',
display: 'flex',
justifyContent: 'space-between',
gap: '16px',
}}
>
<div className="left" style={{ width: '386px', height: '100%' }}>
<AdvancedImageGallery images={photo?.imgs?.split(',') || []} />
</div>
<div
className="right"
style={{ height: '100%', overflow: 'auto', width: '386px' }}
>
<Title level={4}>{photo?.spuName}</Title>
<Paragraph>
<Text type="secondary">{photo?.brief}</Text>
</Paragraph>
<div
style={{
marginBottom: '16px',
background: '#00000005',
padding: '8px',
borderRadius: '8px',
fontSize: '12px',
}}
>
<span>
</span>
</div>
<div
style={{
marginBottom: '16px',
background: '#00000005',
padding: '8px',
borderRadius: '8px',
}}
>
<strong>{photo?.skuName}SKU规格</strong>
</div>
<DangerouslySetInnerHTML content={photo?.content} />
</div>
</div>
</Modal>
);
};
export default React.memo(ProdInfo); export default React.memo(ProdInfo);

View File

@@ -4,8 +4,8 @@ import type { ItemConfig } from '../../order-info';
import ServicePetUI from './uis/pets/service'; import ServicePetUI from './uis/pets/service';
const ServiceInfo: React.FC<ItemConfig<TradeServeInfo>> = (props) => { const ServiceInfo: React.FC<ItemConfig<TradeServeInfo>> = (props) => {
const { data = {}, orderCategoryId } = props; const { data = {}, orderCategoryId, id } = props;
return <>{orderCategoryId === 1 && <ServicePetUI data={data} />}</>; //宠物服务ui return <>{orderCategoryId === 1 && <ServicePetUI data={data} id={id} />}</>; //宠物服务ui
}; };
export default React.memo(ServiceInfo); export default React.memo(ServiceInfo);

View File

@@ -77,15 +77,23 @@ export const baseOrderColumns: ProColumns<TradeExtendServeInfo>[] = [
/> />
<div> <div>
<div>{serve.serveName}</div> <div>{serve.serveName}</div>
<div>
<Text type="secondary">{serve.serveDesc}</Text> <Text type="secondary">{serve.serveDesc}</Text>
</div>
<Space>
<div> <div>
<Text type="secondary"></Text> <Text type="secondary"></Text>
<Text>{serve.count}</Text> <Text>{serve.count}</Text>
</div>
<div>
<Text type="secondary"></Text> <Text type="secondary"></Text>
<Text>{serve.price}</Text> <Text>{serve.price}</Text>
</div>
<div>
<Text type="secondary"></Text> <Text type="secondary"></Text>
<Text>{serve.handPrice}</Text> <Text>{serve.handPrice}</Text>
</div> </div>
</Space>
</div> </div>
</Space> </Space>
))} ))}

View File

@@ -1,16 +1,22 @@
import { ClockCircleOutlined, EnvironmentOutlined } from '@ant-design/icons'; import { ClockCircleOutlined, EnvironmentOutlined } from '@ant-design/icons';
import { ProCard } from '@ant-design/pro-components'; import { ProCard, type ProColumns } from '@ant-design/pro-components';
import { Button, Card, Image, Space, Timeline, Typography } from 'antd'; import { Button, Card, Image, Modal, Space, Timeline, Typography } from 'antd';
import React from 'react'; import React, { useCallback, useEffect, useState } from 'react';
import EnhancedProTable from '@/components/EnhancedProTable';
import { fallback } from '@/constants/antd/image'; import { fallback } from '@/constants/antd/image';
import type { TradeServeInfo } from '@/services/trade/order/detail'; import { getSubTimeLog } from '@/services/trade/order';
import type {
TradeOrderSubLogDO,
TradeServeInfo,
} from '@/services/trade/order/detail';
import type { ItemConfig } from '../../../../order-info'; import type { ItemConfig } from '../../../../order-info';
import styles from '../../index.module.less'; import styles from '../../index.module.less';
const { Text, Paragraph } = Typography; const { Text, Paragraph } = Typography;
const ServicePetUI: React.FC<ItemConfig<TradeServeInfo>> = (props) => { const ServicePetUI: React.FC<ItemConfig<TradeServeInfo>> = (props) => {
const { data = {} } = props; const { data = {}, id } = props;
const { boneInfo, subInfo } = data; const { boneInfo, subInfo } = data;
const [open, setOpen] = useState<boolean>(false);
return ( return (
<div className={styles['order-info']}> <div className={styles['order-info']}>
<Card title="服务信息"> <Card title="服务信息">
@@ -54,7 +60,11 @@ const ServicePetUI: React.FC<ItemConfig<TradeServeInfo>> = (props) => {
size="small" size="small"
title="预约信息" title="预约信息"
headerBordered headerBordered
extra={<Button size="small"></Button>} extra={
<Button size="small" onClick={() => setOpen(true)}>
</Button>
}
> >
<Space size={100}> <Space size={100}>
<div> <div>
@@ -99,8 +109,60 @@ const ServicePetUI: React.FC<ItemConfig<TradeServeInfo>> = (props) => {
</ProCard> </ProCard>
</ProCard> </ProCard>
</Card> </Card>
<SubTimeLogModal id={id} open={open} onCancel={() => setOpen(false)} />
</div> </div>
); );
}; };
//修改记录
const SubTimeLogModal: React.FC<{
id?: number;
open?: boolean;
onCancel: () => void;
}> = (props) => {
const { id, open = false } = props;
const [log, setLog] = useState<TradeOrderSubLogDO[]>([]);
const fetchSubTimeLog = useCallback(
async (id: number) => {
const res = await getSubTimeLog(id);
console.log(res);
setLog(res);
},
[id],
);
const columns: ProColumns<TradeOrderSubLogDO>[] = [
{
title: '预约时间',
dataIndex: 'subTime',
valueType: 'dateTime',
},
{
title: '修改时间',
dataIndex: 'updateTime',
valueType: 'dateTime',
},
];
useEffect(() => {
if (open && id) fetchSubTimeLog(id);
}, [open, id]);
return (
<Modal
width={'60vw'}
open={open}
onCancel={props.onCancel}
title="修改记录"
footer={null}
>
<EnhancedProTable<TradeOrderSubLogDO>
columns={columns}
dataSource={log}
showIndex={false}
search={false}
scroll={{ x: 'max-content' }}
showSelection={false}
pagination={false}
/>
</Modal>
);
};
export default React.memo(ServicePetUI); export default React.memo(ServicePetUI);

View File

@@ -20,7 +20,7 @@ const OrderList: React.FC = () => {
return ( return (
<div className={`${styles['trade-order']} "page-container" `}> <div className={`${styles['trade-order']} "page-container" `}>
<Tabs defaultActiveKey="1" items={items} /> <Tabs defaultActiveKey="1" items={items} destroyOnHidden />
</div> </div>
); );
}; };

View File

@@ -12,9 +12,8 @@ import {
message, message,
Space, Space,
Statistic, Statistic,
Typography,
} from 'antd'; } from 'antd';
import React, { useCallback, useRef, useState } from 'react'; import React, { useCallback, useEffect, useRef, useState } from 'react';
import ConfigurableDrawerForm, { import ConfigurableDrawerForm, {
type ConfigurableDrawerFormRef, type ConfigurableDrawerFormRef,
} from '@/components/DrawerForm'; } from '@/components/DrawerForm';
@@ -29,14 +28,18 @@ import PopconfirmForm, {
} from '@/components/PopconfirmForm'; } from '@/components/PopconfirmForm';
import { import {
mapOrderStatusToBadgeStatus, mapOrderStatusToBadgeStatus,
type OrderStatus, OrderStatus,
OrderStatusLabels, OrderStatusLabels,
} from '@/constants/trade'; } from '@/constants/trade';
import { import {
getTradeOrderPage, getTradeOrderPage,
getTradeSummary,
type TradeOrderPageRespVO, type TradeOrderPageRespVO,
type TradeReq, type TradeReq,
type TradeSummaryRespVO,
updateOrderRemark,
} from '@/services/trade/order'; } from '@/services/trade/order';
import CancleOrderModal from './components/cancleOrderModal';
import DetailCom from './detail'; import DetailCom from './detail';
const OrderListItem: React.FC<{ orderStatus: number }> = (props) => { const OrderListItem: React.FC<{ orderStatus: number }> = (props) => {
@@ -46,6 +49,16 @@ const OrderListItem: React.FC<{ orderStatus: number }> = (props) => {
const [modalData, setModalData] = useState<TradeOrderPageRespVO>(); const [modalData, setModalData] = useState<TradeOrderPageRespVO>();
const [isShowTotal, setIsShowTotal] = useState<boolean>(false); const [isShowTotal, setIsShowTotal] = useState<boolean>(false);
const popconfirmFormRef = useRef<PopconfirmFormRef>(null); const popconfirmFormRef = useRef<PopconfirmFormRef>(null);
const [summary, setSummary] = useState<TradeSummaryRespVO>();
const [cancleOrderVisible, setCancleOrderVisible] = useState<boolean>(false);
const fetchSummary = async () => {
const res = await getTradeSummary();
setSummary(res);
};
useEffect(() => {
fetchSummary();
}, []);
const onFetch = async ( const onFetch = async (
params: TradeReq & { params: TradeReq & {
pageSize: number; pageSize: number;
@@ -74,14 +87,24 @@ const OrderListItem: React.FC<{ orderStatus: number }> = (props) => {
[modalData], [modalData],
); );
const handleOrder = useCallback((id?: number) => { const handleOrder = useCallback(
console.log(id, '取消订单'); (type: string, record?: TradeOrderPageRespVO) => {
setModalData(record);
if (type === 'order') {
setCancleOrderVisible(true);
}
if (type === 'order') {
setCancleOrderVisible(true);
}
// await updateTradeOrder(values.id); // await updateTradeOrder(values.id);
}, []); },
[],
);
const handleUpdate = async (_values: TradeOrderPageRespVO) => { const handleUpdate = async (values: TradeOrderPageRespVO, id?: number) => {
try { try {
// await updateTradeOrder(values.id); await updateOrderRemark({ remark: values.userRemark || '', id: id });
tableRef.current?.reload();
return true; return true;
} finally { } finally {
message.success('更新成功'); message.success('更新成功');
@@ -105,29 +128,47 @@ const OrderListItem: React.FC<{ orderStatus: number }> = (props) => {
}} }}
key={record.id} key={record.id}
> >
<a key="cancel" onClick={() => handleOrder(record.id)}> {record.orderStatus !== OrderStatus.Cancelled && (
<a key="cancel" onClick={() => handleOrder('order', record)}>
</a> </a>
<a key="order" onClick={() => handleOrder(record.id)}> )}
{record.orderStatus === OrderStatus.PendingService && (
<a key="order" onClick={() => handleOrder('service', record)}>
</a> </a>
<a key="sale" onClick={() => handleOrder(record.id)}> )}
{record.orderStatus === OrderStatus.PendingConfirmation && (
<a key="order" onClick={() => handleOrder('service', record)}>
</a>
)}
{(record.orderStatus as number) > OrderStatus.PendingPayment &&
record.orderStatus !== OrderStatus.Cancelled && (
<a key="sale" onClick={() => handleOrder('sales', record)}>
</a> </a>
)}
{record.orderStatus === OrderStatus.PendingAcceptance && (
<a key="sale" onClick={() => handleOrder('sales', record)}>
</a>
)}
<a key="detail" onClick={() => handleDetail(record)}> <a key="detail" onClick={() => handleDetail(record)}>
</a> </a>
{record.orderStatus === OrderStatus.PendingPayment && (
<PopconfirmForm <PopconfirmForm
ref={popconfirmFormRef} ref={popconfirmFormRef}
columns={[ columns={[
{ {
title: '备注', title: '备注',
name: 'userRemark', name: 'merchantRemark',
valueType: 'textarea', valueType: 'textarea',
fieldProps: { rows: 4, autoFocus: false }, fieldProps: { rows: 4, autoFocus: false },
}, },
]} ]}
onSubmit={handleUpdate} onSubmit={(value) => handleUpdate(value, record.id)}
> >
<a <a
key="remark" key="remark"
@@ -136,6 +177,7 @@ const OrderListItem: React.FC<{ orderStatus: number }> = (props) => {
</a> </a>
</PopconfirmForm> </PopconfirmForm>
)}
</div>, </div>,
], ],
}; };
@@ -251,13 +293,25 @@ const OrderListItem: React.FC<{ orderStatus: number }> = (props) => {
{isShowTotal && ( {isShowTotal && (
<ProCard.Group direction="row" style={{ marginBottom: 18 }}> <ProCard.Group direction="row" style={{ marginBottom: 18 }}>
<ProCard layout="center" style={{ background: '#f5f5f5' }}> <ProCard layout="center" style={{ background: '#f5f5f5' }}>
<Statistic title="订单数量" value={79.0} precision={2} /> <Statistic
title="订单数量"
value={summary?.orderCount}
precision={2}
/>
</ProCard> </ProCard>
<ProCard layout="center" style={{ background: '#f5f5f5' }}> <ProCard layout="center" style={{ background: '#f5f5f5' }}>
<Statistic title="实付金额" value={112893.0} precision={2} /> <Statistic
title="实付金额"
value={summary?.payPrice}
precision={2}
/>
</ProCard> </ProCard>
<ProCard layout="center" style={{ background: '#f5f5f5' }}> <ProCard layout="center" style={{ background: '#f5f5f5' }}>
<Statistic title="实收金额" value={93} suffix="/ 100" /> <Statistic
title="实收金额"
value={summary?.livePrice}
precision={2}
/>
</ProCard> </ProCard>
</ProCard.Group> </ProCard.Group>
)} )}
@@ -286,8 +340,13 @@ const OrderListItem: React.FC<{ orderStatus: number }> = (props) => {
> >
<DetailCom data={modalData} /> <DetailCom data={modalData} />
</ConfigurableDrawerForm> </ConfigurableDrawerForm>
<CancleOrderModal
open={cancleOrderVisible}
onClose={() => setCancleOrderVisible(false)}
id={modalData?.id}
/>
</> </>
); );
}; };
export default OrderListItem; export default React.memo(OrderListItem);

View File

@@ -0,0 +1,6 @@
// 售后列表
const TradeSales: React.FC = () => {
return 111;
};
export default TradeSales;

View File

@@ -65,6 +65,7 @@ export interface TradeExtendServeInfo {
} }
export interface TradeOrderDetailRespVO { export interface TradeOrderDetailRespVO {
cancelRemark?: string;
/** /**
* 取消原因 * 取消原因
*/ */
@@ -346,3 +347,73 @@ export interface TradeOrderStatusRespVo {
*/ */
orderId?: number; orderId?: number;
} }
export interface TradeOrderFastPhotoRespVo {
/**
* 商品概述
*/
brief?: string;
/**
* 商品详细描述
*/
content?: string;
/**
* 产品轮播图
*/
imgs?: string;
/**
* 产品主图
*/
pic?: string;
/**
* sku
*/
skuName?: string;
/**
* 商品名称
*/
spuName?: string;
}
export interface TradeOrderSubLogDO {
/**
* 创建时间
*/
createTime?: string;
/**
* 创建者,目前使用 SysUser 的 id 编号
*
* 使用 String 类型的原因是,未来可能会存在非数值的情况,留好拓展性。
*/
creator?: string;
/**
* 是否删除
*/
deleted?: number;
/**
* 编号
*/
id?: number;
/**
* 订单号
*/
orderId?: number;
/**
* 预约时间
*/
subTime?: string;
/**
* 更新者,目前使用 SysUser 的 id 编号
*
* 使用 String 类型的原因是,未来可能会存在非数值的情况,留好拓展性。
*/
updater?: string;
/**
* 最后更新时间
*/
updateTime?: string;
/**
* 用户编号
*/
userId?: number;
}

View File

@@ -1,6 +1,10 @@
import { request } from "@umijs/max"; import { request } from "@umijs/max";
import { TradeOrderPageRespVO, TradeReq } from "./list"; import { TradeOrderPageRespVO, TradeReq, TradeSummaryRespVO } from "./list";
import { TradeOrderDetailRespVO } from "./detail"; import {
TradeOrderDetailRespVO,
TradeOrderFastPhotoRespVo,
TradeOrderSubLogDO,
} from "./detail";
export const getTradeOrderPage = async (params: TradeReq) => { export const getTradeOrderPage = async (params: TradeReq) => {
return request<PageResult<TradeOrderPageRespVO[]>>("/trade/order/page", { return request<PageResult<TradeOrderPageRespVO[]>>("/trade/order/page", {
@@ -16,4 +20,76 @@ export const getTradeOrderDetail = async (id: number) => {
}); });
}; };
export { TradeOrderPageRespVO, TradeOrderDetailRespVO, TradeReq }; //获得交易订单统计
export const getTradeSummary = async () => {
return request<TradeSummaryRespVO>("/trade/order/summary", {
method: "GET",
});
};
//交易快照
export const getfastPhoto = async (params: {
itemId: number;
spuId: number;
}) => {
return request<TradeOrderFastPhotoRespVo>("/trade/order/fastPhoto", {
method: "GET",
params,
});
};
//服务信息修改记录
export const getSubTimeLog = async (id: number) => {
return request<TradeOrderSubLogDO[]>(`/trade/order/subTimeLog/${id}`, {
method: "GET",
});
};
//取消订单
export const cancelOrder = async (data: {
id: number;
cancelReason: string;
cancelRemark: string;
}) => {
return request<boolean>(`/trade/order/cancel`, {
method: "PUT",
data,
});
};
//接单确认
export const acceptConfirm = async (id: number) => {
return request<boolean>(`/trade/order/acceptConfirm`, {
method: "PUT",
params: { id },
});
};
//服务上报
export const reportServe = async (id: number) => {
return request<boolean>(`/trade/order/reportServe`, {
method: "PUT",
params: { id },
});
};
//商家备注
export const updateOrderRemark = async (params: {
id?: number;
remark?: string;
}) => {
return request<boolean>(`/trade/order/update-remark`, {
method: "PUT",
data: params,
});
};
export {
TradeOrderPageRespVO,
TradeOrderDetailRespVO,
TradeReq,
TradeSummaryRespVO,
TradeOrderFastPhotoRespVo,
TradeOrderSubLogDO,
};

View File

@@ -192,3 +192,9 @@ export interface TradeReq {
*/ */
userSearch?: string; userSearch?: string;
} }
export interface TradeSummaryRespVO {
orderCount?: number;
livePrice?: number;
payPrice?: number;
}

View File

@@ -1,5 +1,25 @@
import * as IconPark from '@icon-park/react';
import { Spin } from 'antd'; import { Spin } from 'antd';
import React from 'react'; import React from 'react';
// 动态构造 iconMap
const iconMap: Record<string, React.ReactNode> = {};
// 获取所有图标名称并构建图标映射
const allIcons = Object.keys(IconPark);
allIcons.forEach((iconName) => {
// 排除不需要的属性(如默认导出等)
if (
iconName !== 'default' &&
typeof IconPark[iconName as keyof typeof IconPark] === 'function'
) {
const IconComponent = IconPark[
iconName as keyof typeof IconPark
] as React.ComponentType<any>;
iconMap[iconName] = <IconComponent theme="outline" size="16" />;
}
});
import type { MenuVO } from '@/services/system/menu'; import type { MenuVO } from '@/services/system/menu';
export const loopMenuItem = (menus: MenuVO[], pId: number | string): any[] => { export const loopMenuItem = (menus: MenuVO[], pId: number | string): any[] => {
@@ -17,13 +37,18 @@ export const loopMenuItem = (menus: MenuVO[], pId: number | string): any[] => {
const routeItem: any = { const routeItem: any = {
path: item.path, path: item.path,
name: item.name, name: item.name,
icon: '',
id: item.id, id: item.id,
icon: '',
parentId: pId, parentId: pId,
hideInMenu: !item.visible, hideInMenu: !item.visible,
children: [], children: [],
}; };
// 如果菜单项有图标且图标存在于图标映射中,则添加图标
if (item.icon && iconMap[item.icon]) {
routeItem.icon = iconMap[item.icon];
}
// 只有当 Component 存在时才添加 element 属性 // 只有当 Component 存在时才添加 element 属性
if (Component) { if (Component) {
routeItem.element = ( routeItem.element = (