merge(dev_jacy): admin redeem detail UI (v3.4.13)
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
import { Descriptions } from 'antd';
|
||||
import { REDEEM_CHANNEL_LABELS, type RedeemChannel } from '@dukang/shared-types';
|
||||
import { fmtTime } from '../lib/constants';
|
||||
|
||||
function maskPhone(phone: string | null | undefined) {
|
||||
if (!phone || phone.length < 7) return phone ?? '—';
|
||||
return `${phone.slice(0, 3)}****${phone.slice(-4)}`;
|
||||
}
|
||||
|
||||
type Props = {
|
||||
detail: Record<string, unknown>;
|
||||
/** 列表页已脱敏展示时可设为 false,详情抽屉展示完整手机号 */
|
||||
maskUserPhone?: boolean;
|
||||
};
|
||||
|
||||
export default function RedeemRecordDetailDescriptions({ detail, maskUserPhone = false }: Props) {
|
||||
const user = detail.user as
|
||||
| { userNo?: string; nickname?: string | null; phone?: string | null }
|
||||
| undefined;
|
||||
const store = detail.store as
|
||||
| {
|
||||
name?: string;
|
||||
cityName?: string;
|
||||
address?: string;
|
||||
partnerAccount?: { companyName?: string };
|
||||
}
|
||||
| undefined;
|
||||
const coupon = detail.coupon as { couponNo?: string; order?: { orderNo?: string } } | undefined;
|
||||
const channel = detail.channel === 'PHONE' ? 'PHONE' : 'SCAN';
|
||||
|
||||
return (
|
||||
<Descriptions column={1} bordered size="small">
|
||||
<Descriptions.Item label="核销号">{String(detail.redeemNo ?? '—')}</Descriptions.Item>
|
||||
<Descriptions.Item label="方式">{REDEEM_CHANNEL_LABELS[channel as RedeemChannel]}</Descriptions.Item>
|
||||
<Descriptions.Item label="核销额">¥{Number(detail.amount ?? 0).toFixed(2)}</Descriptions.Item>
|
||||
<Descriptions.Item label="结算额">¥{Number(detail.settleAmount ?? 0).toFixed(2)}</Descriptions.Item>
|
||||
<Descriptions.Item label="时间">{fmtTime(String(detail.createdAt ?? ''))}</Descriptions.Item>
|
||||
<Descriptions.Item label="用户编号">{user?.userNo ?? '—'}</Descriptions.Item>
|
||||
<Descriptions.Item label="用户昵称">{user?.nickname?.trim() || '—'}</Descriptions.Item>
|
||||
<Descriptions.Item label="用户手机">
|
||||
{maskUserPhone ? maskPhone(user?.phone) : user?.phone || '—'}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="门店">
|
||||
{store?.name ?? '—'}
|
||||
{store?.cityName ? `(${store.cityName})` : ''}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="门店地址">{store?.address ?? '—'}</Descriptions.Item>
|
||||
<Descriptions.Item label="合伙人">{store?.partnerAccount?.companyName ?? '—'}</Descriptions.Item>
|
||||
<Descriptions.Item label="权益券号">{coupon?.couponNo ?? '—'}</Descriptions.Item>
|
||||
<Descriptions.Item label="关联订单">{coupon?.order?.orderNo ?? '—'}</Descriptions.Item>
|
||||
{Array.isArray(detail.allocations) && (detail.allocations as unknown[]).length > 0 ? (
|
||||
<Descriptions.Item label="券分摊">
|
||||
{(
|
||||
detail.allocations as Array<{
|
||||
couponNo?: string;
|
||||
orderNo?: string | null;
|
||||
amount?: number;
|
||||
}>
|
||||
)
|
||||
.map((a, idx) => {
|
||||
const role = idx === 0 ? '主' : '次';
|
||||
return `${role} ${a.couponNo ?? '—'}¥${Number(a.amount ?? 0).toFixed(2)}${
|
||||
a.orderNo ? `(订单 ${a.orderNo})` : ''
|
||||
}`;
|
||||
})
|
||||
.join(';')}
|
||||
</Descriptions.Item>
|
||||
) : null}
|
||||
{detail.payout ? (
|
||||
<Descriptions.Item label="门店结算单">
|
||||
¥{Number((detail.payout as { settleAmount?: number }).settleAmount ?? 0).toFixed(2)}
|
||||
{' / '}
|
||||
{String((detail.payout as { status?: string }).status ?? '—')}
|
||||
</Descriptions.Item>
|
||||
) : null}
|
||||
{detail.rating ? (
|
||||
<Descriptions.Item label="评价">
|
||||
服务 {(detail.rating as { serviceScore?: number }).serviceScore ?? '—'} 分 / 环境{' '}
|
||||
{(detail.rating as { envScore?: number }).envScore ?? '—'} 分
|
||||
</Descriptions.Item>
|
||||
) : null}
|
||||
</Descriptions>
|
||||
);
|
||||
}
|
||||
@@ -18,6 +18,7 @@ import type { ColumnsType } from 'antd/es/table';
|
||||
import type { AdminBenefitGrantRequest } from '@dukang/shared-types';
|
||||
import { request } from '../lib/api';
|
||||
import { COUPON_STATUS_LABELS, fmtTime } from '../lib/constants';
|
||||
import RedeemRecordDetailDescriptions from '../components/RedeemRecordDetailDescriptions';
|
||||
import { useAdminList } from '../lib/useAdminList';
|
||||
|
||||
type Row = {
|
||||
@@ -242,7 +243,8 @@ export default function BenefitCouponsPage() {
|
||||
|
||||
<Drawer
|
||||
title="权益券详情"
|
||||
width={720}
|
||||
width={980}
|
||||
styles={{ body: { paddingBottom: 24 } }}
|
||||
open={drawerOpen}
|
||||
onClose={() => setDrawerOpen(false)}
|
||||
extra={
|
||||
@@ -310,14 +312,15 @@ export default function BenefitCouponsPage() {
|
||||
size="small"
|
||||
rowKey="id"
|
||||
pagination={false}
|
||||
tableLayout="fixed"
|
||||
locale={{ emptyText: '暂无关联核销单' }}
|
||||
dataSource={detail.redeemRecords ?? []}
|
||||
columns={[
|
||||
{ title: '核销号', dataIndex: 'redeemNo', width: 160, ellipsis: true },
|
||||
{ title: '核销号', dataIndex: 'redeemNo', ellipsis: true },
|
||||
{
|
||||
title: '角色',
|
||||
dataIndex: 'role',
|
||||
width: 70,
|
||||
width: 72,
|
||||
render: (v: string | undefined) =>
|
||||
v === 'SECONDARY' ? <Tag color="orange">次券</Tag> : <Tag color="blue">主券</Tag>,
|
||||
},
|
||||
@@ -331,31 +334,31 @@ export default function BenefitCouponsPage() {
|
||||
{
|
||||
title: '本券分摊',
|
||||
dataIndex: 'couponAmount',
|
||||
width: 95,
|
||||
width: 96,
|
||||
render: (v: number | undefined, row: CouponRedeemRecord) =>
|
||||
`¥${Number(v ?? row.amount).toFixed(2)}`,
|
||||
},
|
||||
{
|
||||
title: '核销总额',
|
||||
dataIndex: 'amount',
|
||||
width: 90,
|
||||
width: 96,
|
||||
render: (v: number) => `¥${Number(v).toFixed(2)}`,
|
||||
},
|
||||
{
|
||||
title: '结算额',
|
||||
dataIndex: 'settleAmount',
|
||||
width: 90,
|
||||
width: 96,
|
||||
render: (v: number) => `¥${Number(v).toFixed(2)}`,
|
||||
},
|
||||
{
|
||||
title: '时间',
|
||||
dataIndex: 'createdAt',
|
||||
width: 150,
|
||||
width: 148,
|
||||
render: (v: string) => fmtTime(v),
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
width: 70,
|
||||
width: 64,
|
||||
render: (_: unknown, row: CouponRedeemRecord) => (
|
||||
<Button type="link" size="small" onClick={() => void openRedeemDetail(row.id)}>
|
||||
详情
|
||||
@@ -374,7 +377,7 @@ export default function BenefitCouponsPage() {
|
||||
|
||||
<Drawer
|
||||
title="核销单详情"
|
||||
width={520}
|
||||
width={640}
|
||||
open={redeemDrawerOpen}
|
||||
onClose={() => {
|
||||
setRedeemDrawerOpen(false);
|
||||
@@ -385,79 +388,7 @@ export default function BenefitCouponsPage() {
|
||||
{redeemDetailLoading ? (
|
||||
<Typography.Text type="secondary">加载中…</Typography.Text>
|
||||
) : redeemDetail ? (
|
||||
<Descriptions column={1} bordered size="small">
|
||||
<Descriptions.Item label="核销号">{String(redeemDetail.redeemNo ?? '—')}</Descriptions.Item>
|
||||
<Descriptions.Item label="核销额">
|
||||
¥{Number(redeemDetail.amount ?? 0).toFixed(2)}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="结算额">
|
||||
¥{Number(redeemDetail.settleAmount ?? 0).toFixed(2)}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="时间">{fmtTime(String(redeemDetail.createdAt ?? ''))}</Descriptions.Item>
|
||||
<Descriptions.Item label="用户">
|
||||
{String((redeemDetail.user as { userNo?: string } | undefined)?.userNo ?? '—')}
|
||||
{(redeemDetail.user as { phone?: string | null } | undefined)?.phone
|
||||
? ` / ${(redeemDetail.user as { phone?: string | null }).phone}`
|
||||
: ''}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="门店">
|
||||
{String((redeemDetail.store as { name?: string } | undefined)?.name ?? '—')}
|
||||
{(redeemDetail.store as { cityName?: string } | undefined)?.cityName
|
||||
? `(${(redeemDetail.store as { cityName?: string }).cityName})`
|
||||
: ''}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="门店地址">
|
||||
{String((redeemDetail.store as { address?: string } | undefined)?.address ?? '—')}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="合伙人">
|
||||
{String(
|
||||
(redeemDetail.store as { partnerAccount?: { companyName?: string } } | undefined)
|
||||
?.partnerAccount?.companyName ?? '—',
|
||||
)}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="权益券号">
|
||||
{String((redeemDetail.coupon as { couponNo?: string } | undefined)?.couponNo ?? '—')}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="关联订单">
|
||||
{String(
|
||||
(redeemDetail.coupon as { order?: { orderNo?: string } } | undefined)?.order?.orderNo ??
|
||||
'—',
|
||||
)}
|
||||
</Descriptions.Item>
|
||||
{Array.isArray(redeemDetail.allocations) &&
|
||||
(redeemDetail.allocations as unknown[]).length > 0 ? (
|
||||
<Descriptions.Item label="券分摊">
|
||||
{(
|
||||
redeemDetail.allocations as Array<{
|
||||
couponNo?: string;
|
||||
orderNo?: string | null;
|
||||
amount?: number;
|
||||
sortOrder?: number;
|
||||
}>
|
||||
)
|
||||
.map((a, idx) => {
|
||||
const role = idx === 0 ? '主' : '次';
|
||||
return `${role} ${a.couponNo ?? '—'}¥${Number(a.amount ?? 0).toFixed(2)}${
|
||||
a.orderNo ? `(订单 ${a.orderNo})` : ''
|
||||
}`;
|
||||
})
|
||||
.join(';')}
|
||||
</Descriptions.Item>
|
||||
) : null}
|
||||
{redeemDetail.payout ? (
|
||||
<Descriptions.Item label="门店结算单">
|
||||
¥{Number((redeemDetail.payout as { settleAmount?: number }).settleAmount ?? 0).toFixed(2)}
|
||||
{' / '}
|
||||
{String((redeemDetail.payout as { status?: string }).status ?? '—')}
|
||||
</Descriptions.Item>
|
||||
) : null}
|
||||
{redeemDetail.rating ? (
|
||||
<Descriptions.Item label="评价">
|
||||
服务 {(redeemDetail.rating as { serviceScore?: number }).serviceScore ?? '—'} 分 / 环境{' '}
|
||||
{(redeemDetail.rating as { envScore?: number }).envScore ?? '—'} 分
|
||||
</Descriptions.Item>
|
||||
) : null}
|
||||
</Descriptions>
|
||||
<RedeemRecordDetailDescriptions detail={redeemDetail} />
|
||||
) : null}
|
||||
</Drawer>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { useState } from 'react';
|
||||
import { Button, Descriptions, Drawer, Form, Input, Select, Table, Tag, Typography } from 'antd';
|
||||
import { Button, Drawer, Form, Input, Select, Table, Tag, Typography } from 'antd';
|
||||
import type { ColumnsType } from 'antd/es/table';
|
||||
import { REDEEM_CHANNEL_LABELS, type RedeemChannel } from '@dukang/shared-types';
|
||||
import RedeemRecordDetailDescriptions from '../components/RedeemRecordDetailDescriptions';
|
||||
import { request } from '../lib/api';
|
||||
import { fmtTime } from '../lib/constants';
|
||||
import { useAdminList } from '../lib/useAdminList';
|
||||
@@ -39,6 +40,17 @@ export default function RedeemRecordsPage() {
|
||||
);
|
||||
const [detail, setDetail] = useState<Record<string, unknown> | null>(null);
|
||||
const [drawerOpen, setDrawerOpen] = useState(false);
|
||||
const [detailLoading, setDetailLoading] = useState(false);
|
||||
|
||||
async function openDetail(id: string) {
|
||||
setDetailLoading(true);
|
||||
setDrawerOpen(true);
|
||||
try {
|
||||
setDetail(await request(`/admin/redeem-records/${id}`));
|
||||
} finally {
|
||||
setDetailLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
const columns: ColumnsType<Row> = [
|
||||
{ title: '核销号', dataIndex: 'redeemNo', width: 180 },
|
||||
@@ -77,14 +89,7 @@ export default function RedeemRecordsPage() {
|
||||
title: '操作',
|
||||
width: 80,
|
||||
render: (_, row) => (
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
onClick={async () => {
|
||||
setDetail(await request(`/admin/redeem-records/${row.id}`));
|
||||
setDrawerOpen(true);
|
||||
}}
|
||||
>
|
||||
<Button type="link" size="small" onClick={() => void openDetail(row.id)}>
|
||||
详情
|
||||
</Button>
|
||||
),
|
||||
@@ -142,35 +147,21 @@ export default function RedeemRecordsPage() {
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<Drawer title="核销详情" width={520} open={drawerOpen} onClose={() => setDrawerOpen(false)}>
|
||||
{detail && (
|
||||
<Descriptions column={1} bordered size="small">
|
||||
<Descriptions.Item label="核销号">{String(detail.redeemNo)}</Descriptions.Item>
|
||||
<Descriptions.Item label="方式">
|
||||
{
|
||||
REDEEM_CHANNEL_LABELS[
|
||||
(detail.channel === 'PHONE' ? 'PHONE' : 'SCAN') as RedeemChannel
|
||||
]
|
||||
}
|
||||
</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>
|
||||
)}
|
||||
<Drawer
|
||||
title="核销详情"
|
||||
width={640}
|
||||
open={drawerOpen}
|
||||
onClose={() => {
|
||||
setDrawerOpen(false);
|
||||
setDetail(null);
|
||||
}}
|
||||
destroyOnClose
|
||||
>
|
||||
{detailLoading ? (
|
||||
<Typography.Text type="secondary">加载中…</Typography.Text>
|
||||
) : detail ? (
|
||||
<RedeemRecordDetailDescriptions detail={detail} />
|
||||
) : null}
|
||||
</Drawer>
|
||||
</div>
|
||||
);
|
||||
|
||||
+1
-1
@@ -48,7 +48,7 @@
|
||||
|
||||
| 版本 | 日期 | 说明 |
|
||||
|------|------|------|
|
||||
| **3.4.13** | 2026-08-05 | 推广码归因统计 + **指标事件日志/高峰趋势**、核销用户信息、技术支持工单优先级、mini-user **我的页版本号/低于 min 强制退出**、门店/商品/提货/**物流增强(签收照/拨号/ETA/路由回调签收→已完成)**、H5 登录校验、合伙人微信暂停禁登、**OSS 图片超 10MB 客户端压缩**;开发设计见 [`杜康好客-v3.4.13-体验优化开发文档.md`](./杜康好客-v3.4.13-体验优化开发文档.md) |
|
||||
| **3.4.13** | 2026-08-05 | 推广码归因统计 + **指标事件日志/高峰趋势**、核销用户信息 + **权益券详情加宽/核销详情增强**、技术支持工单优先级、mini-user **我的页版本号/低于 min 强制退出**、门店/商品/提货/**物流增强(签收照/拨号/ETA/路由回调签收→已完成)**、H5 登录校验、合伙人微信暂停禁登、**OSS 图片超 10MB 客户端压缩**;开发设计见 [`杜康好客-v3.4.13-体验优化开发文档.md`](./杜康好客-v3.4.13-体验优化开发文档.md) |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
| 模块 | 内容 |
|
||||
|------|------|
|
||||
| admin-web | 推广码 attributionCount + **指标事件日志/ECharts 趋势**;核销记录用户信息;技术支持工单优先级;**大图上传前压缩** |
|
||||
| admin-web | 推广码 attributionCount + **指标事件日志/ECharts 趋势**;**权益券详情加宽**;**核销记录详情增强(对齐权益券内核销详情)**;技术支持工单优先级;**大图上传前压缩** |
|
||||
| mini-user | 门店电话脱敏+拨打埋点;门头/套餐展示;商品去分享+首图 preview;提货确认弹框;**我的页版本号 v3.4.x**;**低于 minClientVersion 强制更新/点我知道了退出**;**物流增强(签收照/拨号/时间线/ETA)**;**头像超 10MB 压缩** |
|
||||
| h5-partner / h5-shop / h5-user | 登录 phone/code 前端校验;**OSS 图片超 10MB 自动 canvas 压缩后上传** |
|
||||
| 后端 | 工单 priority;client-config minClientVersion;合伙人微信暂停禁登;**Courier 适配器**;**log_promo_event 推广码指标日志** |
|
||||
@@ -24,7 +24,7 @@
|
||||
| ST1785906800359657 | 合伙人暂停后禁登 | ✅ loginPartnerWechat status |
|
||||
| ST1785906592340255 | PARTNER_H5 login 校验 | ✅ 前端空字段拦截 |
|
||||
| ST1785906390321653 | 现场提货提交确认弹框 | ✅ showModal |
|
||||
| ST1785905773501871 | 核销记录用户信息 | ✅ 列表+详情 |
|
||||
| ST1785905773501871 | 核销记录用户信息 | ✅ 列表+详情;**详情含门店/合伙人/券分摊/结算/评价** |
|
||||
| ST1785904849234806 | 工单中心 | ✅ 已有 |
|
||||
| ST1785904076632841 | 门店列表开城合伙人 | ✅ 已有 |
|
||||
| ST1785902173093977 | 去掉商品详情分享按钮 | ✅ 移除 ShareNavButton |
|
||||
@@ -145,7 +145,8 @@ POST https://api-test.dukanghaoke.com/api/v1/callbacks/courier/xfx/track
|
||||
|
||||
- [ ] 推广码详情展示 attributionCount;**四指标事件日志 + ECharts 按日/按时趋势 + 高峰标注**
|
||||
- [ ] 推广码 SCAN/ATTRIBUTION/REGISTER/ORDER 事件含 time + IP;幂等(重复 touch 不计 SCAN)
|
||||
- [ ] 核销记录含 userNo/nickname/phone
|
||||
- [ ] 核销记录含 userNo/nickname/phone;**详情抽屉展示门店/地址/合伙人/权益券号/关联订单/券分摊/结算单/评价**
|
||||
- [ ] **权益券详情抽屉加宽(980px),内嵌核销列表无横向滚动条**
|
||||
- [ ] 技术支持可创建/筛选/编辑优先级
|
||||
- [ ] 合伙人/门店登录空字段前端提示;暂停合伙人微信登录被拒
|
||||
- [ ] mini-user:我的页左下角显示 `杜康好客 v3.4.x`;低于 `minClientVersion` 时强制更新,无新包则点「我知道了」退出小程序
|
||||
|
||||
Reference in New Issue
Block a user