34 Commits

Author SHA1 Message Date
a2c5711d43 feat: 样本标签增加枚举标签
All checks were successful
Auto Deploy / build-and-deploy (push) Successful in 36s
2026-03-05 17:03:12 +08:00
945f795f81 Merge branch 'wuxichen' of http://gitea.tashowz.com/tashow/tashow-manager into wuxichen
All checks were successful
Auto Deploy / build-and-deploy (push) Successful in 31s
2026-03-04 10:11:59 +08:00
bf7d0b3bc5 feat: 修改状态名称 2026-03-04 10:11:04 +08:00
cb7b22bca3 chore(docs): 删除 CLAUDE.md 文件并清理登录页面临时文本
All checks were successful
Auto Deploy / build-and-deploy (push) Successful in 33s
- 移除整个 CLAUDE.md 项目指导文件
- 清理登录页面中的临时调试文本 "111"
2026-03-04 09:57:06 +08:00
d5a795de77 feat: 更新登录页面和项目文档
All checks were successful
Auto Deploy / build-and-deploy (push) Successful in 32s
- 添加 CLAUDE.md 项目文档
- 添加自动部署脚本
- 修改登录页面"其他登录方式"文本

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 09:25:22 +08:00
6a3aa3bf5d chore(login): 更新登录页面其他登录方式文本
All checks were successful
Auto Deploy / build-and-deploy (push) Successful in 33s
- 将"其他登录方式"修改为"其他登录方式111111"
2026-03-02 17:52:06 +08:00
4ad3706dfa chore(login): 更新登录页面其他登录方式文本
All checks were successful
Auto Deploy / build-and-deploy (push) Successful in 38s
- 将"其他登录方式"修改为"其他登录方式111111"
2026-03-02 17:44:23 +08:00
44426d8812 chore(ci): 更新部署工作流注释
All checks were successful
Auto Deploy / build-and-deploy (push) Successful in 33s
- 移除 pnpm 缓存相关注释内容
2026-03-02 17:40:20 +08:00
c970931954 chore(login): 更新登录页面其他登录方式文本
All checks were successful
Auto Deploy / build-and-deploy (push) Successful in 32s
- 将"其他登录方式"修改为"其他登录方式1111"
2026-03-02 17:35:31 +08:00
ba1a7f392b chore(login): 更新登录页面其他登录方式文本
All checks were successful
Auto Deploy / build-and-deploy (push) Successful in 35s
- 将"其他登录方式"修改为"其他登录方式1111"
2026-03-02 17:33:26 +08:00
16bb5afaa7 chore(login): 更新登录页面其他登录方式文本
All checks were successful
Auto Deploy / build-and-deploy (push) Successful in 34s
- 将"其他登录方式"修改为"其他登录方式1111"
2026-03-02 17:32:10 +08:00
22ef893529 chore(login): 更新登录页面其他登录方式文本
All checks were successful
Auto Deploy / build-and-deploy (push) Successful in 2m7s
- 将"其他登录方式"修改为"其他登录方式1111"
2026-03-02 17:28:50 +08:00
066cee46b0 chore(login): 更新登录页面其他登录方式文本
Some checks failed
Auto Deploy / build-and-deploy (push) Has been cancelled
- 将"其他登录方式"修改为"其他登录方式1111"
2026-03-02 17:27:58 +08:00
b4017597f9 fix: 移除 frozen-lockfile 标志以兼容服务器环境 2026-03-02 17:27:36 +08:00
f27fc56fd4 chore(login): 更新登录页面其他登录方式文本
Some checks failed
Auto Deploy / build-and-deploy (push) Failing after 1s
- 将"其他登录方式"修改为"其他登录方式1111"
2026-03-02 17:22:51 +08:00
47623d6b1e perf: 优化 CI/CD 流程,使用持久化目录和缓存加速部署
Some checks failed
Auto Deploy / build-and-deploy (push) Failing after 6s
2026-03-02 17:21:08 +08:00
7426746239 chore(login): 更新登录页面其他登录方式文本
All checks were successful
Auto Deploy / build-and-deploy (push) Successful in 2m19s
- 将"其他登录方式"修改为"其他登录方式1111"
2026-03-02 17:12:56 +08:00
ecbd4eefd7 chore(login): 更新登录页面其他登录方式文本
All checks were successful
Auto Deploy / build-and-deploy (push) Successful in 1m52s
- 将"其他登录方式"修改为"其他登录方式1111"
2026-03-02 17:08:43 +08:00
448531f9d3 test: trigger CI/CD after installing Node.js and pnpm
All checks were successful
Auto Deploy / build-and-deploy (push) Successful in 4m25s
2026-03-02 16:55:07 +08:00
4facb02777 test: trigger CI/CD with self-hosted runner
Some checks failed
Auto Deploy / build-and-deploy (push) Failing after 6s
2026-03-02 16:47:33 +08:00
929cb1966b fix: use self-hosted runner for CI/CD
Some checks failed
Auto Deploy / build-and-deploy (push) Failing after 6s
2026-03-02 16:39:21 +08:00
aa2f07de41 test: add test file for CI/CD verification
Some checks failed
Auto Deploy / build-and-deploy (push) Failing after 11m8s
2026-03-02 16:23:28 +08:00
5c8cb3bc88 feat: update workflow for self-hosted runner
Some checks failed
Auto Deploy / build-and-deploy (push) Failing after 13m48s
2026-03-02 16:08:31 +08:00
bbdcef9d56 Merge remote-tracking branch 'origin/wuxichen' into wuxichen
Some checks failed
Auto Deploy / build-and-deploy (push) Has been cancelled
2026-03-02 15:55:19 +08:00
c4f3235fcf feat: add CI/CD auto deploy workflow 2026-03-02 15:53:48 +08:00
f842dcaf14 fix: 样本管理样式更改 2026-03-02 14:55:24 +08:00
95338a3ddf feat: 标签管理增加分组 2026-03-02 13:39:34 +08:00
4418a95822 feat: 文件下载 2026-02-28 17:26:13 +08:00
62abb284a6 feat: 模型管理 2026-02-27 14:23:53 +08:00
912ab4c321 fix: 样本管理 2026-02-27 10:34:49 +08:00
a5d3342d93 feat: 路由登录 动态路由重定向 2026-01-26 17:59:52 +08:00
502c236b0d feat: new page 2026-01-21 15:07:11 +08:00
607b292f36 feat: 售后列表文件 2025-11-19 10:05:22 +08:00
a15bda739a feat: 取消订单 2025-10-31 16:04:17 +08:00
71 changed files with 4341 additions and 642 deletions

View File

@@ -0,0 +1,46 @@
name: Auto Deploy
on:
push:
branches:
- wuxichen # 当前分支
- main
- master
jobs:
build-and-deploy:
runs-on: self-hosted
steps:
- name: Build and Deploy
run: |
# 使用持久化工作目录,避免每次重新克隆
WORK_DIR="/root/tashow-deploy"
# 首次运行时克隆代码
if [ ! -d "$WORK_DIR" ]; then
echo "首次部署,克隆代码..."
git clone http://gitea.tashowz.com/tashow/tashow-manager.git $WORK_DIR
fi
cd $WORK_DIR
# 更新代码到最新版本
echo "更新代码..."
git fetch origin
git checkout ${{ github.ref_name }}
git reset --hard origin/${{ github.ref_name }}
# 安装依赖
echo "安装依赖..."
pnpm install
# 构建项目
echo "构建项目..."
pnpm build
# 部署到服务器
echo "部署到服务器..."
rsync -av --delete dist/ /home/1panel/www/sites/admin.petshy.tashowz.com/index/
echo "部署成功!"

View File

