feat(release): v3.4.13 experience optimizations across admin, mini-user, and H5 login

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-05 22:34:29 +08:00
parent 490c26a7e1
commit 1c4b986924
29 changed files with 617 additions and 23 deletions
+33 -3
View File
@@ -13,11 +13,16 @@ type Row = {
settleAmount: number;
channel?: RedeemChannel;
createdAt: string;
user?: { userNo: string; phone: string | null };
user?: { userNo: string; phone: string | null; nickname?: string | null };
store?: { name: string; cityName: string };
coupon?: { couponNo: string };
};
function maskPhone(phone: string | null | undefined) {
if (!phone || phone.length < 7) return phone ?? '—';
return `${phone.slice(0, 3)}****${phone.slice(-4)}`;
}
export default function RedeemRecordsPage() {
const [form] = Form.useForm();
const [filters, setFilters] = useState<Record<string, string>>({});
@@ -50,7 +55,19 @@ export default function RedeemRecordsPage() {
);
},
},
{ title: '用户', dataIndex: ['user', 'userNo'], width: 110 },
{ title: '用户编号', dataIndex: ['user', 'userNo'], width: 110 },
{
title: '用户昵称',
dataIndex: ['user', 'nickname'],
width: 100,
render: (v: string | null | undefined) => v || '—',
},
{
title: '用户手机',
dataIndex: ['user', 'phone'],
width: 120,
render: (v: string | null | undefined) => maskPhone(v),
},
{ title: '门店', dataIndex: ['store', 'name'] },
{ title: '核销额', dataIndex: 'amount', width: 90, render: (v) => `¥${v}` },
{ title: '结算额', dataIndex: 'settleAmount', width: 90, render: (v) => `¥${v}` },
@@ -113,7 +130,7 @@ export default function RedeemRecordsPage() {
loading={loading}
columns={columns}
dataSource={data?.items ?? []}
scroll={{ x: 1100 }}
scroll={{ x: 1300 }}
pagination={{
current: page,
pageSize,
@@ -138,6 +155,19 @@ export default function RedeemRecordsPage() {
</Descriptions.Item>
<Descriptions.Item label="核销额">¥{String(detail.amount)}</Descriptions.Item>
<Descriptions.Item label="结算额">¥{String(detail.settleAmount)}</Descriptions.Item>
{detail.user && typeof detail.user === 'object' ? (
<>
<Descriptions.Item label="用户编号">
{String((detail.user as { userNo?: string }).userNo ?? '—')}
</Descriptions.Item>
<Descriptions.Item label="用户昵称">
{String((detail.user as { nickname?: string | null }).nickname ?? '—')}
</Descriptions.Item>
<Descriptions.Item label="用户手机">
{maskPhone((detail.user as { phone?: string | null }).phone)}
</Descriptions.Item>
</>
) : null}
<Descriptions.Item label="时间">{fmtTime(String(detail.createdAt))}</Descriptions.Item>
</Descriptions>
)}
@@ -30,6 +30,7 @@ import {
DEV_PLAN_TASK_TYPE_LABELS,
SUPPORT_TICKET_STATUS_LABELS,
SUPPORT_TICKET_TYPE_LABELS,
SUPPORT_TICKET_PRIORITY_LABELS,
mapSupportTicketTypeToDevPlanTask,
type BatchReviewPreviewItem,
type BatchReviewPreviewResponse,
@@ -37,6 +38,7 @@ import {
type DevPlanVersionDto,
type SupportTicketDto,
type SupportTicketLinkedTaskDto,
type SupportTicketPriorityDto,
type SupportTicketStatusDto,
type SupportTicketTypeDto,
} from '@dukang/shared-types';
@@ -64,6 +66,17 @@ const STATUS_OPTIONS = (Object.keys(SUPPORT_TICKET_STATUS_LABELS) as SupportTick
(value) => ({ value, label: SUPPORT_TICKET_STATUS_LABELS[value] }),
);
const PRIORITY_COLOR: Record<SupportTicketPriorityDto, string> = {
LOW: 'default',
NORMAL: 'blue',
HIGH: 'orange',
URGENT: 'red',
};
const PRIORITY_OPTIONS = (Object.keys(SUPPORT_TICKET_PRIORITY_LABELS) as SupportTicketPriorityDto[]).map(
(value) => ({ value, label: SUPPORT_TICKET_PRIORITY_LABELS[value] }),
);
const TASK_TYPE_OPTIONS = (Object.keys(DEV_PLAN_TASK_TYPE_LABELS) as DevPlanTaskTypeDto[]).map(
(v) => ({ value: v, label: DEV_PLAN_TASK_TYPE_LABELS[v] }),
);
@@ -80,6 +93,7 @@ export default function SupportTicketsPage() {
const qs = new URLSearchParams();
if (filters.ticketType) qs.set('ticketType', filters.ticketType);
if (filters.status) qs.set('status', filters.status);
if (filters.priority) qs.set('priority', filters.priority);
return qs;
},
[filters],
@@ -154,6 +168,7 @@ export default function SupportTicketsPage() {
title: values.title.trim(),
content: values.content?.trim() || undefined,
remark: values.remark?.trim() || undefined,
priority: values.priority,
attachmentUrls: (values.attachmentUrls ?? []).map((u: string) => u?.trim()).filter(Boolean),
}),
});
@@ -217,6 +232,7 @@ export default function SupportTicketsPage() {
title: detail.title,
content: detail.content || '',
remark: detail.remark || '',
priority: detail.priority ?? 'NORMAL',
attachmentUrls: detail.attachmentUrls?.length ? detail.attachmentUrls : [''],
});
setEditOpen(true);
@@ -233,6 +249,7 @@ export default function SupportTicketsPage() {
title: values.title.trim(),
content: values.content?.trim() || undefined,
remark: values.remark?.trim() || undefined,
priority: values.priority,
attachmentUrls: (values.attachmentUrls ?? []).map((u: string) => u?.trim()).filter(Boolean),
}),
});
@@ -451,6 +468,16 @@ export default function SupportTicketsPage() {
width: 90,
render: (t: SupportTicketTypeDto) => SUPPORT_TICKET_TYPE_LABELS[t] ?? t,
},
{
title: '优先级',
dataIndex: 'priority',
width: 90,
render: (p: SupportTicketPriorityDto) => (
<Tag color={PRIORITY_COLOR[p] ?? 'default'}>
{SUPPORT_TICKET_PRIORITY_LABELS[p] ?? p ?? '普通'}
</Tag>
),
},
{
title: '状态',
dataIndex: 'status',
@@ -562,6 +589,9 @@ export default function SupportTicketsPage() {
<Form.Item name="status" label="状态">
<Select allowClear style={{ width: 120 }} options={STATUS_OPTIONS} />
</Form.Item>
<Form.Item name="priority" label="优先级">
<Select allowClear style={{ width: 120 }} options={PRIORITY_OPTIONS} />
</Form.Item>
<Button type="primary" htmlType="submit">
</Button>
@@ -603,6 +633,9 @@ export default function SupportTicketsPage() {
{detail.ticketNo}
</Typography.Text>
<Tag>{SUPPORT_TICKET_TYPE_LABELS[detail.ticketType] ?? detail.ticketType}</Tag>
<Tag color={PRIORITY_COLOR[detail.priority ?? 'NORMAL']}>
{SUPPORT_TICKET_PRIORITY_LABELS[detail.priority ?? 'NORMAL']}
</Tag>
</>
)}
</Space>
@@ -861,10 +894,13 @@ export default function SupportTicketsPage() {
</Drawer>
<Modal title="创建技术支持工单" open={createOpen} onCancel={() => setCreateOpen(false)} onOk={() => void submitCreate()} confirmLoading={creating} destroyOnClose okText="提交">
<Form form={createForm} layout="vertical" initialValues={{ ticketType: 'BUG', attachmentUrls: [''] }}>
<Form form={createForm} layout="vertical" initialValues={{ ticketType: 'BUG', priority: 'NORMAL', attachmentUrls: [''] }}>
<Form.Item name="ticketType" label="类型" rules={[{ required: true }]}>
<Select options={TYPE_OPTIONS} />
</Form.Item>
<Form.Item name="priority" label="优先级" rules={[{ required: true }]}>
<Select options={PRIORITY_OPTIONS} />
</Form.Item>
<Form.Item name="title" label="标题" rules={[{ required: true }]}>
<Input maxLength={128} showCount />
</Form.Item>
@@ -968,6 +1004,9 @@ export default function SupportTicketsPage() {
<Form.Item name="ticketType" label="类型" rules={[{ required: true }]}>
<Select options={TYPE_OPTIONS} />
</Form.Item>
<Form.Item name="priority" label="优先级" rules={[{ required: true }]}>
<Select options={PRIORITY_OPTIONS} />
</Form.Item>
<Form.Item name="title" label="标题" rules={[{ required: true }]}>
<Input maxLength={128} showCount />
</Form.Item>
@@ -212,6 +212,14 @@ export default function PromoCodeDetailPage() {
<Statistic title="扫码进入次数" value={stats?.scanCount ?? detail.scanCount} />
</Card>
</Col>
<Col xs={12} sm={6}>
<Card size="small">
<Statistic
title="归因用户数"
value={stats?.attributionCount ?? 0}
/>
</Card>
</Col>
<Col xs={12} sm={6}>
<Card size="small">
<Statistic