feat: v3.4.12 ticket iteration

Refund rollback, winery T+3, multi withdraw, mini-user store detail, dev plan batch edit and WeCom dispatch, support ticket edit/attachments/batch status, package imageUrl.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-04 23:53:31 +08:00
parent 71f508e02b
commit 4372018c09
34 changed files with 1032 additions and 86 deletions
@@ -1,14 +1,14 @@
import { useEffect, useState } from 'react';
import { Alert, Button, Form, Input, InputNumber, Space, Typography, message } from 'antd';
import { Alert, Button, Form, Input, InputNumber, Modal, Space, Typography, message } from 'antd';
import { DownOutlined, UpOutlined } from '@ant-design/icons';
import type { StorePackageItemDto } from '@dukang/shared-types';
import { STORE_PACKAGE_MAX_COUNT } from '@dukang/shared-types';
import { request } from '../lib/api';
import OssUpload from './OssUpload';
type PackageRow = StorePackageItemDto;
function emptyRow(index = 0): PackageRow {
return { name: '', price: '0', dishes: '', usableTime: '', otherNotes: '', sortOrder: index };
return { name: '', price: '0', dishes: '', usableTime: '', otherNotes: '', imageUrl: '', sortOrder: index };
}
export default function AdminStorePackagesSection({ storeId }: { storeId: string }) {
@@ -41,18 +41,33 @@ export default function AdminStorePackagesSection({ storeId }: { storeId: string
}
function removeAt(index: number) {
setItems((prev) => prev.filter((_, i) => i !== index).map((item, i) => ({ ...item, sortOrder: i })));
setCollapsed((prev) => {
const next: Record<number, boolean> = {};
Object.entries(prev).forEach(([k, v]) => {
const i = Number(k);
if (i < index) next[i] = v;
else if (i > index) next[i - 1] = v;
const run = () => {
setItems((prev) => {
const next = prev.filter((_, i) => i !== index).map((item, i) => ({ ...item, sortOrder: i }));
return next.length ? next : [];
});
return next;
});
setCollapsed((prev) => {
const next: Record<number, boolean> = {};
Object.entries(prev).forEach(([k, v]) => {
const i = Number(k);
if (i < index) next[i] = v;
else if (i > index) next[i - 1] = v;
});
return next;
});
};
if (items.length === 1) {
Modal.confirm({
title: '清空门店套餐',
content: '删除最后一条套餐后,该门店将无展示套餐,确认继续?',
okText: '确认删除',
cancelText: '取消',
onOk: run,
});
return;
}
run();
}
function toggleCollapse(index: number) {
setCollapsed((prev) => ({ ...prev, [index]: !prev[index] }));
}
@@ -65,6 +80,7 @@ export default function AdminStorePackagesSection({ storeId }: { storeId: string
dishes: item.dishes.trim(),
usableTime: item.usableTime?.trim() || null,
otherNotes: item.otherNotes?.trim() || null,
imageUrl: item.imageUrl?.trim() || null,
sortOrder: index,
}))
.filter((item) => item.name || item.dishes || item.price);
@@ -106,8 +122,21 @@ export default function AdminStorePackagesSection({ storeId }: { storeId: string
return <Typography.Text type="secondary"></Typography.Text>;
}
return (
<Form layout="vertical" requiredMark={false}>
if (!items.length) {
return (
<Form layout="vertical" requiredMark={false}>
<Alert type="info" showIcon style={{ marginBottom: 16 }} message="该门店暂无套餐,可添加或保存为空。" />
<Button onClick={addRow} style={{ marginBottom: 16 }}>
</Button>
<Button type="primary" loading={saving} onClick={() => void save()}>
</Button>
</Form>
);
}
return ( <Form layout="vertical" requiredMark={false}>
<Alert
type="info"
showIcon
@@ -140,12 +169,11 @@ export default function AdminStorePackagesSection({ storeId }: { storeId: string
{displayName}
</Typography.Title>
</Button>
{items.length > 1 ? (
{items.length > 0 ? (
<Button type="link" danger onClick={() => removeAt(index)}>
</Button>
) : null}
</Space>
) : null} </Space>
{!isCollapsed ? (
<>
@@ -186,8 +214,16 @@ export default function AdminStorePackagesSection({ storeId }: { storeId: string
/>
</Form.Item>
<Form.Item label="其他说明" style={{ marginBottom: 0 }}>
<Input
<Form.Item label="套餐图片" style={{ marginBottom: 12 }}>
<OssUpload
bizType="STORE_PACKAGE"
mediaType="IMAGE"
value={item.imageUrl || undefined}
onChange={(url) => updateAt(index, { imageUrl: url })}
/>
</Form.Item>
<Form.Item label="其他说明" style={{ marginBottom: 0 }}> <Input
placeholder="不可叠加"
value={item.otherNotes || ''}
onChange={(e) => updateAt(index, { otherNotes: e.target.value })}