@@ -19,8 +19,7 @@ const Settings: ProLayoutProps & {
pwa: true,
logo: '/logo.svg',
iconfontUrl: '',
splitMenus: true,
// splitMenus: true,
token: {
// 参见ts声明demo 见文档通过token 修改样式
// 设置内容区域的边距

View File

@@ -15,8 +15,9 @@ export default {
// localhost:8000/api/** -> https://preview.pro.ant.design/api/**
'/admin-api/': {
// http://192.168.1.231:48080 伟强
// http://192.168.1.89:48086 子杰
// https://petshy.tashowz.com/
target: 'https://petshy.tashowz.com',
target: 'http://192.168.1.89:48080',
changeOrigin: true,
},
},

View File

@@ -46,17 +46,18 @@ export default [
// ],
// },
{
path: '/ai1',
name: 'AI1',
routes: [
{
name: 'ai样本',
path: '/ai1/tag',
component: './ai/sample-tag',
},
],
},
// {
// path: "/ai1",
// name: "AI1",
// // redirect: "tag",
// routes: [
// {
// name: "ai样本",
// path: "tag",
// component: "./prod/list/components/service-rule/index",
// },
// ],
// },
// {
// path: "/system",
// name: "系统管理",

View File

@@ -48,6 +48,7 @@
"browser-id3-writer": "^6.3.1",
"classnames": "^2.5.1",
"dayjs": "^1.11.13",
"dompurify": "^3.3.0",
"jsencrypt": "^3.5.4",
"rc-resize-observer": "^1.4.3",
"react": "^19.1.0",

16
pnpm-lock.yaml generated
View File

@@ -44,6 +44,9 @@ importers:
dayjs:
specifier: ^1.11.13
version: 1.11.18
dompurify:
specifier: ^3.3.0
version: 3.3.0
jsencrypt:
specifier: ^3.5.4
version: 3.5.4
@@ -2818,6 +2821,9 @@ packages:
'@types/tough-cookie@4.0.5':
resolution: {integrity: sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==}
'@types/trusted-types@2.0.7':
resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==}
'@types/unist@2.0.11':
resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==}
@@ -4807,6 +4813,9 @@ packages:
resolution: {integrity: sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==}
engines: {node: '>= 4'}
dompurify@3.3.0:
resolution: {integrity: sha512-r+f6MYR1gGN1eJv0TVQbhA7if/U7P87cdPl3HN5rikqaBSBxLiCb/b9O+2eG0cxz0ghyU+mU1QkbsOwERMYlWQ==}
domutils@1.7.0:
resolution: {integrity: sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==}
@@ -15041,6 +15050,9 @@ snapshots:
'@types/tough-cookie@4.0.5': {}
'@types/trusted-types@2.0.7':
optional: true
'@types/unist@2.0.11': {}
'@types/use-sync-external-store@0.0.3': {}
@@ -17939,6 +17951,10 @@ snapshots:
dependencies:
domelementtype: 2.3.0
dompurify@3.3.0:
optionalDependencies:
'@types/trusted-types': 2.0.7
domutils@1.7.0:
dependencies:
dom-serializer: 0.2.2

2
public/test.txt Normal file
View File

@@ -0,0 +1,2 @@
CI/CD Test - Deployed at 2026-03-02 16:23
This file is used to verify automatic deployment is working.

View File

@@ -45,6 +45,7 @@ export async function getInitialState(): Promise<{
throw new Error('No token found');
}
const data = await getInfo();
console.log(data, 'data');
wsCache.set(CACHE_KEY.USER, data);
wsCache.set(CACHE_KEY.ROLE_ROUTERS, data.menus);
@@ -73,7 +74,6 @@ export async function getInitialState(): Promise<{
await fetchUserInfo();
}
const menus = wsCache.get(CACHE_KEY.ROLE_ROUTERS);
console.log(111);
return {
fetchUserInfo,
currentUser,
@@ -98,9 +98,9 @@ export const layout: RunTimeLayoutConfig = ({
<Question key="doc" />,
<SelectLang key="SelectLang" />,
],
menu: {
locale: false,
// 关闭国际化-
},
avatarProps: {
src: initialState?.currentUser?.user.avatar,
@@ -112,7 +112,7 @@ export const layout: RunTimeLayoutConfig = ({
waterMarkProps: {
content: initialState?.currentUser?.user.nickname,
},
footerRender: () => <Footer />,
// footerRender: () => <Footer />,
onPageChange: () => {
const { location } = history;
// 如果没有登录,重定向到 login
@@ -146,8 +146,8 @@ export const layout: RunTimeLayoutConfig = ({
useRoutes: true,
},
menuHeaderRender: undefined,
// 自定义 403 页面
unAccessible: <div>unAccessible</div>,
// // 自定义 403 页面
// unAccessible: <div>unAccessible</div>,
// 增加一个 loading 的状态
childrenRender: (children) => {
// if (initialState?.loading) return <PageLoading />;
@@ -238,7 +238,7 @@ export const request: RequestConfig = {
// Umi 4 支持异步的 patchClientRoutes
export async function patchClientRoutes({ routes }: any) {
const { wsCache } = useCache();
console.log('patchClientRoutes', patchClientRoutes);
// 先尝试从缓存获取
let menus = wsCache.get(CACHE_KEY.ROLE_ROUTERS);

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

@@ -0,0 +1,60 @@
import {
ActionType,
EditableFormInstance,
EditableProTable,
type EditableProTableProps,
type ParamsType,
ProColumns,
} from '@ant-design/pro-components';
import { Form, Table } from 'antd';
import React, { forwardRef, useEffect, useRef, useState } from 'react';
interface EditableProTableComProps<
T extends Record<string, any>,
U extends ParamsType = ParamsType,
> extends Omit<
EditableProTableProps<T, U>,
'editableFormRef' | 'recordCreatorProps'
> {
editableFormRef?: React.Ref<any>;
recordCreatorProps?: any;
}
const EditableProTableCom = forwardRef(
<T extends Record<string, any>>(
props: EditableProTableComProps<T>,
ref: React.Ref<any>,
) => {
// 提取不应该直接传递给 EditableProTable 的属性
const { value, columns, ...restProps } = props;
return (
<EditableProTable
value={value}
columns={columns}
actionRef={ref}
recordCreatorProps={false}
{...restProps}
/>
);
},
);
// const EditableProTableCom = <T extends Record<string, any>>(
// props: EditableProTableComProps<T>
// ) => {
// // 提取不应该直接传递给 EditableProTable 的属性
// const { value, columns, ...restProps } = props;
// return (
// <EditableProTable
// value={value}
// columns={columns}
// recordCreatorProps={false}
// {...restProps}
// />
// );
// };
export default EditableProTableCom as <T extends Record<string, any>>(
props: EditableProTableComProps<T> & { ref?: React.Ref<any> },
) => React.ReactElement;

View File

@@ -22,6 +22,7 @@ function EnhancedProTable<T extends BaseRecord, U extends ParamsType = any>(
toolbarActions,
rowKey = 'id',
pagination = true,
scroll,
...restProps
} = props;
@@ -34,7 +35,6 @@ function EnhancedProTable<T extends BaseRecord, U extends ParamsType = any>(
type={action.type}
danger={action.danger}
disabled={action.disabled}
// icon={action.icon ?? <PlusOutlined />}
onClick={() => action.onClick()}
>
{action.label}
@@ -54,8 +54,7 @@ function EnhancedProTable<T extends BaseRecord, U extends ParamsType = any>(
toolBarRender={toolBarRender}
manualRequest={false}
showSorterTooltip
// scroll={{ x: "max-content" }}
scroll={{ x: 1200 }}
scroll={scroll ? scroll : { x: 1200 }}
components={components}
search={
search
@@ -82,7 +81,8 @@ function EnhancedProTable<T extends BaseRecord, U extends ParamsType = any>(
? {
showSizeChanger: true,
showQuickJumper: true,
pageSize: 10,
// pageSize: 10,
defaultPageSize: 10,
showTotal: formatPaginationTotal,
}
: false

View File

@@ -32,10 +32,12 @@ const GroupTagCore: React.FC<GroupTagCoreProps> = (props) => {
const [type, setType] = useState<'group' | 'tag'>('group');
const [visible, setVisible] = useState<boolean>(false);
const [name, setName] = useState<string>('');
const [tagName, setTagName] = useState<string>('');
const [total, setTotal] = useState<number>(0);
const [currentId, setCurrentId] = useState<number>();
const [tagsModalValue, setTagsModalValue] = useState<{
tagName?: string;
groupName?: string;
groupIds?: number[];
id?: number;
}>({});
@@ -55,7 +57,11 @@ const GroupTagCore: React.FC<GroupTagCoreProps> = (props) => {
const fetchTagsApi = useCallback(async () => {
try {
setLoadingTags(true);
const res = await tagsApi.get({ groupId: currentId, pageNo: 1 });
const res = await tagsApi.get({
groupId: currentId,
pageNo: 1,
tagName: tagName,
});
const newGroup = { ...currentGroup, tags: res.list };
const newData = groups.map((g) => (g.id === currentId ? newGroup : g));
setGroups(newData as GroupItem[]);
@@ -86,7 +92,7 @@ const GroupTagCore: React.FC<GroupTagCoreProps> = (props) => {
useEffect(() => {
if (modalType === 'edit' && visible) {
if (type === 'group' && currentGroup) {
setName(currentGroup.groupName);
setName(currentGroup.groupName || '');
} else if (type === 'tag' && tagsModalValue) {
setName(tagsModalValue.tagName || '');
}
@@ -95,12 +101,16 @@ const GroupTagCore: React.FC<GroupTagCoreProps> = (props) => {
setName('');
}
}, [type, modalType, currentGroup, tagsModalValue, visible]);
const handleGroup = (type: 'add' | 'edit' | 'delete') => {
const handleGroup = (type: 'add' | 'edit' | 'delete', group?: GroupItem) => {
setType('group');
setModalType(type);
if (type === 'add' || type === 'edit') {
setVisible(true);
}
if (type === 'edit') {
console.log('group', group);
setTagsModalValue({ groupName: group?.groupName, id: group?.id });
}
};
const handleTag = (type: 'add' | 'edit' | 'delete', tag?: TagItem) => {
@@ -115,9 +125,8 @@ const GroupTagCore: React.FC<GroupTagCoreProps> = (props) => {
};
const handleAdd = async (value: {
groupName?: string;
groupIds?: number[];
tagName?: string;
groupName?: string;
}) => {
if (type === 'group') {
const id = await groupsApi.create({ groupName: value.groupName });
@@ -132,15 +141,18 @@ const GroupTagCore: React.FC<GroupTagCoreProps> = (props) => {
const handleEdit = async (value: {
tagName?: string;
groupName?: string;
groupIds?: number[];
}) => {
if (type === 'group') {
try {
setLoading(true);
await groupsApi.update({ id: currentGroup?.id, groupName: name });
await groupsApi.update({
id: currentGroup?.id,
groupName: value.groupName,
});
const newData = groups.map((item) => {
if (item.id === currentGroup?.id) {
const itemData = { ...item, groupName: name };
const itemData = { ...item, groupName: value.groupName };
setCurrentGroup(itemData);
return itemData;
} else {
@@ -148,8 +160,8 @@ const GroupTagCore: React.FC<GroupTagCoreProps> = (props) => {
}
});
setGroups(newData);
} finally {
setLoading(false);
} catch (error) {
message.success('修改失败');
}
} else {
await tagsApi.update({
@@ -157,19 +169,10 @@ const GroupTagCore: React.FC<GroupTagCoreProps> = (props) => {
id: tagsModalValue?.id,
...value,
});
message.success('修改成功');
const newTag = currentGroup?.tags?.map((tag) => {
if (tag.id === tagsModalValue?.id) {
return { ...tag, tagName: value.tagName };
} else {
return tag;
}
});
const newGroup = { ...currentGroup, tags: newTag };
setCurrentGroup(newGroup as GroupItem);
fetchTagsApi();
}
setVisible(false);
message.success('修改成功');
};
const handleDelete = async (type: 'group' | 'tag', id: number) => {
@@ -182,12 +185,13 @@ const GroupTagCore: React.FC<GroupTagCoreProps> = (props) => {
setCurrentGroup(newGroups[0]);
}
} else {
tagsApi.delete(id);
await tagsApi.delete(id);
fetchTagsApi();
}
};
const handleConfirm = useCallback(
async (value: { tagName?: string; groupIds?: number[] }) => {
async (value: { groupIds?: number[]; name?: string }) => {
if (modalType === 'add') {
await handleAdd(value);
}
@@ -201,6 +205,7 @@ const GroupTagCore: React.FC<GroupTagCoreProps> = (props) => {
const handleCancle = useCallback(() => {
setVisible(false);
setTagsModalValue({});
}, [visible]);
const onTagItemChange = (tag: TagItem, e: CheckboxChangeEvent) => {
@@ -229,6 +234,7 @@ const GroupTagCore: React.FC<GroupTagCoreProps> = (props) => {
const onSearchTags = async (e: React.KeyboardEvent<HTMLInputElement>) => {
const searchValue = (e.target as HTMLInputElement).value;
setTagName(searchValue);
const res = await tagsApi.get({
groupId: currentId,
pageNo: 1,
@@ -238,11 +244,15 @@ const GroupTagCore: React.FC<GroupTagCoreProps> = (props) => {
...currentGroup,
tags: res.list,
};
setTotal(res.total);
document.getElementById('scrollableDiv')?.scrollTo(0, 0);
setCurrentGroup(newGroup as GroupItem);
setPageNo(1);
};
const onSearchTagsClear = async () => {
setTagName('');
fetchTagsApi();
};
return (
<div className={`group-tag-core`}>
<div className="search-wrapper">
@@ -251,6 +261,7 @@ const GroupTagCore: React.FC<GroupTagCoreProps> = (props) => {
placeholder="搜索"
onPressEnter={onSearchTags}
allowClear
onClear={onSearchTagsClear}
disabled={isInEditMode}
/>
</div>
@@ -284,7 +295,7 @@ const GroupTagCore: React.FC<GroupTagCoreProps> = (props) => {
groups,
currentId,
onGroupClick,
onEdit: () => handleGroup('edit'),
onEdit: (groud) => handleGroup('edit', groud),
onDelete: (id) => handleDelete('group', id),
})}
</div>

View File

@@ -1,19 +1,23 @@
import { Form, Input, Modal, Select } from 'antd';
import { useEffect, useState } from 'react';
import { getGroupListByYagId } from '@/services/ai/sample';
interface TagsModalProps {
visible: boolean;
type: 'group' | 'tag';
modalType?: 'add' | 'edit' | 'delete';
onCancel: () => void;
groups: { id: number; groupName: string }[];
groups: { id?: number; groupName?: string }[];
value: {
id?: number;
tagName?: string;
groupIds?: number[];
groupName?: string;
};
onConfirm: (values: { tagName: string; groupIds?: number[] }) => void;
onConfirm: (
values: { groupIds?: number[]; name: string },
type: 'group' | 'tag',
) => void;
}
const TagsModal = (props: TagsModalProps) => {
const [form] = Form.useForm();
@@ -21,8 +25,19 @@ const TagsModal = (props: TagsModalProps) => {
const { value, visible, type, modalType, groups, onCancel, onConfirm } =
props;
const onGroupListByYagId = async (id: number) => {
const groupItem: { id?: number; groupName?: string }[] =
await getGroupListByYagId(id);
form.setFieldsValue({
groupIds: groupItem.map((item) => item.id),
});
};
useEffect(() => {
form.setFieldsValue(value);
console.log('value', value);
if (visible && type === 'tag' && value.id) {
onGroupListByYagId(value.id);
}
return () => {
form.resetFields();
setLoading(false);
@@ -32,7 +47,7 @@ const TagsModal = (props: TagsModalProps) => {
try {
setLoading(true);
const values = await form.validateFields();
await onConfirm(values);
await onConfirm(values, type);
} finally {
setLoading(false);
}
@@ -58,7 +73,7 @@ const TagsModal = (props: TagsModalProps) => {
wrapperCol={{ span: 20 }}
style={{ maxWidth: 600, marginTop: 30 }}
>
{type === 'tag' && modalType === 'add' && (
{type === 'tag' && (
<Form.Item
label="分组"
name="groupIds"

View File

@@ -19,7 +19,7 @@ interface GroupsProps {
total?: number;
loadMoreData?: () => void;
pageSize?: number;
onEdit?: () => void;
onEdit?: (group: GroupItem) => void;
onDelete?: (id: number) => void;
}
export const renderGroups = (data: GroupsProps) => {
@@ -67,7 +67,7 @@ export const renderGroups = (data: GroupsProps) => {
icon={<EditFilled />}
onClick={(e) => {
e.stopPropagation();
onEdit?.();
onEdit?.(item);
}}
/>
<Button
@@ -116,7 +116,7 @@ export const renderTags = (data: GroupTagProps) => {
<Empty description="暂无分组" image={Empty.PRESENTED_IMAGE_SIMPLE} />
);
}
console.log(list.length);
console.log(list, total);
return (
<InfiniteScroll
dataLength={list.length}

View File

@@ -8,7 +8,7 @@ export interface TagItem {
export interface GroupItem {
id: number;
groupName: string;
groupName?: string;
createTime?: string;
tags?: TagItem[];
}

View File

@@ -0,0 +1,36 @@
import { Modal, type ModalProps } from 'antd';
import React from 'react';
const CommonModal: React.FC<ModalProps> = (props) => {
const { children } = props;
return (
<Modal
{...props}
width={'70vw'}
styles={{
body: {
height: '65vh',
overflowY: 'auto',
padding: '0 16px',
minWidth: '100%',
},
content: {
background: '#f5f5f5',
padding: 0,
},
header: {
padding: '16px 24px',
marginBottom: 16,
},
footer: {
padding: '16px 24px',
background: '#ffff',
},
}}
>
{children}
</Modal>
);
};
export default React.memo(CommonModal);

View File

@@ -1,5 +1,6 @@
.uploader-card {
background-color: #fff;
padding: 10px;
:global {
.ant-upload-drag {
background-color: #fff;

View File

@@ -80,7 +80,7 @@ const AudioUploader: React.FC<AudioUploaderProps> = ({
onProgress?.({ percent: 10 });
// 调用后端接口
const result = await uploadToServer(file as File);
console.log(result, 'res');
// console.log(result, 'res');
onProgress?.({ percent: 100 });
if (result) {
// 构造返回数据
@@ -92,6 +92,7 @@ const AudioUploader: React.FC<AudioUploaderProps> = ({
response: result,
};
onSuccess?.(responseData);
onChange?.(responseData);
} else {
throw new Error(result.message || '上传失败');
}

View File

@@ -35,7 +35,15 @@ const UploadImages: React.FC<{
const [uploading, setUploading] = useState(false);
useEffect(() => {
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 {
setFileList([]);
}
@@ -63,7 +71,7 @@ const UploadImages: React.FC<{
const handleRemove = (file: UploadFile): boolean => {
const newFileList = fileList.filter((item) => item.uid !== file.uid);
const newUrl = newFileList.map((item) => item.url) as string[];
onChange?.(newUrl[0]);
onChange?.(newUrl.join(','));
return true;
};
@@ -101,22 +109,32 @@ const UploadImages: React.FC<{
const url = await uploadFile(file as File);
onProgress?.({ percent: 100 });
if (url) {
if (maxCount === 1) {
onChange?.(url);
onSuccess?.({ url });
} else {
const res = value?.split(',') || [];
console.log([...res, url].join(','));
onChange?.([...res, url].join(','));
}
message.success('上传成功');
} else {
setUploading(false);
throw new Error('上传失败');
}
} catch (error) {
console.error('Upload error:', error);
onError?.(error as Error);
setUploading(false);
message.error(`上传失败: ${(error as Error).message}`);
} finally {
console.log('finally');
setUploading(false);
}
};
return (
<Spin spinning={uploading}>
{uploading}
<>
<Upload
listType="picture-card"
fileList={fileList}
@@ -140,6 +158,7 @@ const UploadImages: React.FC<{
src={previewImage}
/>
)}
</>
</Spin>
);
};

View File

@@ -1,7 +1,7 @@
import type { BadgeProps } from 'antd';
export enum OrderStatus {
All = 1,
All = 0,
PendingPayment = 10,
PendingConfirmation = 20,
PendingService = 30,
@@ -53,3 +53,21 @@ export const mapOrderStatusToBadgeStatus = (
return statusMap[statusColor] || 'default';
};
export enum SalesStatus {
All = 0,
Apply = 10,
SellerAgree = 20,
BuyerDelivery = 30,
WaitRefund = 40,
Completed = 50,
}
export const SalesStatusLableMap: Record<SalesStatus, string> = {
[SalesStatus.All]: '所有售后单',
[SalesStatus.Apply]: '等待审核',
[SalesStatus.SellerAgree]: '审核通过',
[SalesStatus.BuyerDelivery]: '已完成',
[SalesStatus.WaitRefund]: '已取消',
[SalesStatus.Completed]: '已拒绝',
};

View File

@@ -75,7 +75,9 @@ ul,
ol {
list-style: none;
}
.ant-statistic {
text-align: center;
}
@media (max-width: 768px) {
.ant-table {
width: 100%;
@@ -93,6 +95,11 @@ ol {
}
}
.ant-table-row {
.ant-form-item {
margin-block: -16px !important;
}
}
.page-container {
background: #fff;
width: 100%;

View File

@@ -131,7 +131,7 @@ const Welcome: React.FC = () => {
umiAnt Design ProComponents
//
</p>
<div
{/* <div
style={{
display: 'flex',
flexWrap: 'wrap',
@@ -156,7 +156,7 @@ const Welcome: React.FC = () => {
href="https://procomponents.ant.design"
desc="ProComponents 是一个基于 Ant Design 做了更高抽象的模板组件,以 一个组件就是一个页面为开发理念,为中后台开发带来更好的体验。"
/>
</div>
</div> */}
</div>
</Card>
</PageContainer>

View File

@@ -0,0 +1,130 @@
import type {
ProColumns,
ProCoreActionType,
ProFormColumnsType,
} from '@ant-design/pro-components';
import { Modal, message, Switch } from 'antd';
import dayjs from 'dayjs';
import {
type AiModelRespVO,
updateModel,
updateModelStatus,
} from '@/services/ai/model';
export const baseDeptColumns: ProColumns<AiModelRespVO>[] = [
{
title: '模型名称',
dataIndex: 'modelName',
hideInSearch: true,
},
{
title: '版本号',
dataIndex: 'version',
hideInSearch: true,
},
{
title: '负载',
dataIndex: 'loadPercentage',
hideInSearch: true,
},
{
title: '状态',
dataIndex: 'status',
valueType: 'switch',
hideInSearch: true,
render: (
_,
record: AiModelRespVO,
_index: number,
action: ProCoreActionType | undefined,
) => (
<Switch
checked={record.status === 1}
checkedChildren="启用"
unCheckedChildren="禁用"
onChange={(checked) => {
Modal.confirm({
title: '确认操作',
content: `确认要"${checked ? '启用' : '禁用'}${
record.modelName
}"类目吗?`,
onOk: async () => {
console.log(checked);
await updateModel({
...record,
status: checked ? 1 : 0,
});
message.success('修改成功');
action?.reload();
},
});
}}
/>
),
},
{
title: '创建时间',
dataIndex: 'createTime',
valueType: 'dateRange',
hideInSearch: true,
render: (_, record: AiModelRespVO) =>
dayjs(record.createTime).format('YYYY-MM-DD HH:mm:ss'),
},
];
export const formColumns = (_type: string): ProFormColumnsType[] => [
{
title: '模型名称',
dataIndex: 'modelName',
formItemProps: {
rules: [
{
required: true,
message: '请输入模型名称',
},
],
},
},
{
title: '版本号',
dataIndex: 'version',
valueType: 'text',
formItemProps: {
rules: [
{
required: true,
message: '请输入部门名称',
},
],
},
},
{
title: '负载',
dataIndex: 'loadPercentage',
valueType: 'digit',
fieldProps: {
min: 0,
},
formItemProps: {
rules: [
{
required: true,
message: '请输入显示顺序',
},
],
},
},
{
title: '状态',
dataIndex: 'status',
valueType: 'switch',
fieldProps: {
checkedChildren: '禁用',
unCheckedChildren: '启用',
defaultChecked: true,
},
},
];

View File

@@ -0,0 +1,131 @@
import { PlusOutlined } from '@ant-design/icons';
import type { ActionType, ProColumns } from '@ant-design/pro-components';
import { message, Popconfirm } from 'antd';
import React, { useCallback, useRef, useState } from 'react';
import ConfigurableDrawerForm, {
type ConfigurableDrawerFormRef,
} from '@/components/DrawerForm';
import EnhancedProTable from '@/components/EnhancedProTable';
import type { ToolbarAction } from '@/components/EnhancedProTable/types';
import { formStatusType } from '@/constants';
import {
type AiModelRespVO,
createModel,
delModel,
getModelList,
updateModel,
updateModelStatus,
} from '@/services/ai/model';
import { baseDeptColumns, formColumns } from './config';
const ModelPage = () => {
const configurableDrawerRef = useRef<ConfigurableDrawerFormRef>(null);
const tableRef = useRef<ActionType>(null);
const [type, setType] = useState<'create' | 'update'>('create');
const [id, setId] = useState<number>(0);
const handleEdit = (record: AiModelRespVO) => {
setType('update');
setId(record.id as number);
configurableDrawerRef.current?.open(record);
};
const onFetch = async (params: { pageSize?: number; current?: number }) => {
const data = await getModelList({ ...params, pageNo: params.current });
return {
data: data.list,
success: true,
total: data.total,
};
};
const handleAdd = () => {
setType('create');
configurableDrawerRef.current?.open({ status: true });
};
const handleSubmit = useCallback(
async (values: AiModelRespVO) => {
if (type === 'create') {
console.log('values', values);
await createModel({ ...values, status: values.status ? 1 : 0 });
} else {
await updateModel({
...values,
status: values.status ? 1 : 0,
id,
});
}
tableRef.current?.reload();
return true;
},
[type, id],
);
const toolbarActions: ToolbarAction[] = [
{
key: 'add',
label: '新建',
type: 'primary',
icon: <PlusOutlined />,
onClick: handleAdd,
},
];
const actionColumns: ProColumns<AiModelRespVO> = {
title: '操作',
dataIndex: 'option',
valueType: 'option',
fixed: 'right',
width: 120,
render: (
_text: React.ReactNode,
record: AiModelRespVO,
_: any,
action: any,
) => [
<a key="editable" onClick={() => handleEdit(record)}>
</a>,
<Popconfirm
title="是否删除?"
key="delete"
onConfirm={async () => {
await delModel(record.id as number);
message.success('删除成功');
action?.reload();
}}
okText="是"
cancelText="否"
>
<a></a>
</Popconfirm>,
],
};
const columns = [...baseDeptColumns, actionColumns];
return (
<>
<EnhancedProTable<AiModelRespVO>
ref={tableRef}
columns={columns}
request={onFetch}
toolbarActions={toolbarActions}
headerTitle="模型管理"
showIndex={false}
showSelection={false}
search={false}
/>
<ConfigurableDrawerForm
ref={configurableDrawerRef}
title={formStatusType[type]}
columns={formColumns(type)}
onSubmit={handleSubmit}
/>
</>
);
};
export default ModelPage;

View File

@@ -1,4 +1,5 @@
import type { ProColumns } from '@ant-design/pro-components';
import { Tag } from 'antd';
import GroupTagSelect from '@/components/GroupTag/GroupTagSelect';
import {
type AiSampleRespVO,
@@ -15,13 +16,60 @@ export const baseTenantColumns: ProColumns<AiSampleRespVO>[] = [
{
title: '样本名称',
dataIndex: 'sampleName',
// width: 500,
width: 200,
ellipsis: true,
},
{
title: '枚举标签',
dataIndex: 'enumTags',
width: 120,
ellipsis: true,
render: (_, record) => {
return (
record.enumTags?.map((tag) => {
return (
<>
{tag.enumValue}:<Tag key={tag.id}>{tag.tagName}</Tag>
</>
);
}) || '-'
);
},
},
{
title: '个性标签',
dataIndex: 'tags',
width: 200,
hideInSearch: true,
render: (_, record) => {
return record.tags?.map((tag) => {
return (
<Tag key={tag.id} style={{ marginBottom: 5, marginTop: 5 }}>
{tag.tagName}
</Tag>
);
});
},
},
{
title: '关联模型',
width: 100,
dataIndex: 'relatedModels',
},
{
title: '文件格式',
width: 100,
dataIndex: 'sampleMineType',
},
{
title: '注释',
width: 100,
dataIndex: 'remark',
hideInSearch: true,
},
{
title: '标签',
hideInTable: true,
@@ -52,16 +100,11 @@ export const baseTenantColumns: ProColumns<AiSampleRespVO>[] = [
},
}}
editable
placeholder="请选择技术栈"
placeholder="请选择标签"
/>
);
},
},
{
title: '样本格式',
hideInTable: true,
dataIndex: 'sample_mine_type',
},
];
// export const formColumns = (data: {

View File

@@ -1,5 +1,6 @@
import { ProForm, ProFormGroup, ProFormText } from '@ant-design/pro-components';
import { Button, message, Space, Tag } from 'antd';
import { Button, Empty, Input, message, Space, Tag } from 'antd';
import FormItem from 'antd/es/form/FormItem';
import type { RowSelectionType } from 'antd/es/table/interface';
import type { FormInstance } from 'antd/lib';
import dayjs from 'dayjs';
@@ -13,6 +14,7 @@ import React, {
import GroupTagModal from '@/components/GroupTag/GroupTagModal';
import type { TagItem } from '@/components/GroupTag/types';
import type { FileItem } from '@/components/RenameRule';
import TagEditor from '@/components/TagEditor';
import {
createSampleTag,
createSampleTagGroup,
@@ -20,6 +22,8 @@ import {
deleteSampleTag,
deleteSampleTagGroup,
deleteSampleTagRelate,
downloadSample,
downloadZipFile,
getSampleTagGroup,
getSampleTagPage,
relateSample,
@@ -123,7 +127,10 @@ const SampleTagDetail = <T extends Record<string, any>>(
};
// 下载
const handleDownloadAll = () => {};
const handleDownloadAll = async () => {
const ids = data?.map((sample) => sample.id) as number[];
downloadZipFile(ids);
};
const handleTagManager = () => {
setTagManagerVisible(true);
@@ -158,8 +165,21 @@ const SampleTagDetail = <T extends Record<string, any>>(
setTagManagerVisible(false);
};
const onDownload = () => {
const item = data?.[0];
const link = document.createElement('a');
link.href = item?.sampleFilePath;
link.download = item?.sampleName; // 设置下载的文件名
link.style.display = 'none';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
};
return (
<>
{data!.length > 0 ? (
<>
<div className="sample-tag-detail">
<ProForm name="validate_other" formRef={formRef} submitter={false}>
{type === 'radio' && (
<ProFormGroup title="预览">
@@ -194,7 +214,7 @@ const SampleTagDetail = <T extends Record<string, any>>(
}) || [];
await updateSamples(newData);
props?.onRefresh?.();
message.success('更新成功');
message.success('更新样本名称成功');
}
},
}}
@@ -215,6 +235,7 @@ const SampleTagDetail = <T extends Record<string, any>>(
}) || [];
await updateSamples(newData);
props?.onRefresh?.();
message.success('更新注释成功');
}
},
}}
@@ -222,8 +243,14 @@ const SampleTagDetail = <T extends Record<string, any>>(
placeholder="请输入注释"
/>
</ProFormGroup>
<ProFormGroup title="标签">
<ProFormGroup title="枚举标签" block></ProFormGroup>
<ProForm.Item name="tag1" label="物种">
<TagEditor maxCount={1} />
</ProForm.Item>
<ProForm.Item name="tag2" label="情绪">
<TagEditor maxCount={1} />
</ProForm.Item>
<ProFormGroup title="个性标签">
{/* <Form.Item name="tag"> */}
{forMap(value.tags || [])}
</ProFormGroup>
@@ -238,11 +265,15 @@ const SampleTagDetail = <T extends Record<string, any>>(
<ProFormGroup title="文本信息" block></ProFormGroup>
<Space size={10} style={{ width: '100%', marginBottom: 12 }}>
<span>: </span>
<span>{dayjs(value.createTime).format('YYYY-MM-DD HH:mm:ss')}</span>
<span>
{dayjs(value.createTime).format('YYYY-MM-DD HH:mm:ss')}
</span>
</Space>
<Space size={10} style={{ width: '100%', marginBottom: 12 }}>
<span></span>
<span>{dayjs(value.updateTime).format('YYYY-MM-DD HH:mm:ss')}</span>
<span>
{dayjs(value.updateTime).format('YYYY-MM-DD HH:mm:ss')}
</span>
</Space>
<Space size={10} style={{ width: '100%', marginBottom: 12 }}>
<span>: </span>
@@ -252,34 +283,6 @@ const SampleTagDetail = <T extends Record<string, any>>(
<span>: </span>
<span>{value.sampleMineType}</span>
</Space>
{type === 'checkbox' && (
<>
<ProFormGroup title="其他"></ProFormGroup>
<Button
block
style={{ marginBottom: 24 }}
onClick={handleTagManager}
>
</Button>
<Button
block
style={{ marginBottom: 24 }}
onClick={handleDownloadAll}
>
</Button>
<Button
block
color="danger"
style={{ marginBottom: 24 }}
onClick={handleDeleteAll}
>
</Button>
</>
)}
</ProForm>
<GroupTagModal
visible={modalVisible}
@@ -305,20 +308,52 @@ const SampleTagDetail = <T extends Record<string, any>>(
width={800}
height={500}
/>
</div>
<TagManager
visible={tagManagerVisible}
files={tagNames}
onOk={onRename}
onCancel={() => setTagManagerVisible(false)}
></TagManager>
<Space style={{ width: '100%', justifyContent: 'center', padding: 12 }}>
<Button color="danger" onClick={() => {}}>
{type === 'radio' ? (
<Space
style={{ width: '100%', justifyContent: 'center', padding: 12 }}
className="tag-manager-btns"
>
<Button color="danger" onClick={onDownload}>
</Button>
<Button color="danger" variant="solid" onClick={handleDeleteAll}>
</Button>
</Space>
) : (
<Space
style={{
width: '100%',
justifyContent: 'center',
padding: 12,
flexWrap: 'wrap',
flexDirection: 'column',
}}
className="tag-manager-btns"
>
<Button block onClick={handleTagManager}>
</Button>
<Button block onClick={handleDownloadAll}>
</Button>
<Button block color="danger" onClick={handleDeleteAll}>
</Button>
</Space>
)}
</>
) : (
<Empty description="未选择样本" />
)}
</>
);
};

View File

@@ -1,18 +1,38 @@
.tag-content {
display: flex;
background: #fff;
width: 100%;
height: calc(100vh - 90px);
display: flex;
overflow: hidden;
:global {
.left {
background: #fff;
overflow: auto;
:global {
.ant-pro-table {
flex: 1 auto;
flex: 1;
}
.detail {
display: flex;
flex-direction: column;
border-left: 1px solid #e8e8e8;
width: 400px;
width: 360px;
height: calc(100vh - 80px);
overflow-y: auto;
background: #fff;
overflow-x: hidden;
position: relative;
.ant-pro-form-group-title {
margin-bottom: 20px;
}
.sample-tag-detail {
padding: 16px;
}
.tag-manager-btns {
position: sticky;
bottom: 10px;
background: #fff;
box-shadow: 0 0 10px #e8e8e8;
align-items: center;
}
form {
flex: 1;
}

View File

@@ -1,5 +1,7 @@
import type { ActionType } from '@ant-design/pro-components';
import type { RowSelectionType } from 'antd/es/table/interface';
import type { UploadFile } from 'antd/es/upload';
import type { UploadProps } from 'antd/lib/upload';
import React, { useRef, useState } from 'react';
import EnhancedProTable from '@/components/EnhancedProTable';
import type { ToolbarAction } from '@/components/EnhancedProTable/types';
@@ -56,6 +58,7 @@ const SampleTag: React.FC = () => {
const onFetch = async (params: SampleReqVo) => {
const data = await getSamplePage({
...params,
pageNo: params.current,
});
return {
data: data.list,
@@ -68,10 +71,15 @@ const SampleTag: React.FC = () => {
tableRef.current?.onValuesChange({}, {});
type && setSelectedRows([]);
};
const onChangeVideo = (file: UploadFile[] | UploadFile | null) => {
tableRef.current?.reload();
};
return (
<>
<UploadCard />
<div className={styles['tag-content']}>
<div className="left">
<UploadCard onChange={onChangeVideo} />
<EnhancedProTable<AiSampleRespVO>
ref={tableRef}
columns={baseTenantColumns}
@@ -80,15 +88,19 @@ const SampleTag: React.FC = () => {
headerTitle="样本列表"
showIndex={false}
enableRowClick={true}
scroll={{ x: 400 }}
rowSelection={{
type: selectTableType,
selectedRowKeys: selectedRows.map((item) => item.id) as React.Key[],
selectedRowKeys: selectedRows.map(
(item) => item.id,
) as React.Key[],
onChange: (_, selectedRows) => {
setSelectedRows(selectedRows);
},
}}
/>
{selectedRows.length > 0 && (
</div>
<div className="detail">
<SampleTagDetail<AiSampleRespVO>
type={selectTableType}
@@ -96,7 +108,6 @@ const SampleTag: React.FC = () => {
onRefresh={onRefresh}
/>
</div>
)}
</div>
<GroupTagModal
visible={modalVisible}

View File

@@ -27,20 +27,60 @@ const ProdInfo = <T extends Record<string, any>>(props: ProdInfoProps<T>) => {
name="prodName"
label="商品名字"
rules={[{ required: true }]}
width="xl"
colProps={{
span: 24,
}}
/>
<ProFormText
name="abbreviation"
label="商品简称"
colProps={{
span: 24,
}}
/>
<ProFormText
name="brief"
label="商品概述"
colProps={{
span: 24,
}}
/>
<ProFormText
name="prodNumber"
label="商品编码"
colProps={{
span: 24,
}}
/>
<ProFormText
name="brand"
label="品牌"
colProps={{
span: 24,
}}
/>
<ProFormText
name="shopId"
label="商品所有权"
colProps={{
span: 24,
}}
/>
<ProFormText
name="categoryName"
label="关联类目"
colProps={{
span: 24,
}}
/>
<ProFormText name="abbreviation" label="商品简称" width="xl" />
<ProFormText name="brief" label="商品概述" width="xl" />
<ProFormText name="prodNumber" label="商品编码" width="xl" />
<ProFormText name="brand" label="品牌" width="xl" />
<ProFormText name="shopId" label="商品所有权" width="xl" />
<ProFormText name="categoryName" label="关联类目" width="xl" />
<ProForm.Item
name="tag"
label="商品标签"
tooltip="设置卖点标签单标签限xx字符最多x个标签"
layout="horizontal"
width="xl"
colProps={{
span: 24,
}}
required={false}
// getValueFromEvent={(e) => e.fileList}
>
@@ -58,7 +98,9 @@ const ProdInfo = <T extends Record<string, any>>(props: ProdInfoProps<T>) => {
style={{ width: '100%' }}
name="pic"
label="主图"
width="xl"
colProps={{
span: 24,
}}
extra="仅支持.jpg .png 格式建议图片比例1:1限1张"
// rules={[{ required: true }]}
// getValueFromEvent={(e) => e.fileList}
@@ -70,26 +112,33 @@ const ProdInfo = <T extends Record<string, any>>(props: ProdInfoProps<T>) => {
name="imgs"
label="轮播图"
layout="horizontal"
width="xl"
colProps={{
span: 24,
}}
extra="仅支持.jpg .png 格式建议图片比例1:1限7张"
// getValueFromEvent={(e) => e.fileList}
>
<UploadImages />
<UploadImages maxCount={7} />
</ProForm.Item>
<ProForm.Item
style={{ width: '100%' }}
name="whiteImg"
label="白底图"
width="xl"
colProps={{
span: 24,
}}
extra="仅支持.jpg .png 格式建议图片比例1:1限1张"
// getValueFromEvent={(e) => e.fileList}
>
<UploadImages />
</ProForm.Item>
<ProForm.Item
style={{ width: '100%' }}
name="video"
label="主视频"
width="xl"
colProps={{
span: 24,
}}
extra="仅支持.MP4 .MOV 格式建议比例1:1、16:9限1个"
// getValueFromEvent={(e) => e.fileList}
>
@@ -99,7 +148,9 @@ const ProdInfo = <T extends Record<string, any>>(props: ProdInfoProps<T>) => {
name="content"
label="图文介绍"
rules={[{ required: true }]}
width="xl"
colProps={{
span: 24,
}}
// getValueFromEvent={(e) => e.fileList}
>
<TinymceEditor />
@@ -110,7 +161,7 @@ const ProdInfo = <T extends Record<string, any>>(props: ProdInfoProps<T>) => {
name="seoShortName"
label="短标题"
colProps={{
span: 20,
span: 24,
}}
/>
<ProFormText name="seoSearch" label="SEO标题" width={'xl'} />
@@ -118,7 +169,9 @@ const ProdInfo = <T extends Record<string, any>>(props: ProdInfoProps<T>) => {
<Divider />
<ProForm.Item
style={{ width: '100%' }}
colProps={{
span: 24,
}}
name="shareImage"
label="分享图"
width="xl"
@@ -130,7 +183,9 @@ const ProdInfo = <T extends Record<string, any>>(props: ProdInfoProps<T>) => {
name="shareContent"
label="分享话术"
placeholder={'请输入分享话术'}
width={'xl'}
colProps={{
span: 24,
}}
/>
</ProFormGroup>
<ProFormGroup>

View File

@@ -1,67 +1,85 @@
import { ProForm, ProFormText } from '@ant-design/pro-components';
import { Anchor, Col, Form, Row } from 'antd';
import ProdReservationConfig from './prodReservationConfig';
import ProdServiceAreasInfo from './prodServiceAreasInfo';
import ProdWeightConfig from './prodWeightConfig';
const ServiceRule: React.FC = () => {
return (
<ProForm<{
name: string;
company: string;
}>
grid
onFinish={async (values) => {
// await waitTime(2000);
console.log(values);
// message.success('提交成功');
}}
initialValues={{
name: '蚂蚁设计有限公司',
useMode: 'chapter',
}}
<ProForm
onFinish={async (value) => console.log(value)}
layout="horizontal"
style={{ width: '100%' }}
labelCol={{ span: '100px' }}
>
<Row gutter={24}>
<Col span={18}>
<div id="prodServiceAreasInfo">
<ProdServiceAreasInfo />
</div>
<div id="prodReservationConfig">
<ProForm.Group>
<ProFormText
width="md"
name="name"
label="签约客户名称"
tooltip="最长为 24 位"
placeholder="请输入名称"
/>
<ProFormText
width="md"
name="company"
label="我方公司名称"
placeholder="请输入名称"
/>
<ProdReservationConfig />
</ProForm.Group>
<ProFormText width="sm" name="id" label="主合同编号" />
<ProForm.Item
label="数组数据"
name="dataSource"
// initialValue={defaultData}
trigger="onValuesChange"
>
{/* <EditableProTable<DataSourceType>
rowKey="id"
toolBarRender={false}
columns={columns}
recordCreatorProps={{
newRecordType: 'dataSource',
position: 'top',
record: () => ({
id: Date.now(),
addonBefore: 'ccccccc',
decs: 'testdesc',
}),
}}
editable={{
type: 'multiple',
editableKeys,
onChange: setEditableRowKeys,
actionRender: (row, _, dom) => {
return [dom.delete];
</div>
<div id="prodEmergencyInfoVO">
<ProForm.Group></ProForm.Group>
</div>
<div id="productOrderLimitVO">
<ProForm.Group title="周期接单上限"></ProForm.Group>
</div>
<div id="prodAdditionalFeeDatesList">
<ProForm.Group title="特殊时段规则"></ProForm.Group>
</div>
<div id="prodAdditionalFeePeriodsList">
<ProForm.Group title="特殊日期规则"></ProForm.Group>
</div>
<div id="prodWeightConfig">
<ProdWeightConfig />
</div>
</Col>
<Col span="400px">
<Anchor
items={[
{
key: 'prodServiceAreasInfo',
href: '#prodServiceAreasInfo',
title: '可服务区域',
},
}} */}
{/* /> */}
</ProForm.Item>
{
key: 'prodReservationConfig',
href: '#prodReservationConfig',
title: '可预约时段',
},
{
key: 'prodEmergencyInfoVO',
href: '#prodEmergencyInfoVO',
title: '紧急响应服务',
},
{
key: 'productOrderLimitVO',
href: '#productOrderLimitVO',
title: '周期接单上限',
},
{
key: 'prodAdditionalFeeDatesList',
href: '#prodAdditionalFeeDatesList',
title: '特殊时段规则',
},
{
key: 'prodAdditionalFeePeriodsList',
href: '#prodAdditionalFeePeriodsList',
title: '特殊日期规则',
},
{
key: 'prodWeightConfig',
href: '#prodWeightConfig',
title: '体重/体型选项',
},
]}
/>
</Col>
</Row>
</ProForm>
);
};

View File

@@ -0,0 +1,139 @@
import {
ProFormCheckbox,
ProFormDigit,
ProFormRadio,
ProFormSelect,
ProFormSwitch,
ProFormText,
} from '@ant-design/pro-components';
import { Divider, Space, Typography } from 'antd';
import React from 'react';
import type { ProdReservationInfoVO } from '@/services/prod/prod-manager/rule';
const { Text, Title } = Typography;
type ValueConfig = {
reservationSwitch?: number;
prodReservationConfig?: ProdReservationInfoVO;
};
interface ProdReservationConfigProps {
value?: ValueConfig;
onChange?: (value?: ValueConfig) => void;
}
const ProdReservationConfig: React.FC<ProdReservationConfigProps> = (props) => {
return (
<>
<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
flexWrap: 'wrap',
gap: 12,
}}
>
<div>
<Title level={4}></Title>
<Text type="secondary">
</Text>
</div>
<ProFormSwitch
name="reservationSwitch"
label="应用配置"
labelCol={{ span: 13 }}
fieldProps={{
onChange: (checked) => {
props.onChange?.({
reservationSwitch: checked ? 1 : 0,
});
},
checkedChildren: '开启',
unCheckedChildren: '关闭',
}}
/>
</div>
<Divider />
<ProFormDigit
name="advanceHours"
label="预约时间约定"
wrapperCol={{ span: 11 }}
rules={[
{
required: true,
message: '请输入提前预约时间',
},
]}
fieldProps={{
placeholder: '请输入提前预约时间',
min: 0,
max: 24,
addonBefore: '提前',
addonAfter: '小时',
}}
/>
<ProFormRadio.Group
name="reservationDateRange"
label="预约日期范围"
options={[
{ label: '7天', value: 7 },
{ label: '10天', value: 10 },
{ label: '15天', value: 15 },
{ label: '30天', value: 30 },
]}
rules={[
{
required: true,
message: '请选择预约日期范围',
},
]}
/>
<ProFormRadio.Group
name="allowChange"
label="更改预约时间"
options={[
{ label: '允许更改预约时间', value: 1 },
{ label: '禁止更改预约时间', value: 0 },
]}
rules={[
{
required: true,
message: '请选择更改预约时间',
},
]}
/>
<ProFormRadio.Group
name="changeTimeRule"
label="时间更改规则"
options={[
{ label: '服务开始前 1小时 可更改预约时间', value: 1 },
{ label: '服务开始前 2小时 可更改预约时间', value: 2 },
{ label: '服务开始前 3小时 可更改预约时间', value: 3 },
]}
rules={[
{
required: true,
message: '请选择时间更改规则',
},
]}
/>
<ProFormRadio.Group
name="maxChangeTimes"
label="更改次数规则"
options={[
{ label: '允许修改 1 次', value: 1 },
{ label: '允许修改 2 次', value: 2 },
{ label: '允许修改 3 次', value: 3 },
]}
rules={[
{
required: true,
message: '请选择时间更改规则',
},
]}
/>
</>
);
};
export default React.memo(ProdReservationConfig);

View File

@@ -0,0 +1,115 @@
import {
ProFormCheckbox,
ProFormDigit,
ProFormRadio,
ProFormSelect,
ProFormSwitch,
ProFormText,
} from '@ant-design/pro-components';
import { Divider, Space, Typography } from 'antd';
import React from 'react';
import type { ProdServiceAreasInfoVO } from '@/services/prod/prod-manager/rule';
const { Text, Title } = Typography;
type ValueConfig = {
regionSwitch?: number;
prodServiceAreasInfo?: ProdServiceAreasInfoVO;
};
interface ProdServiceAreasInfoProps {
value?: ValueConfig;
onChange?: (value?: ValueConfig) => void;
}
const ProdServiceAreasInfo: React.FC<ProdServiceAreasInfoProps> = (props) => {
return (
<>
<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
flexWrap: 'wrap',
gap: 12,
}}
>
<div>
<Title level={4}></Title>
<Text type="secondary">
</Text>
</div>
<ProFormSwitch
name="regionSwitch"
label="应用配置"
labelCol={{ span: 13 }}
fieldProps={{
onChange: (checked) => {
props.onChange?.({
regionSwitch: checked ? 1 : 0,
});
},
checkedChildren: '开启',
unCheckedChildren: '关闭',
}}
/>
</div>
<Divider />
<ProFormSelect
name="areaNameList"
label="服务区域"
options={[
'鼓楼区',
'台江区',
'晋安区',
'马尾区',
'闽侯县',
'福清市(县级市)',
'长乐区',
'罗源县',
'永泰县',
'平潭县',
]}
fieldProps={{
mode: 'multiple',
}}
placeholder="请选择服务区域"
rules={[
{
required: true,
message: '请选择服务区域',
type: 'array',
},
]}
/>
<ProFormRadio.Group
name="ruleType"
label="超区规则"
options={[
{ label: '拒单', value: 0 },
{ label: '接单-收超区费', value: 2 },
{ label: '接单-收超区费-收超区费', value: 3 },
]}
rules={[
{
required: true,
message: '请选择服务区域',
},
]}
/>
<ProFormDigit
rules={[
{
required: true,
message: '请设置费用',
},
]}
label="设置费用"
name="fee"
/>
</>
);
};
export default React.memo(ProdServiceAreasInfo);

View File

@@ -0,0 +1,278 @@
import {
DragSortTable,
EditableProTable,
type ProColumns,
ProFormCheckbox,
ProFormDigit,
ProFormRadio,
ProFormSelect,
ProFormSwitch,
ProFormText,
} from '@ant-design/pro-components';
import { Divider, Space, Typography } from 'antd';
import React from 'react';
import type {
ProdWeightRangePricesDO,
ProdWeightRangePricesSaveInfoVO,
} from '@/services/prod/prod-manager/rule';
const { Text, Title } = Typography;
type ValueConfig = {
weightSwitch?: number;
prodWeightConfig?: ProdWeightRangePricesSaveInfoVO;
};
interface ProdWeightConfigProps {
value?: ValueConfig;
onChange?: (value?: ValueConfig) => void;
}
const columns = [
{
title: '体型&体重',
dataIndex: 'name',
key: 'name',
width: '20%',
render: (text: string, record: any) => {
return <ProFormText name={record.key} label={text} />;
},
},
{
title: '价格',
dataIndex: 'price',
key: 'price',
width: '20%',
},
{
title: '操作',
dataIndex: 'price',
key: 'price',
width: '20%',
},
];
const prodWeightConfig: React.FC<ProdWeightConfigProps> = (props) => {
const { value } = props;
const columns: ProColumns<ProdWeightRangePricesDO>[] = [
{
title: '体型&体重 *',
dataIndex: 'weightRange',
width: '30%',
formItemProps: {
rules: [
{
required: true,
whitespace: true,
message: '此项是必填项',
},
],
},
},
{
title: '价格 *',
dataIndex: 'price',
width: '30%',
formItemProps: {
rules: [
{
required: true,
whitespace: true,
message: '此项是必填项',
},
],
},
},
{
title: '操作',
valueType: 'option',
width: 250,
render: () => {
return null;
},
},
];
return (
<>
<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
flexWrap: 'wrap',
gap: 12,
}}
>
<div>
<Title level={4}></Title>
<Text type="secondary">
</Text>
</div>
<ProFormSwitch
name="weightSwitch"
label="应用配置"
labelCol={{ span: 13 }}
fieldProps={{
onChange: (checked) => {
props.onChange?.({
weightSwitch: checked ? 1 : 0,
});
},
checkedChildren: '开启',
unCheckedChildren: '关闭',
}}
/>
</div>
<Divider />
<ProFormRadio.Group
name="isWeightCharge"
label="是否收费"
options={[
{
label: '是',
value: 0,
},
{
label: '否',
value: 1,
},
]}
rules={[
{
required: true,
message: '请选择是否收费',
},
]}
/>
{/* <EditableProTable<ProdWeightRangePricesDO>
rowKey="id"
headerTitle="可拖拽编辑表格"
maxLength={10}
recordCreatorProps={{
position: 'bottom',
record: () => ({ id: Date.now().toString(), weightRange: '', price: 0, }),
}}
columns={columns}
value={dataSource}
onChange={setDataSource}
editable={{
type: 'multiple',
editableKeys,
onSave: async (rowKey, data) => {
console.log('保存数据:', data);
},
onChange: setEditableRowKeys,
}}
// 拖拽排序配置
dragSortKey="sort"
onDragSortEnd={(newDataSource) => {
console.log('排序后:', newDataSource);
setDataSource(newDataSource as DataType[]);
}}
/> */}
</>
);
};
export default React.memo(prodWeightConfig);
// import { EditableProTable } from '@ant-design/pro-components';
// import type { ProColumns } from '@ant-design/pro-components';
// import { useState } from 'react';
// import { MenuOutlined } from '@ant-design/icons';
// interface DataType {
// id: string;
// name: string;
// age: number;
// email: string;
// }
// const DragEditableProTable: React.FC = () => {
// const [editableKeys, setEditableRowKeys] = useState<React.Key[]>([]);
// const [dataSource, setDataSource] = useState<DataType[]>([
// { id: '1', name: '张三', age: 25, email: 'zhangsan@example.com' },
// { id: '2', name: '李四', age: 30, email: 'lisi@example.com' },
// { id: '3', name: '王五', age: 28, email: 'wangwu@example.com' },
// ]);
// const columns: ProColumns<DataType>[] = [
// {
// title: '排序',
// dataIndex: 'sort',
// width: 60,
// render: () => <MenuOutlined style={{ cursor: 'grab', color: '#999' }} />,
// },
// {
// title: '姓名',
// dataIndex: 'name',
// formItemProps: {
// rules: [{ required: true, message: '请输入姓名' }],
// },
// },
// {
// title: '年龄',
// dataIndex: 'age',
// valueType: 'digit',
// },
// {
// title: '邮箱',
// dataIndex: 'email',
// valueType: 'text',
// },
// {
// title: '操作',
// valueType: 'option',
// width: 200,
// render: (text, record, _, action) => [
// <a
// key="editable"
// onClick={() => {
// action?.startEditable?.(record.id);
// }}
// >
// 编辑
// </a>,
// <a
// key="delete"
// onClick={() => {
// setDataSource(dataSource.filter((item) => item.id !== record.id));
// }}
// >
// 删除
// </a>,
// ],
// },
// ];
// return (
// <EditableProTable<DataType>
// rowKey="id"
// headerTitle="可拖拽编辑表格"
// maxLength={10}
// recordCreatorProps={{
// position: 'bottom',
// record: () => ({ id: Date.now().toString(), name: '', age: 0, email: '' }),
// }}
// columns={columns}
// value={dataSource}
// onChange={setDataSource}
// editable={{
// type: 'multiple',
// editableKeys,
// onSave: async (rowKey, data) => {
// console.log('保存数据:', data);
// },
// onChange: setEditableRowKeys,
// }}
// // 拖拽排序配置
// dragSortKey="sort"
// onDragSortEnd={(newDataSource) => {
// console.log('排序后:', newDataSource);
// setDataSource(newDataSource as DataType[]);
// }}
// />
// );
// };
// export default DragEditableProTable;

View File

@@ -6,16 +6,9 @@ import ProdInfo from '@/pages/prod/list/components/prod-info';
import Sku from '@/pages/prod/list/components/sku';
import type { Prod, ProdDetail, SkuConfig } from '@/services/prod/prod-manager';
const waitTime = (time: number = 100) => {
return new Promise((resolve) => {
setTimeout(() => {
resolve(true);
}, time);
});
};
const ProdDetailPage: React.FC<{
data?: Prod;
type?: string;
formRef?: React.RefObject<FormInstance<any>>;
}> = (props) => {
const { data, formRef } = props;
@@ -42,8 +35,8 @@ const ProdDetailPage: React.FC<{
formRef={formRef}
submitter={false}
onFinish={async () => {
await waitTime(1000);
message.success('提交成功');
// await waitTime(1000);
// message.success('提交成功');
}}
formProps={{
validateMessages: {

View File

@@ -1,5 +1,6 @@
import { EllipsisOutlined, PlusOutlined } from '@ant-design/icons';
import type { ActionType, ProColumns } from '@ant-design/pro-components';
import { useNavigate } from '@umijs/max';
import type { MenuProps, TabsProps } from 'antd';
import { Button, Dropdown, Space, Tabs } from 'antd';
import { useCallback, useRef, useState } from 'react';
@@ -25,6 +26,7 @@ const ProdList = () => {
const [type, setType] = useState<'create' | 'update' | 'test'>('create');
const [status, setStatus] = useState<number>();
const detailRef = useRef<ConfigurableDrawerFormRef>(null);
const navigator = useNavigate();
// const editRef = useRef<ConfigurableDrawerFormRef>(null);
const [id, setId] = useState<number>(0);
const onChange = useCallback(
@@ -89,6 +91,12 @@ const ProdList = () => {
[id, type],
);
const handleLink = (key: string, row: Prod) => {
if (key === 'sku') {
// navigator(`/prod/list/sku/${row.prodId}`);
}
console.log(key, row);
};
// const renderDetailFooter = () => {
// if (type === "update") {
// return (
@@ -118,7 +126,11 @@ const ProdList = () => {
},
{
key: 'sku',
label: <Button type="link">SKU管理</Button>,
label: (
<Button type="link" onClick={() => handleLink('sku', row)}>
SKU管理
</Button>
),
},
{
key: 'rules-service',
@@ -155,7 +167,9 @@ const ProdList = () => {
<Button type="link" onClick={() => handleEdit(record)}>
</Button>
<Button type="link">SKU管理</Button>
<Button type="link" onClick={() => handleLink('sku', record)}>
SKU管理
</Button>
</Space.Compact>
<Space.Compact direction="vertical" block key="2">
<Button type="link"></Button>

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

@@ -0,0 +1,20 @@
.sales {
:global {
.ant-pro-card-body {
padding: 16px !important;
}
.ant-pro-card-size-small {
.ant-pro-card-header {
background: rgba(0, 0, 0, 0.02);
min-height: 41px;
}
.ant-form-item {
margin-bottom: 0;
}
.ant-pro-card-col {
flex: 1 auto;
overflow: auto;
}
}
}
}

View File

@@ -0,0 +1,762 @@
//新建售后
import {
BetaSchemaForm,
type EditableFormInstance,
EditableProTable,
ProCard,
type ProColumns,
ProForm,
type ProFormColumnsType,
ProFormDependency,
ProFormDigit,
ProFormRadio,
ProFormTextArea,
} from '@ant-design/pro-components';
import {
Badge,
Button,
Divider,
Empty,
Form,
type FormInstance,
Input,
InputNumber,
Space,
Spin,
Statistic,
Table,
Typography,
} from 'antd';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import EditableProTableCom from '@/components/EditableProTable';
import CommonModal from '@/components/ModalCom/CommonModal';
import UploadImages from '@/components/Upload/UploadImages';
import styles from './createSales.module.less';
const { Title, Text } = Typography;
interface DataType {
key: string;
name: string;
age: number;
address: string;
}
const originData = Array.from({ length: 100 }).map<DataType>((_, i) => ({
key: i.toString(),
name: `Edward ${i}`,
age: 32,
address: `London Park no. ${i}`,
}));
const waitTime = (time: number = 100) => {
return new Promise((resolve) => {
setTimeout(() => {
resolve(true);
}, time);
});
};
const CreateSalesModal = () => {
const [loading, setLoading] = useState(false);
const formRefSales = useRef<FormInstance>(null);
const formRef = useRef<FormInstance>(null);
const [open, setOpen] = useState<boolean>(false);
const handleOk = useCallback(async () => {
try {
setLoading(true);
const values1 = formRef.current?.getFieldsValue();
const values2 = formRefSales.current?.getFieldsValue();
console.log({ ...values1, values2 });
} finally {
setLoading(false);
}
}, [open]);
const onOpen = useCallback(
(flag: boolean) => {
setOpen(flag);
},
[open],
);
const salesColumns: ProFormColumnsType[] = [
{
title: '售后类型',
dataIndex: 'cancelReason',
valueType: 'radioButton',
formItemProps: {
rules: [
{
required: true,
},
],
},
fieldProps: {
options: [{ value: '仅退款', label: '仅退款' }],
},
},
{
title: '退款类型',
dataIndex: 'cancelRemark',
valueType: 'radioButton',
formItemProps: {
rules: [
{
required: true,
},
],
},
fieldProps: {
options: [
{ value: '整单退款(通用)', label: '整单退款' },
{ value: '选项二', label: '商品退款' },
{ value: '选项三', label: '扩展服务退款' },
{ value: '服务附加费退款', label: '服务附加费退款' },
{ value: '选项五', label: '退运费' },
{ value: '其他', label: '其他' },
],
},
},
// {
// title: "",
// dependencies: ["cancelRemark"],
// dataIndex: "cancelRemark1",
// valueType: "textarea",
// formItemProps: {
// rules: [
// {
// required: true,
// },
// ],
// },
// fieldProps: {
// placeholder: "请输入",
// },
// renderFormItem: (schema, config, form) => {
// const grade = form.getFieldValue("cancelRemark");
// if (grade === "其他") {
// return <Input.TextArea placeholder="请输入" rows={4} />;
// }
// if (grade === "服务附加费退款") {
// return (
// <Form.Item>
// <ExtendRefunds />
// </Form.Item>
// );
// }
// return <></>;
// },
// },
];
const applyColumns: ProFormColumnsType[] = [
{
title: '售后类型',
dataIndex: 'cancelReason1',
valueType: 'radio',
formItemProps: {
rules: [
{
required: true,
},
],
},
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: 'cancelRemark22',
valueType: 'textarea',
},
{
title: '问题凭证',
dataIndex: 'cancelRemark23',
formItemProps: {
extra: '请上传问题凭证,支持.jpg .png 格式,最多 4 张。',
},
renderFormItem: () => {
return <UploadImages multiple={true} maxCount={4} />;
},
},
{
title: '商家备注',
dataIndex: 'cancelRemark2311',
valueType: 'textarea',
},
];
return (
<>
<Button onClick={() => onOpen(true)}></Button>
<CommonModal
title="新建售后单"
open={true}
onOk={handleOk}
onCancel={() => onOpen(false)}
confirmLoading={loading}
>
<Space
direction="vertical"
style={{ width: '100%' }}
size={16}
className={styles.sales}
>
<ProCard.Group direction="row">
<ProCard layout="center">
<Statistic title="实付金额" value={1000} precision={2} />
</ProCard>
<ProCard layout="center">
<Statistic title="累计退款金额" value={200} precision={2} />
</ProCard>
<ProCard layout="center">
<Statistic title="可退款金额" value={800} precision={2} />
</ProCard>
</ProCard.Group>
<ProCard title="售后选项">
<ProForm
formRef={formRefSales}
layout="horizontal"
submitter={false}
labelCol={{ style: { width: '80px' } }}
labelAlign="right"
>
<ProFormRadio.Group
name="cancelReason"
label="售后类型"
rules={[{ required: true, message: '请选择售后类型' }]}
options={[{ value: '仅退款', label: '仅退款' }]}
radioType="button"
/>
<ProFormRadio.Group
name="cancelRemark"
label="退款类型"
rules={[{ required: true, message: '请选择退款类型' }]}
options={[
{ value: '整单退款(通用)', label: '整单退款' },
{ value: '选项二', label: '商品退款' },
{ value: '选项三', label: '扩展服务退款' },
{ value: '服务附加费退款', label: '服务附加费退款' },
{ value: '选项五', label: '退运费' },
{ value: '其他', label: '其他' },
]}
radioType="button"
/>
<ConditionalFields />
</ProForm>
</ProCard>
<ProCard title="申请信息(通用表单样式,根据当前订单状态调用不同的申请原因表单)">
<BetaSchemaForm
layoutType="Form"
formRef={formRef}
columns={applyColumns || []}
layout="horizontal"
submitter={false}
labelCol={{ style: { width: '80px' } }}
labelAlign="right"
/>
</ProCard>
<ProCard split="vertical" headerBordered title="预估退款明细">
<ProCard
colSpan={'50%'}
headerBordered
title="预估退款明细"
size="small"
>
<Space align="center" style={{ marginBottom: 16 }}>
<div>
<Text type="danger">退</Text>
<Text type="secondary"></Text>
</div>
<ProFormDigit
name="input-number"
min={1}
max={10}
fieldProps={{
precision: 0,
prefix: '¥',
style: { width: '100px', margin: 0 },
}}
/>
</Space>
<div>
<Text type="secondary">
退 <Text type="danger">¥700</Text>
退退
</Text>
</div>
<Divider />
<Title level={5}>退</Title>
<Space direction="vertical" size={8} style={{ width: '100%' }}>
<Badge
status="processing"
style={{ width: '100%' }}
text={
<div
style={{
display: 'inline-flex',
justifyContent: 'space-between',
width: '90%',
}}
>
<Text>/</Text>
<Text>¥500</Text>
</div>
}
/>
<Badge
status="processing"
style={{ width: '100%' }}
text={
<div
style={{
display: 'inline-flex',
justifyContent: 'space-between',
width: '90%',
}}
>
<Text>/</Text>
<Text>¥500</Text>
</div>
}
/>
<Badge
status="processing"
style={{ width: '100%' }}
text={
<div
style={{
display: 'inline-flex',
justifyContent: 'space-between',
width: '90%',
}}
>
<Text></Text>
<Text>¥500</Text>
</div>
}
/>
</Space>
</ProCard>
<ProCard
size="small"
headerBordered
title="预估退款至"
colSpan={'50%'}
>
<Title level={5}>
退
<Text type="secondary">退</Text>
</Title>
<Space direction="vertical" size={8} style={{ width: '100%' }}>
<Badge
status="processing"
style={{ width: '100%' }}
text={
<div
style={{
display: 'inline-flex',
justifyContent: 'space-between',
width: '90%',
}}
>
<Text>
<Text type="secondary"> 1626</Text>
</Text>
<Text>¥500</Text>
</div>
}
/>
<Badge
status="processing"
style={{ width: '100%' }}
text={
<div
style={{
display: 'inline-flex',
justifyContent: 'space-between',
width: '90%',
}}
>
<Text>
<Text type="secondary"> 1090</Text>
</Text>
<Text>¥500</Text>
</div>
}
/>
<Badge
status="processing"
style={{ width: '100%' }}
text={
<div
style={{
display: 'inline-flex',
justifyContent: 'space-between',
width: '90%',
}}
>
<Text>()</Text>
<Text>¥500</Text>
</div>
}
/>
<Badge
status="processing"
style={{ width: '100%' }}
text={
<div
style={{
display: 'inline-flex',
justifyContent: 'space-between',
width: '90%',
}}
>
<Text></Text>
<Text>¥500</Text>
</div>
}
/>
</Space>
<Divider />
<Title level={5}></Title>
<Text type="secondary">
退
</Text>
<Space direction="vertical" size={8} style={{ width: '100%' }}>
<Badge
status="processing"
style={{ width: '100%' }}
text={
<div
style={{
display: 'inline-flex',
justifyContent: 'space-between',
width: '90%',
}}
>
<Text>
<Text type="secondary">60退</Text>
</Text>
<Text>¥500</Text>
</div>
}
/>
</Space>
</ProCard>
</ProCard>
</Space>
</CommonModal>
</>
);
};
type DataSourceType = {
id: React.Key;
prod?: string;
num?: number;
state?: string;
created_at?: number;
children?: DataSourceType[];
};
const columnsSale: ProColumns<DataSourceType>[] = [
{
title: '商品',
dataIndex: 'prod',
editable: () => false,
},
{
title: '退款信息',
key: 'refund',
editable: () => false,
dataIndex: 'state',
},
{
title: '退款数量',
dataIndex: 'num',
valueType: 'digit',
editable: () => true,
formItemProps: {
rules: [
{
required: true,
message: '请输入退款数量',
},
],
},
},
];
const defaultData: DataSourceType[] = new Array(20).fill(1).map((_, index) => {
return {
id: (Date.now() + index).toString(),
prod: `活动名称${index}`,
num: index,
state: 'open',
created_at: 1590486176000,
};
});
const ExtendRefunds = (props: {
onChange?: (value: readonly DataSourceType[]) => void;
}) => {
const [data, setData] = useState<readonly DataSourceType[]>([]);
const [loading, setLoading] = useState(true);
const editorFormRef = useRef<EditableFormInstance<DataSourceType>>(null);
const [editableKeys, setEditableRowKeys] = useState<React.Key[]>([]);
const [formformSales] = Form.useForm();
const [formExtend] = Form.useForm();
const [formServeExtFee] = Form.useForm();
const [formShippingFee] = Form.useForm();
const fetchData = async () => {
await waitTime();
setData(defaultData);
setLoading(false);
};
useEffect(() => {
fetchData();
}, []);
useEffect(() => {
props.onChange?.(data);
}, [data, props.onChange]);
useEffect(() => {
setEditableRowKeys(data!.map((item) => item.id));
}, [data]);
console.log(editableKeys);
return (
<EditableProTable
value={defaultData}
columns={columnsSale}
rowKey="id"
recordCreatorProps={false}
editable={{
// form: formformSales,
editableKeys: editableKeys,
onChange: setEditableRowKeys,
onValuesChange: (record, recordList) => {
setData(recordList);
},
}}
/>
// <EditableProTableCom<DataSourceType>
// columns={columnsSale}
// value={defaultData}
// rowKey="id"
// rowSelection={{
// selections: [Table.SELECTION_ALL, Table.SELECTION_INVERT],
// }}
// editable={{
// form: formformSales,
// editableKeys: editableKeys,
// onChange: setEditableRowKeys,
// onValuesChange: (record, recordList) => {
// setData(recordList);
// },
// }}
// />
);
};
// const ExtendRefunds = (props: {
// onChange?: (value: readonly DataSourceType[]) => void;
// }) => {
// const [data, setData] = useState<readonly DataSourceType[]>([]);
// const [loading, setLoading] = useState(true);
// const editorFormRef = useRef<EditableFormInstance<DataSourceType>>(null);
// const [editableKeys, setEditableRowKeys] = useState<React.Key[]>([]);
// const [formformSales] = Form.useForm();
// const [formExtend] = Form.useForm();
// const [formServeExtFee] = Form.useForm();
// const [formShippingFee] = Form.useForm();
// const fetchData = async () => {
// await waitTime();
// setData(defaultData);
// setLoading(false);
// };
// useEffect(() => {
// fetchData();
// }, []);
// useEffect(() => {
// props.onChange?.(data);
// }, [data]);
// useEffect(() => {
// setEditableRowKeys(data!.map((item) => item.id));
// console.log(data);
// }, [data]);
// const renderContent = useCallback(() => {
// // if (data.length === 0 && !loading) {
// // return (
// // <div>
// // <Empty
// // description="订单无此退款项/类型"
// // style={{
// // background: "#fff",
// // padding: "16px",
// // margin: 0,
// // borderRadius: "4px",
// // }}
// // />
// // </div>);
// // }
// return (
// <Spin spinning={loading} >
// <EditableProTable<DataSourceType>
// columns={columnsSale}
// rowKey="id"
// rowSelection={{
// selections: [Table.SELECTION_ALL, Table.SELECTION_INVERT],
// }}
// value={data}
// editable={{
// form: formformSales,
// editableKeys: data.map((item) => item.id),
// onChange: setEditableRowKeys,
// onValuesChange: (record, recordList) => {
// setData(recordList);
// },
// }}
// />
// {/* <EditableProTableCom<DataSourceType>
// columns={columnsSale}
// rowKey="id"
// value={data}
// key={"sale"}
// // onChange={onTableChange}
// rowSelection={{
// selections: [Table.SELECTION_ALL, Table.SELECTION_INVERT],
// }}
// editable={{
// form: formformSales,
// editableKeys: data.map((item) => item.id),
// onChange: setEditableRowKeys,
// // onValuesChange: (record, recordList) => {
// // setData(recordList);
// // },
// }}
// />
// </ProCard>
// <ProCard title="退款项">
// <EditableProTableCom<DataSourceType>
// columns={columnsSale}
// rowKey="id"
// value={data}
// // onChange={onTableChange}
// rowSelection={{
// selections: [Table.SELECTION_ALL, Table.SELECTION_INVERT],
// }}
// key={"sale1"}
// editable={{
// form: formExtend,
// editableKeys,
// onChange: setEditableRowKeys,
// // onValuesChange: (record, recordList) => {
// // setData(recordList);
// // },
// }}
// />
// </ProCard>
// <ProCard>
// <EditableProTableCom<DataSourceType>
// columns={columns}
// rowKey="id"
// value={data}
// // onChange={onTableChange}
// rowSelection={{
// selections: [Table.SELECTION_ALL, Table.SELECTION_INVERT],
// }}
// key={"sale2"}
// editable={{
// form: formServeExtFee,
// editableKeys,
// onChange: setEditableRowKeys,
// // onValuesChange: (record, recordList) => {
// // setData(recordList);
// // },
// }}
// />
// </ProCard>
// <ProCard>
// <EditableProTableCom<DataSourceType>
// columns={columns}
// rowKey="id"
// value={data}
// key={"sale4"}
// onChange={onTableChange}
// rowSelection={{
// selections: [Table.SELECTION_ALL, Table.SELECTION_INVERT],
// }}
// editable={{
// form: formShippingFee,
// editableKeys,
// onChange: setEditableRowKeys,
// // onValuesChange: (record, recordList) => {
// // setData(recordList);
// // },
// }}
// />
// </ProCard> */}
// </Spin>
// );
// }, [loading, data]);
// return <div key="salcessw"> {renderContent()}</div>;
// };
export default CreateSalesModal;
const ConditionalFields: React.FC = () => {
const cancelRemark = Form.useWatch('cancelRemark', { preserve: true });
if (cancelRemark === '其他') {
return (
<ProFormTextArea
name="cancelRemarkText"
label=""
placeholder="请输入"
rules={[{ required: true, message: '请输入内容' }]}
fieldProps={{
rows: 4,
}}
/>
);
}
if (cancelRemark === '服务附加费退款') {
return (
<ProForm.Item name="extendRefunds" label="">
<ExtendRefunds />
</ProForm.Item>
);
}
return null;
};

View File

@@ -0,0 +1,609 @@
import {
BetaSchemaForm,
ProCard,
type ProFormColumnsType,
} from '@ant-design/pro-components';
import {
Badge,
Button,
Divider,
Form,
Input,
InputNumber,
message,
Radio,
Space,
Statistic,
Table,
Typography,
} from 'antd';
import React, { useCallback, useEffect, useState } from 'react';
import CommonModal from '@/components/ModalCom/CommonModal';
import styles from './createSales.module.less';
const { Title, Text } = Typography;
type DataSourceType = {
id: React.Key;
prod: string;
num: number;
state: string;
availableNum: number; // 新增:可退款数量
};
const CreateSalesModal = () => {
const [loading, setLoading] = useState(false);
const [formSales] = Form.useForm();
const [formApply] = Form.useForm();
const [open, setOpen] = useState<boolean>(false);
const [cancelRemarkType, setCancelRemarkType] = useState<string>('');
const handleOk = useCallback(async () => {
try {
setLoading(true);
const values1 = await formApply.validateFields();
const values2 = await formSales.validateFields();
console.log('申请信息:', values1);
console.log('售后信息:', values2);
console.log('表格数据:', values2.extendRefunds);
message.success('提交成功');
onOpen(false);
} catch (error) {
console.error('表单验证失败:', error);
message.error('请完善表单信息');
} finally {
setLoading(false);
}
}, [formApply, formSales]);
const onOpen = useCallback(
(flag: boolean) => {
setOpen(flag);
if (!flag) {
setCancelRemarkType('');
formSales.resetFields();
formApply.resetFields();
}
},
[formSales, formApply],
);
const applyColumns: ProFormColumnsType[] = [
{
title: '售后类型',
dataIndex: 'cancelReason1',
valueType: 'radio',
formItemProps: {
rules: [{ required: true }],
},
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: 'cancelRemark22',
valueType: 'textarea',
},
{
title: '商家备注',
dataIndex: 'cancelRemark2311',
valueType: 'textarea',
},
];
return (
<>
<Button onClick={() => onOpen(true)}></Button>
<CommonModal
title="新建售后单"
open={open}
onOk={handleOk}
onCancel={() => onOpen(false)}
confirmLoading={loading}
width={1000}
>
<Space
direction="vertical"
style={{ width: '100%' }}
size={16}
className={styles.sales}
>
{/* 统计卡片 */}
<ProCard.Group direction="row">
<ProCard layout="center">
<Statistic title="实付金额" value={1000} precision={2} />
</ProCard>
<ProCard layout="center">
<Statistic title="累计退款金额" value={200} precision={2} />
</ProCard>
<ProCard layout="center">
<Statistic title="可退款金额" value={800} precision={2} />
</ProCard>
</ProCard.Group>
{/* 售后选项 */}
<ProCard title="售后选项">
<Form
form={formSales}
layout="horizontal"
labelCol={{ style: { width: '80px' } }}
labelAlign="right"
>
<Form.Item
name="cancelReason"
label="售后类型"
rules={[{ required: true, message: '请选择售后类型' }]}
>
<Radio.Group buttonStyle="solid">
<Radio.Button value="仅退款">退</Radio.Button>
</Radio.Group>
</Form.Item>
<Form.Item
name="cancelRemark"
label="退款类型"
rules={[{ required: true, message: '请选择退款类型' }]}
>
<Radio.Group
buttonStyle="solid"
onChange={(e) => {
setCancelRemarkType(e.target.value);
// 切换类型时清空相关字段
formSales.setFieldsValue({
cancelRemarkText: undefined,
extendRefunds: undefined,
});
}}
>
<Radio.Button value="整单退款(通用)">退</Radio.Button>
<Radio.Button value="选项二">退</Radio.Button>
<Radio.Button value="选项三">退</Radio.Button>
<Radio.Button value="服务附加费退款">
退
</Radio.Button>
<Radio.Button value="选项五">退</Radio.Button>
<Radio.Button value="其他"></Radio.Button>
</Radio.Group>
</Form.Item>
{cancelRemarkType === '其他' && (
<Form.Item
name="cancelRemarkText"
label=""
rules={[{ required: true, message: '请输入内容' }]}
labelCol={{ style: { width: '80px' } }}
>
<Input.TextArea placeholder="请输入" rows={4} />
</Form.Item>
)}
{cancelRemarkType === '服务附加费退款' && (
<Form.Item
name="extendRefunds"
label=""
labelCol={{ style: { width: '80px' } }}
rules={[
{
required: true,
validator: (_, value) => {
if (!value || value.length === 0) {
return Promise.reject(
new Error('请至少选择一项服务附加费'),
);
}
return Promise.resolve();
},
},
]}
>
<ExtendRefundsTable />
</Form.Item>
)}
</Form>
</ProCard>
{/* 申请信息 */}
<ProCard title="申请信息(通用表单样式,根据当前订单状态调用不同的申请原因表单)">
<BetaSchemaForm
layoutType="Form"
form={formApply}
columns={applyColumns}
layout="horizontal"
submitter={false}
labelCol={{ style: { width: '80px' } }}
labelAlign="right"
/>
</ProCard>
{/* 预估退款明细 */}
<ProCard split="vertical" headerBordered title="预估退款明细">
<ProCard
colSpan={'50%'}
headerBordered
title="预估退款明细"
size="small"
>
<Space align="center" style={{ marginBottom: 16 }}>
<div>
<Text type="danger">退</Text>
<Text type="secondary"></Text>
</div>
<InputNumber
min={1}
max={10000}
precision={0}
prefix="¥"
style={{ width: '120px' }}
defaultValue={700}
/>
</Space>
<div>
<Text type="secondary">
退 <Text type="danger">¥700</Text>
退退
</Text>
</div>
<Divider />
<Title level={5}>退</Title>
<Space direction="vertical" size={8} style={{ width: '100%' }}>
<Badge
status="processing"
style={{ width: '100%' }}
text={
<div
style={{
display: 'inline-flex',
justifyContent: 'space-between',
width: '90%',
}}
>
<Text>/</Text>
<Text>¥500</Text>
</div>
}
/>
<Badge
status="processing"
style={{ width: '100%' }}
text={
<div
style={{
display: 'inline-flex',
justifyContent: 'space-between',
width: '90%',
}}
>
<Text>/</Text>
<Text>¥500</Text>
</div>
}
/>
<Badge
status="processing"
style={{ width: '100%' }}
text={
<div
style={{
display: 'inline-flex',
justifyContent: 'space-between',
width: '90%',
}}
>
<Text></Text>
<Text>¥500</Text>
</div>
}
/>
</Space>
</ProCard>
<ProCard
size="small"
headerBordered
title="预估退款至"
colSpan={'50%'}
>
<Title level={5}>
退
<Text type="secondary">退</Text>
</Title>
<Space direction="vertical" size={8} style={{ width: '100%' }}>
<Badge
status="processing"
style={{ width: '100%' }}
text={
<div
style={{
display: 'inline-flex',
justifyContent: 'space-between',
width: '90%',
}}
>
<Text>
<Text type="secondary"> 1626</Text>
</Text>
<Text>¥500</Text>
</div>
}
/>
<Badge
status="processing"
style={{ width: '100%' }}
text={
<div
style={{
display: 'inline-flex',
justifyContent: 'space-between',
width: '90%',
}}
>
<Text>
<Text type="secondary"> 1090</Text>
</Text>
<Text>¥500</Text>
</div>
}
/>
<Badge
status="processing"
style={{ width: '100%' }}
text={
<div
style={{
display: 'inline-flex',
justifyContent: 'space-between',
width: '90%',
}}
>
<Text>()</Text>
<Text>¥500</Text>
</div>
}
/>
<Badge
status="processing"
style={{ width: '100%' }}
text={
<div
style={{
display: 'inline-flex',
justifyContent: 'space-between',
width: '90%',
}}
>
<Text></Text>
<Text>¥500</Text>
</div>
}
/>
</Space>
<Divider />
<Title level={5}></Title>
<Text type="secondary">
退
</Text>
<Space direction="vertical" size={8} style={{ width: '100%' }}>
<Badge
status="processing"
style={{ width: '100%' }}
text={
<div
style={{
display: 'inline-flex',
justifyContent: 'space-between',
width: '90%',
}}
>
<Text>
<Text type="secondary">60退</Text>
</Text>
<Text>¥500</Text>
</div>
}
/>
</Space>
</ProCard>
</ProCard>
</Space>
</CommonModal>
</>
);
};
const ExtendRefundsTable: React.FC<{
value?: DataSourceType[];
onChange?: (value: DataSourceType[]) => void;
}> = ({ value, onChange }) => {
const [dataSource, setDataSource] = useState<DataSourceType[]>([]);
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
// 初始化数据 - 默认值为 1并设置可退款数量
useEffect(() => {
const initData: DataSourceType[] = Array.from({ length: 10 }).map(
(_, index) => ({
id: `service_${index}`,
prod: `服务附加费项目${index + 1}`,
num: 1,
state: '待退款',
availableNum: index % 3 === 0 ? 0 : Math.floor(Math.random() * 10) + 1, // 模拟数据每3个有一个为0
}),
);
if (!value) {
setDataSource(initData);
// 初始化时通知父组件,只选中可退款数量 > 0 的项
const validItems = initData.filter((item) => item.availableNum > 0);
const validKeys = validItems.map((item) => item.id);
notifyChange(initData, validKeys);
setSelectedRowKeys(validKeys);
}
}, []);
// 当外部 value 变化时更新
useEffect(() => {
if (value && value.length > 0) {
// 更新选中状态,只保留可退款数量 > 0 的项
setSelectedRowKeys((prev) =>
prev.filter((key) => {
const item = value.find((v) => v.id === key);
return item && item.availableNum > 0;
}),
);
}
}, [value]);
// 通知父组件数据变化(包含选中信息)
const notifyChange = (data: DataSourceType[], selected: React.Key[]) => {
// 只返回选中的项
const selectedData = data.filter((item) => selected.includes(item.id));
onChange?.(selectedData);
};
const handleNumChange = (id: React.Key, num: number | null) => {
const record = dataSource.find((item) => item.id === id);
if (!record) return;
// 如果可退款数量为0不允许修改
if (record.availableNum <= 0) return;
const finalNum = num ?? 1;
// 不能超过可退款数量
const validNum = Math.min(finalNum, record.availableNum);
const newData = dataSource.map((item) =>
item.id === id ? { ...item, num: validNum } : item,
);
setDataSource(newData);
// 如果数量变为 0 或负数,取消选中
let newSelectedKeys = selectedRowKeys;
if (validNum <= 0) {
newSelectedKeys = selectedRowKeys.filter((key) => key !== id);
setSelectedRowKeys(newSelectedKeys);
}
notifyChange(newData, newSelectedKeys);
};
// 判断是否有错误
const isInvalid = (num: number | undefined | null): boolean => {
if (num === undefined || num === null) return true;
if (typeof num !== 'number') return true;
if (num <= 0) return true;
return false;
};
const columns = [
{
title: '服务项目',
dataIndex: 'prod',
key: 'prod',
width: '30%',
ellipsis: true,
},
{
title: '退款信息',
dataIndex: 'state',
key: 'state',
width: '20%',
},
{
title: '可退款数量',
dataIndex: 'availableNum',
key: 'availableNum',
width: '20%',
},
{
title: <span style={{ color: 'red' }}>* 退</span>,
dataIndex: 'num',
key: 'num',
width: '30%',
render: (value: number, record: DataSourceType) => {
const hasError = isInvalid(value);
const isDisabled = record.availableNum <= 0;
return (
<InputNumber
min={1}
max={record.availableNum} // 最大值为可退款数量
value={value}
onChange={(val) => handleNumChange(record.id, val)}
disabled={isDisabled}
style={{
width: '100%',
}}
placeholder={isDisabled ? '不可退款' : '请输入退款数量'}
precision={0}
/>
);
},
},
];
// 行选择配置
const rowSelection = {
selectedRowKeys,
onChange: (newSelectedRowKeys: React.Key[]) => {
setSelectedRowKeys(newSelectedRowKeys);
notifyChange(dataSource, newSelectedRowKeys);
},
getCheckboxProps: (record: DataSourceType) => ({
disabled: record.availableNum <= 0, // 可退款数量为 0 时禁用选中
name: record.prod,
}),
};
return (
<div>
<div
style={{
marginBottom: 8,
fontWeight: 500,
fontSize: 14,
color: '#000',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
}}
>
<span>退</span>
<span style={{ fontSize: 12, fontWeight: 'normal', color: '#666' }}>
{selectedRowKeys.length}
</span>
</div>
<Table
rowKey="id"
dataSource={dataSource}
columns={columns}
rowSelection={rowSelection}
size="small"
bordered
scroll={{ y: 300 }}
/>
</div>
);
};
export default CreateSalesModal;

View File

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

View File

@@ -1,7 +1,9 @@
import { Tabs, type TabsProps } from 'antd';
import React from 'react';
import type { TradeOrderPageRespVO } from '@/services/trade/order';
import { SalesTable } from '../../sales/list';
import OrderInfo from './order-info';
import SalesRefunds from './src/sales-refunds';
const DetailCom: React.FC<{ data?: TradeOrderPageRespVO }> = (props) => {
const items: TabsProps['items'] = [
@@ -20,8 +22,13 @@ const DetailCom: React.FC<{ data?: TradeOrderPageRespVO }> = (props) => {
label: '商品配送',
children: '商品配送',
},
{
key: '4',
label: '售后与退款',
children: <SalesRefunds orderStatus={props.data?.orderStatus} />,
},
];
return <Tabs defaultActiveKey="1" items={items} />;
return <Tabs defaultActiveKey="1" items={items} destroyOnHidden />;
};
export default React.memo(DetailCom);

View File

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

View File

@@ -23,7 +23,7 @@ const sharedOnCell = (_: DeptVO, index?: number) => {
export const surchargeInfoColumns: ProColumns<DeptVO>[] = [
{
title: '服务附加费',
dataIndex: 'name',
dataIndex: 'serveExtFee',
width: '33.33%',
render: (_, record, index) => {
if (index === 3) {
@@ -73,10 +73,13 @@ export const renderBaseInfoOrder = (
>
{data?.orderStatus && orderStatusObj.label}
</Title>
{data.orderStatus === 10 && (
<span>
<Text type="secondary"></Text>
<Text>430</Text>
</span>
)}
{data.orderStatus &&
Number(data.orderStatus) === OrderStatus.Refunded && (
<Paragraph>
@@ -121,18 +124,18 @@ export const renderBaseInfoOrder = (
{data.orderStatus &&
Number(data.orderStatus) === OrderStatus.Cancelled && (
<>
<Paragraph>
<div>
<Text type="secondary"></Text>
<Text></Text>
</Paragraph>
<Paragraph>
<Text>{data.cancelTime}</Text>
</div>
<div>
<Text type="secondary"></Text>
<Text>{data.cancelReason}</Text>
</Paragraph>
<Paragraph>
</div>
<div>
<Text type="secondary"></Text>
<Text>{data?.merchantRemark}</Text>
</Paragraph>
<Text>{data?.cancelRemark}</Text>
</div>
</>
)}
{data.orderStatus &&

View File

@@ -1,14 +1,22 @@
import { ProCard } from '@ant-design/pro-components';
import { Button, Card, Image, Space, Tag, Typography } from 'antd';
import React, { useCallback } from 'react';
import { Button, Card, Image, Modal, Space, Spin, Tag, Typography } from 'antd';
import React, { useCallback, useEffect, useState } from 'react';
import AdvancedImageGallery from '@/components/AdvancedImageGallery';
import DangerouslySetInnerHTML from '@/components/DangerouslySetInnerHTML';
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 styles from './index.module.less';
const { Text, Paragraph } = Typography;
const { Text, Paragraph, Title } = Typography;
const ProdInfo: React.FC<ItemConfig<Item[]>> = (props) => {
const { data = [] } = props;
const [visible, setVisible] = useState(false);
const [item, setItem] = useState<Item>();
const renderTitle = useCallback((item: Item) => {
return (
<Space style={{ height: '100%' }} size={16}>
@@ -24,9 +32,17 @@ const ProdInfo: React.FC<ItemConfig<Item[]>> = (props) => {
</Space>
);
}, []);
const onPhoto = useCallback(
async (item: Item) => {
setVisible(true);
setItem(item);
},
[item, visible],
);
return (
<div className={styles['order-info']}>
<Card title="商品信息">
<Card title="商品信息" loading={props.loading}>
{data?.map((item) => (
<ProCard
key={item.id}
@@ -35,7 +51,11 @@ const ProdInfo: React.FC<ItemConfig<Item[]>> = (props) => {
bordered
headerBordered
gutter={8}
extra={<Button size="small"></Button>}
extra={
<Button size="small" onClick={() => onPhoto(item)}>
</Button>
}
>
<ProCard
layout="default"
@@ -74,14 +94,12 @@ const ProdInfo: React.FC<ItemConfig<Item[]>> = (props) => {
>
<Text type="secondary"></Text>
<Text>
{' '}
¥{item.handedPrice} {item.unit}
</Text>
</Paragraph>
<Paragraph className="order-paragraph">
<Text type="secondary"></Text>
<Text>
{' '}
¥{item.expensePrice} {item.unit}/-
</Text>
</Paragraph>
@@ -141,8 +159,102 @@ const ProdInfo: React.FC<ItemConfig<Item[]>> = (props) => {
</ProCard>
))}
</Card>
<PhotoModal
open={visible}
onCancel={() => setVisible(false)}
item={item}
/>
</div>
);
};
// 交易快照
const PhotoModal: React.FC<{
open: boolean;
onCancel: () => void;
item?: Item;
}> = (props) => {
const [photo, setPhoto] = useState<TradeOrderFastPhotoRespVo>();
const [loading, setLoading] = useState<boolean>(false);
const { item } = props;
console.log(item, 'item');
const onPhoto = useCallback(async () => {
try {
setLoading(true);
const res = await getfastPhoto({
itemId: item?.id as number,
spuId: item?.spuId as number,
});
setPhoto(res);
} finally {
setLoading(false);
}
}, [item]);
useEffect(() => {
if (props.open && item) {
onPhoto();
}
}, [props.open, item]);
return (
<Modal
title="交易快照"
open={props.open}
onCancel={props.onCancel}
footer={null}
width={820}
>
<Spin spinning={loading}>
<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>
</Spin>
</Modal>
);
};
export default React.memo(ProdInfo);

View File

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

View File

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

View File

@@ -1,16 +1,22 @@
import { ClockCircleOutlined, EnvironmentOutlined } from '@ant-design/icons';
import { ProCard } from '@ant-design/pro-components';
import { Button, Card, Image, Space, Timeline, Typography } from 'antd';
import React from 'react';
import { ProCard, type ProColumns } from '@ant-design/pro-components';
import { Button, Card, Image, Modal, Space, Timeline, Typography } from 'antd';
import React, { useCallback, useEffect, useState } from 'react';
import EnhancedProTable from '@/components/EnhancedProTable';
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 styles from '../../index.module.less';
const { Text, Paragraph } = Typography;
const ServicePetUI: React.FC<ItemConfig<TradeServeInfo>> = (props) => {
const { data = {} } = props;
const { data = {}, id } = props;
const { boneInfo, subInfo } = data;
const [open, setOpen] = useState<boolean>(false);
return (
<div className={styles['order-info']}>
<Card title="服务信息">
@@ -54,7 +60,11 @@ const ServicePetUI: React.FC<ItemConfig<TradeServeInfo>> = (props) => {
size="small"
title="预约信息"
headerBordered
extra={<Button size="small"></Button>}
extra={
<Button size="small" onClick={() => setOpen(true)}>
</Button>
}
>
<Space size={100}>
<div>
@@ -99,8 +109,60 @@ const ServicePetUI: React.FC<ItemConfig<TradeServeInfo>> = (props) => {
</ProCard>
</ProCard>
</Card>
<SubTimeLogModal id={id} open={open} onCancel={() => setOpen(false)} />
</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);

View File

@@ -0,0 +1,22 @@
import { Button, Space, Typography } from 'antd';
import React from 'react';
import { SalesTable } from '@/pages/trade/sales/list';
import CreateSalesModal from '../../components/createSalesModal';
const { Title } = Typography;
const SalesRefunds: React.FC<{ orderStatus?: number }> = (props) => {
return (
<>
<Space align="center" style={{ marginBottom: 16 }} size={16}>
<Title level={3} style={{ margin: 0 }}>
退
</Title>
<CreateSalesModal />
</Space>
<SalesTable orderStatus={props?.orderStatus} />
</>
);
};
export default React.memo(SalesRefunds);

View File

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

View File

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

View File

@@ -0,0 +1,157 @@
import type { ProColumns } from '@ant-design/pro-components';
import { Badge, Button, Image, Space, Tag, Typography } from 'antd';
import type { TradeOrderPageRespVO } from '@/services/trade/order';
const { Text, Paragraph } = Typography;
export const baseOrderColumns: ProColumns<TradeOrderPageRespVO>[] = [
{
title: '售后服务单',
dataIndex: 'items',
hideInSearch: true,
ellipsis: true,
render: (_, record) => (
<Space direction="vertical">
<Badge status="error" text="等待退款" />
<div>
<Text type="secondary"></Text>
<Text>{record?.payPrice || '-'}</Text>
</div>
<div>
<Text type="secondary">退</Text>
<Text>{record.payType || '-'}</Text>
</div>
<div>
<Text type="secondary"></Text>
<Text>{record.financeStatus || '-'}</Text>
</div>
<div>
<Text type="secondary"></Text>
<Text>{record.financeStatus || '-'}</Text>
</div>
</Space>
),
},
{
title: '售后商品',
dataIndex: 'serveAddress',
hideInSearch: true,
ellipsis: true,
render: (_, record) => {
if (!record.items) {
return _;
}
return record.items.map((item, index) => (
<>
<div
style={{ width: '100%', display: 'flex', gap: '8px' }}
key={`${index}${Math.random()}`}
>
<Image src={item.picUrl} width={64} height={64} />
<div style={{ flex: '1', overflow: 'hidden' }}>
<Paragraph ellipsis style={{ width: '100%', marginBottom: 0 }}>
{item.spuName}
</Paragraph>
<div>{item.skuName}</div>
<div>
<Text type="secondary"></Text>
<Text>{item.count || 0}</Text>
</div>
</div>
</div>
<Button size="small" style={{ marginTop: 10, textAlign: 'right' }}>
</Button>
</>
));
},
},
{
title: '申请信息',
dataIndex: 'price',
hideInSearch: true,
render: (_, record) => (
<Space direction="vertical">
<div>
<Text type="secondary"></Text>
<Text>{record?.payPrice || '-'}</Text>
</div>
<div>
<Text type="secondary">退</Text>
<Text>{record.payType || '-'}</Text>
</div>
<div>
<Text type="secondary"></Text>
<Text>{record.financeStatus || '-'}</Text>
</div>
<div>
<Text type="secondary"></Text>
<Text>{record.financeStatus || '-'}</Text>
</div>
</Space>
),
},
{
title: '退款信息',
dataIndex: 'merchantName',
hideInSearch: true,
render: (_, record) => (
<Space direction="vertical">
<Badge status="default" text="无退款" />
<div>
<Text type="secondary">退</Text>
<Text>{record?.payPrice || '-'}</Text>
</div>
<div>
<Text type="secondary">退</Text>
<Text>{record.payType || '-'}</Text>
</div>
<Space>
<Button size="small" type="primary">
退
</Button>
<Button size="small" type="primary">
</Button>
<Button size="small">退</Button>
<Button size="small"></Button>
</Space>
</Space>
),
},
{
title: '申请人姓名、手机或ID',
dataIndex: 'userSearch',
hideInTable: true,
},
{
title: '售后类型',
dataIndex: 'orderCategoryId',
valueType: 'select',
hideInTable: true,
},
{
title: '退款类型',
dataIndex: 'orderTerminal',
valueType: 'select',
hideInTable: true,
},
{
title: '退款状态',
dataIndex: 'financeStatus',
valueType: 'select',
hideInTable: true,
},
{
title: '申请时间',
dataIndex: 'createTime',
valueType: 'dateRange',
hideInTable: true,
},
{
title: '退款时间',
dataIndex: 'subTime',
valueType: 'dateRange',
hideInTable: true,
},
];

View File

@@ -0,0 +1,13 @@
.sales {
:global {
.ant-pro-card-header {
background: rgba(0, 0, 0, 0.02);
min-height: 41px;
}
.ant-pro-card-col {
flex: 1 auto;
overflow: auto;
}
}
}

View File

@@ -0,0 +1,24 @@
// 售后列表
import type { TabsProps } from 'antd';
import { Tabs } from 'antd';
import React from 'react';
import { SalesStatusLableMap } from '@/constants/trade';
import SalesListItem from './list';
const TradeSalesList: React.FC = () => {
const items: TabsProps['items'] = Object.entries(SalesStatusLableMap).map(
([value, label]) => ({
key: value,
label: label,
children: <SalesListItem orderStatus={Number(value)} />,
}),
);
return (
<div className="page-container">
<Tabs defaultActiveKey="1" items={items} destroyOnHidden />
</div>
);
};
export default TradeSalesList;

View File

@@ -0,0 +1,133 @@
import { type ActionType, ProCard } from '@ant-design/pro-components';
import { Button, Input, Space, Statistic } from 'antd';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import EnhancedProTable from '@/components/EnhancedProTable';
import { baseOrderColumns } from './config';
const { Search } = Input;
import { DownOutlined, UpOutlined, UserOutlined } from '@ant-design/icons';
import {
getTradeOrderPage,
getTradeSummary,
type TradeOrderPageRespVO,
type TradeReq,
type TradeSummaryRespVO,
} from '@/services/trade/order';
const OrderListItem: React.FC<{ orderStatus: number }> = (props) => {
const { orderStatus } = props;
const tableRef = useRef<ActionType>(null);
const [isShowTotal, setIsShowTotal] = useState<boolean>(false);
const [summary, setSummary] = useState<TradeSummaryRespVO>();
const fetchSummary = async () => {
const res = await getTradeSummary();
setSummary(res);
};
useEffect(() => {
fetchSummary();
}, []);
const handleIsTotal = useCallback(() => {
setIsShowTotal(!isShowTotal);
}, [isShowTotal]);
const handleSearch = useCallback((value: string) => {
console.log('搜索', value);
tableRef.current?.reload();
}, []);
return (
<>
<Space
style={{
padding: '18px',
background: '#fff',
marginBottom: 16,
}}
>
<Search
placeholder="输入售后单号/关联订单号搜索"
enterButton
onSearch={handleSearch}
style={{ width: 300 }}
/>
<Button
icon={isShowTotal ? <DownOutlined /> : <UpOutlined />}
onClick={handleIsTotal}
>
</Button>
</Space>
{isShowTotal && (
<ProCard.Group direction="row" style={{ marginBottom: 18 }}>
<ProCard layout="center" style={{ background: '#f5f5f5' }}>
<Statistic
title="售后单数量"
value={summary?.orderCount}
precision={2}
/>
</ProCard>
<ProCard layout="center" style={{ background: '#f5f5f5' }}>
<Statistic
title="待退款金额"
value={summary?.payPrice}
precision={2}
/>
</ProCard>
<ProCard layout="center" style={{ background: '#f5f5f5' }}>
<Statistic
title="累计退款金额"
value={summary?.livePrice}
precision={2}
/>
</ProCard>
</ProCard.Group>
)}
<SalesTable orderStatus={orderStatus} search={true} />
</>
);
};
export const SalesTable = (props: {
orderStatus?: number;
search?: boolean;
}) => {
const tableRef = useRef<ActionType>(null);
const onFetch = async (
params: TradeReq & {
pageSize: number;
current: number;
},
) => {
const data = await getTradeOrderPage({
...params,
orderStatus: props?.orderStatus ? props?.orderStatus : undefined,
pageNo: params.current,
pageSize: params.pageSize,
});
return {
data: data.list,
success: true,
total: data.total,
};
};
const tabelConfig = {};
return (
<EnhancedProTable<TradeOrderPageRespVO>
ref={tableRef}
columns={baseOrderColumns}
request={onFetch}
headerTitle="售后列表"
showIndex={false}
showSelection={false}
search={props.search ? { defaultCollapsed: true } : false}
/>
);
};
export default React.memo(OrderListItem);

View File

@@ -1,4 +1,3 @@
import { getTenantIdByName, login } from "@/services/login";
import {
AlipayOutlined,
LockOutlined,
@@ -6,34 +5,36 @@ import {
TaobaoOutlined,
UserOutlined,
WeiboOutlined,
} from "@ant-design/icons";
} from '@ant-design/icons';
import {
LoginFormPage,
ProConfigProvider,
ProFormCaptcha,
ProFormCheckbox,
ProFormText,
} from "@ant-design/pro-components";
import { history, useModel, useNavigate } from "@umijs/max";
import { Button, Divider, Space, Tabs, theme, message } from "antd";
import type { CSSProperties } from "react";
import { useState } from "react";
import * as authUtil from "@/utils/auth";
import { flushSync } from "react-dom";
type LoginType = "phone" | "account";
} from '@ant-design/pro-components';
import { history, useModel, useNavigate } from '@umijs/max';
import { Button, Divider, message, Space, Tabs, theme } from 'antd';
import type { CSSProperties } from 'react';
import { useState } from 'react';
import { flushSync } from 'react-dom';
import { getTenantIdByName, login } from '@/services/login';
import * as authUtil from '@/utils/auth';
type LoginType = 'phone' | 'account';
const iconStyles: CSSProperties = {
color: "rgba(0, 0, 0, 0.2)",
fontSize: "18px",
verticalAlign: "middle",
cursor: "pointer",
color: 'rgba(0, 0, 0, 0.2)',
fontSize: '18px',
verticalAlign: 'middle',
cursor: 'pointer',
};
const Page = () => {
const [loginType, setLoginType] = useState<LoginType>("account");
const [loginType, setLoginType] = useState<LoginType>('account');
const { token } = theme.useToken();
const [messageApi, contextHolder] = message.useMessage();
const { initialState, setInitialState } = useModel("@@initialState");
const { initialState, setInitialState } = useModel('@@initialState');
const navigate = useNavigate();
// 获取租户 ID
const getTenantId = async (name: string) => {
@@ -57,7 +58,7 @@ const Page = () => {
try {
// 根据登录类型处理不同的参数
const params = {
tenantName: "芋道源码",
tenantName: '芋道源码',
...values,
};
@@ -71,21 +72,22 @@ const Page = () => {
// 调用登录接口
const data = await login(params);
// 登录成功
messageApi.success("登录成功!");
messageApi.success('登录成功!');
// 跳转到首页
authUtil.setToken(data);
await fetchUserInfo();
const urlParams = new URL(window.location.href).searchParams;
navigate(urlParams.get("redirect") || "/");
// navigate(urlParams.get("redirect") || "/");
window.location.href = urlParams.get('redirect') || '/';
} catch (error) {
messageApi.error("登录失败,请检查网络或稍后重试!");
messageApi.error('登录失败,请检查网络或稍后重试!');
}
};
return (
<div
style={{
backgroundColor: "white",
height: "100vh",
backgroundColor: 'white',
height: '100vh',
}}
>
{contextHolder}
@@ -95,8 +97,8 @@ const Page = () => {
backgroundVideoUrl="https://gw.alipayobjects.com/v/huamei_gcee1x/afts/video/jXRBRK_VAwoAAAAAAAAAAAAAK4eUAQBr"
// title="BY"
containerStyle={{
backgroundColor: "rgb(0 0 0 / 51%)",
backdropFilter: "blur(4px)",
backgroundColor: 'rgb(0 0 0 / 51%)',
backdropFilter: 'blur(4px)',
}}
onFinish={handleSubmit}
subTitle="百业到家云控台"
@@ -127,17 +129,17 @@ const Page = () => {
actions={
<div
style={{
display: "flex",
justifyContent: "center",
alignItems: "center",
flexDirection: "column",
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
flexDirection: 'column',
}}
>
<Divider plain>
<span
style={{
color: token.colorTextPlaceholder,
fontWeight: "normal",
fontWeight: 'normal',
fontSize: 14,
}}
>
@@ -147,48 +149,48 @@ const Page = () => {
<Space align="center" size={24}>
<div
style={{
display: "flex",
justifyContent: "center",
alignItems: "center",
flexDirection: "column",
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
flexDirection: 'column',
height: 40,
width: 40,
border: "1px solid " + token.colorPrimaryBorder,
border: '1px solid ' + token.colorPrimaryBorder,
background: token.colorBgContainer,
borderRadius: "50%",
borderRadius: '50%',
}}
>
<AlipayOutlined style={{ ...iconStyles, color: "#1677FF" }} />
<AlipayOutlined style={{ ...iconStyles, color: '#1677FF' }} />
</div>
<div
style={{
display: "flex",
justifyContent: "center",
alignItems: "center",
flexDirection: "column",
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
flexDirection: 'column',
height: 40,
width: 40,
border: "1px solid " + token.colorPrimaryBorder,
border: '1px solid ' + token.colorPrimaryBorder,
background: token.colorBgContainer,
borderRadius: "50%",
borderRadius: '50%',
}}
>
<TaobaoOutlined style={{ ...iconStyles, color: "#FF6A10" }} />
<TaobaoOutlined style={{ ...iconStyles, color: '#FF6A10' }} />
</div>
<div
style={{
display: "flex",
justifyContent: "center",
alignItems: "center",
flexDirection: "column",
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
flexDirection: 'column',
height: 40,
width: 40,
background: token.colorBgContainer,
border: "1px solid " + token.colorPrimaryBorder,
borderRadius: "50%",
border: '1px solid ' + token.colorPrimaryBorder,
borderRadius: '50%',
}}
>
<WeiboOutlined style={{ ...iconStyles, color: "#1890ff" }} />
<WeiboOutlined style={{ ...iconStyles, color: '#1890ff' }} />
</div>
</Space>
</div>
@@ -199,125 +201,125 @@ const Page = () => {
activeKey={loginType}
onChange={(activeKey) => setLoginType(activeKey as LoginType)}
>
<Tabs.TabPane key={"account"} tab={"账号密码登录"} />
<Tabs.TabPane key={"phone"} tab={"手机号登录"} />
<Tabs.TabPane key={'account'} tab={'账号密码登录'} />
<Tabs.TabPane key={'phone'} tab={'手机号登录'} />
</Tabs>
{loginType === "account" && (
{loginType === 'account' && (
<>
<ProFormText
name="username"
fieldProps={{
size: "large",
size: 'large',
prefix: (
<UserOutlined
style={{
color: token.colorText,
}}
className={"prefixIcon"}
className={'prefixIcon'}
/>
),
style: {
backgroundColor: "rgb(0 0 0 / 77%)",
color: "white",
backgroundColor: 'rgb(0 0 0 / 77%)',
color: 'white',
},
}}
placeholder={"用户名: admin or user"}
placeholder={'用户名: admin or user'}
rules={[
{
required: true,
message: "请输入用户名!",
message: '请输入用户名!',
},
]}
/>
<ProFormText.Password
name="password"
fieldProps={{
size: "large",
size: 'large',
prefix: (
<LockOutlined
style={{
color: token.colorText,
}}
className={"prefixIcon"}
className={'prefixIcon'}
/>
),
style: {
backgroundColor: "rgb(0 0 0 / 77%)",
color: "white",
backgroundColor: 'rgb(0 0 0 / 77%)',
color: 'white',
},
}}
placeholder={"密码: ant.design"}
placeholder={'密码: ant.design'}
rules={[
{
required: true,
message: "请输入密码!",
message: '请输入密码!',
},
]}
/>
</>
)}
{loginType === "phone" && (
{loginType === 'phone' && (
<>
<ProFormText
fieldProps={{
size: "large",
size: 'large',
prefix: (
<MobileOutlined
style={{
color: token.colorText,
}}
className={"prefixIcon"}
className={'prefixIcon'}
/>
),
style: {
backgroundColor: "rgb(0 0 0 / 77%)",
color: "white",
backgroundColor: 'rgb(0 0 0 / 77%)',
color: 'white',
},
}}
name="mobile"
placeholder={"手机号"}
placeholder={'手机号'}
rules={[
{
required: true,
message: "请输入手机号!",
message: '请输入手机号!',
},
{
pattern: /^1\d{10}$/,
message: "手机号格式错误!",
message: '手机号格式错误!',
},
]}
/>
<ProFormCaptcha
fieldProps={{
size: "large",
size: 'large',
prefix: (
<LockOutlined
style={{
color: token.colorText,
}}
className={"prefixIcon"}
className={'prefixIcon'}
/>
),
}}
captchaProps={{
size: "large",
size: 'large',
}}
placeholder={"请输入验证码"}
placeholder={'请输入验证码'}
captchaTextRender={(timing, count) => {
if (timing) {
return `${count} ${"获取验证码"}`;
return `${count} ${'获取验证码'}`;
}
return "获取验证码";
return '获取验证码';
}}
name="captcha"
rules={[
{
required: true,
message: "请输入验证码!",
message: '请输入验证码!',
},
]}
onGetCaptcha={async () => {
message.success("获取验证码成功验证码为1234");
message.success('获取验证码成功验证码为1234');
}}
/>
</>
@@ -332,7 +334,7 @@ const Page = () => {
</ProFormCheckbox>
<a
style={{
float: "right",
float: 'right',
}}
>

View File

@@ -100,9 +100,6 @@ export const errorConfig: RequestConfig = {
},
// 错误接收及处理
errorHandler: async (error: any, opts: any) => {
if (opts?.skipErrorHandler) throw error;
// 我们的 errorThrower 抛出的错误。
console.log('errorHandler', error);
const errorInfo: ResponseStructure | undefined = error.info;
if (error.name === 'BizError') {
if (errorInfo) {
@@ -118,6 +115,7 @@ export const errorConfig: RequestConfig = {
} else {
message.error(`发送请求时出了点问题:${error.msg}`);
}
throw error;
},
},
@@ -136,23 +134,20 @@ export const errorConfig: RequestConfig = {
const { data } = response as unknown as ResponseStructure;
const config = response.config;
const { code } = data;
// if (!data) {
// // 返回“[HTTP]请求没有返回值”;
// throw new Error();
// }
// 未设置状态码则默认成功状态
// 二进制数据则直接返回,例如说 Excel 导出
// if (
// response.request.responseType === "blob" ||
// response.request.responseType === "arraybuffer"
// ) {
// // 注意:如果导出的响应为 json说明可能失败了不直接返回进行下载
// // if (response.data.type !== "application/json") {
// // return response.data;
// // }
if (!data) {
// 返回“[HTTP]请求没有返回值”;
throw new Error();
}
if (
response.request.responseType === 'blob' ||
response.request.responseType === 'arraybuffer'
) {
return response;
// data = await new Response(data).json();
// }
// // 获取错误信息
}
// 获取错误信息
// const msg = data.msg || errorCode[code] || errorCode["default"];
// if (ignoreMsgs.indexOf(msg) !== -1) {
// // 如果是忽略的错误码,直接返回 msg 异常

View File

@@ -0,0 +1,74 @@
/**
* 返回数据
*
* AiModelRespVO
*/
export interface AiModelRespVO {
/**
* 创建时间
*/
createTime?: string;
/**
* 版本描述
*/
description?: string;
/**
* 主键
*/
id?: number;
/**
* 负载
*/
loadPercentage?: number;
/**
* 模型名称
*/
modelName?: string;
/**
* 状态0-禁用 1-启用 2-测试中 3-已废弃)
*/
status?: number;
/**
* 更新时间
*/
updateTime?: string;
/**
* 版本号
*/
version?: string;
}
import { request } from "@umijs/max";
export const getModelList = async (params: PageParam) => {
return request("/ai/model/page", {
method: "GET",
params,
});
};
export const createModel = async (params: AiModelRespVO) => {
return request("/ai/model/create", {
method: "POST",
data: params,
});
};
export const updateModel = async (params: AiModelRespVO) => {
return request("/ai/model/update", {
method: "PUT",
data: params,
});
};
export const delModel = async (id: number) => {
return request("/ai/model/delete", {
method: "DELETE",
params: { id },
});
};
export const updateModelStatus = async (params: AiModelRespVO) => {
return request("/ai/model/update-status", {
method: "PUT",
params: params,
});
};

View File

@@ -1,4 +1,5 @@
import { request } from "@umijs/max";
import { message } from "antd";
export interface SampleVo {
/**
@@ -85,9 +86,12 @@ export interface AiSampleRespVO {
* 样本时长
*/
sampleTime?: string;
tags?: { tagName?: string; id?: number }[];
enumTags?: { tagName?: string; id?: number; enumValue?: string }[];
}
export interface SampleReqVo extends PageParam {
current: number | undefined;
name?: string;
status?: number;
}
@@ -244,3 +248,80 @@ export const updateSampleTag = async (params: {
data: params,
});
};
// 下载
export const downloadSample = async (ids: number[]) => {
return request("/ai/sample/download", {
method: "GET",
params: { ids },
responseType: "blob",
});
};
export const getGroupListByYagId = async (id: number) => {
return request("/ai/sampleTag/group-list-by-tag-id", {
method: "GET",
params: { tagId: id },
});
};
export async function downloadZipFile(ids: number[]) {
try {
const response = await downloadSample(ids);
const blob = response; // Blob {size: 3164203, type: 'application/zip'}
// 验证文件类型
if (blob.type !== "application/zip" && !blob.type.includes("zip")) {
console.warn("返回的可能不是 ZIP 文件:", blob.type);
const downloadUrl = window.URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = downloadUrl;
link.download = `音频文件_${new Date().getTime()}.wav`; // 设置文件名和后缀
document.body.appendChild(link);
link.click();
// 清理
document.body.removeChild(link);
window.URL.revokeObjectURL(downloadUrl);
return;
}
// 从响应头获取文件名
let fileName = "音频文件";
const contentDisposition =
response.response?.headers?.["content-disposition"];
if (!fileName && contentDisposition) {
const match = contentDisposition.match(
/filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/
);
if (match?.[1]) {
fileName = decodeURIComponent(match[1].replace(/['"]/g, ""));
}
}
// 默认文件名
fileName = fileName || `archive_${new Date().getTime()}.zip`;
// 创建下载链接
const downloadUrl = window.URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = downloadUrl;
link.download = fileName;
document.body.appendChild(link);
link.click();
// 清理
document.body.removeChild(link);
window.URL.revokeObjectURL(downloadUrl);
message.success(
`下载成功,文件大小:${(blob.size / 1024 / 1024).toFixed(2)} MB`
);
return true;
} catch (error) {
console.error("下载失败:", error);
message.error("下载失败,请稍后重试");
return false;
}
}

View File

@@ -65,6 +65,7 @@ export interface TradeExtendServeInfo {
}
export interface TradeOrderDetailRespVO {
cancelRemark?: string;
/**
* 取消原因
*/
@@ -346,3 +347,73 @@ export interface TradeOrderStatusRespVo {
*/
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 { TradeOrderPageRespVO, TradeReq } from "./list";
import { TradeOrderDetailRespVO } from "./detail";
import { TradeOrderPageRespVO, TradeReq, TradeSummaryRespVO } from "./list";
import {
TradeOrderDetailRespVO,
TradeOrderFastPhotoRespVo,
TradeOrderSubLogDO,
} from "./detail";
export const getTradeOrderPage = async (params: TradeReq) => {
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;
}
export interface TradeSummaryRespVO {
orderCount?: number;
livePrice?: number;
payPrice?: number;
}

View File

@@ -2,7 +2,10 @@ import { Spin } from 'antd';
import React from 'react';
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[] => {
return menus.map((item) => {
let Component: React.ComponentType<any> | null = null;
if (item.component && item.component.trim().length > 0) {
@@ -34,85 +37,11 @@ export const loopMenuItem = (menus: MenuVO[], pId: number | string): any[] => {
</React.Suspense>
);
} else if (item.children && item.children.length > 0) {
// routeItem.redirect = "/prod/list";
// // 只有当没有 Component 但有子菜单时,才添加重定向
// const firstLeafPath = getFirstLeafPath(item.children);
// // 确保 firstLeafPath 存在,且不是一个会导致循环的路径
// if (
// firstLeafPath &&
// firstLeafPath !== item.path &&
// firstLeafPath.length > 0
// ) {
// // 在 UmiJS 中,路径是相对的,不需要构建完整路径
// const separator =
// item.path.endsWith("/") || firstLeafPath.startsWith("/") ? "" : "/";
// const fullPath = `${item.path}${separator}${firstLeafPath}`;
// console.log(`Redirecting from ${item.path} to ${fullPath}`);
// routeItem.element = <Navigate to={fullPath} replace={true} />;
// }
}
// 处理子菜单
if (item.children && item.children.length > 0) {
routeItem.children = loopMenuItem(item.children, item.id);
}
return routeItem;
});
};
// return menus.flatMap((item) => {
// let Component: React.ComponentType<any> | null = null;
// if (item.component && item.component.length > 0) {
// // 防止配置了路由,但本地暂未添加对应的页面,产生的错误
// Component = React.lazy(() => {
// const importComponent = () => import(`@/pages/${item.component}`);
// const import404 = () => import("@/pages/404");
// return importComponent().catch(import404);
// });
// }
// if (item.children && item.children.length > 0) {
// return [
// {
// path: item.path,
// hideInMenu: false,
// parentId: pId,
// id: item.id,
// children: [...loopMenuItem(item.children, item.id)], // 添加缺失的 children 属性
// },
// ];
// } else {
// return [
// {
// path: item.path,
// name: item.name,
// // icon: item.icon,
// id: item.id,
// parentId: pId,
// hideInMenu: !item.visible,
// element: (
// <React.Suspense
// fallback={<Spin style={{ width: "100%", height: "100%" }} />}
// >
// {Component && <Component />}
// </React.Suspense>
// ),
// },
// ];
// }
// });
// return [];
// };
// function getFirstLeafPath(menus: any[], parentPath: string): string {
// const firstMenu = menus[0];
// const currentPath = `${parentPath}/${firstMenu.path}`;
// if (firstMenu.children && firstMenu.children.length > 0) {
// if (!firstMenu.hideInMenu) {
// return getFirstLeafPath(firstMenu.children, currentPath);
// } else {
// return getFirstLeafPath(firstMenu.children, parentPath);
// }
// } else {
// return currentPath;
// }
// }