feat(ops,store,partner): 用户权益列 + 门店合同多图上传

- admin 用户列表新增剩余/已用/累计好客权益金额三列(benefitCoupon groupBy 聚合,排除 VOID)
- admin/partner 门店入驻合同支持多张照片与 PDF(CommonResource 多记录,复用 ENV 多图逻辑)
- 新增 contract-urls.util 归一化工具,向后兼容旧 contractUrl 字段并标记 deprecated

需求1/2/3
This commit is contained in:
2026-08-12 22:48:02 +08:00
parent 478291e2b3
commit 88053353ab
13 changed files with 356 additions and 59 deletions
+35 -9
View File
@@ -116,7 +116,7 @@ function StoreAuditMediaEditor() {
type="info"
showIcon
style={{ marginBottom: 16 }}
message="可替换或删除门头照 / 环境照 / 签约合同,点击右上角「保存修改」后生效。环境照最多 20 张。套餐请在「套餐」页签编辑,同样由「保存修改」一并提交。"
message="可替换或删除门头照 / 环境照 / 签约合同,点击右上角「保存修改」后生效。环境照、签约合同均最多 20 张。套餐请在「套餐」页签编辑,同样由「保存修改」一并提交。"
/>
<Form.Item name="coverUrl" label="门头照">
<OssUpload bizType="STORE_TITLE" mediaType="IMAGE" />
@@ -133,8 +133,20 @@ function StoreAuditMediaEditor() {
tip="环境照支持一次选择多张批量上传"
/>
</Form.Item>
<Form.Item name="contractUrl" label="签约合同" style={{ marginTop: 16 }}>
<OssUpload bizType="STORE_CONTRACT" mediaType="FILE" accept="image/*,.pdf" />
<Form.Item
name="contractUrls"
label="签约合同"
style={{ marginTop: 16 }}
extra="支持多张合同照片(如首页、盖章页),也可上传 PDF,最多 20 个。"
>
<MultiImageUpload
bizType="STORE_CONTRACT"
mediaType="FILE"
accept="image/*,.pdf"
maxCount={20}
buttonText="批量上传合同"
tip="合同支持一次选择多张照片批量上传,最多 20 个"
/>
</Form.Item>
</div>
);
@@ -393,9 +405,9 @@ export default function StoresPage() {
const urls = envs.map((item) => item.url).filter(Boolean);
return urls;
})(),
contractUrl: (() => {
contractUrls: (() => {
const { contracts } = collectMediaUrls(d);
return contracts[0]?.url || '';
return contracts.map((item) => item.url).filter(Boolean);
})(),
province: d.province,
city: d.cityName,
@@ -442,7 +454,9 @@ export default function StoresPage() {
envPhotoUrls: Array.isArray(v.envPhotoUrls)
? v.envPhotoUrls.map((u: string) => String(u || '').trim()).filter(Boolean)
: [],
contractUrl: v.contractUrl ?? '',
contractUrls: Array.isArray(v.contractUrls)
? v.contractUrls.map((u: string) => String(u || '').trim()).filter(Boolean)
: [],
intro: v.intro,
benefitUsageRule:
typeof v.benefitUsageRule === 'string' &&
@@ -605,6 +619,7 @@ export default function StoresPage() {
}
const envPhotoUrls = (values.envPhotoUrls ?? []).map((u: string) => u?.trim()).filter(Boolean) as string[];
const contractUrls = (values.contractUrls ?? []).map((u: string) => u?.trim()).filter(Boolean) as string[];
await request('/admin/stores', {
method: 'POST',
body: JSON.stringify({
@@ -639,7 +654,7 @@ export default function StoresPage() {
: {}),
coverUrl: values.coverUrl?.trim() || undefined,
envPhotoUrls: envPhotoUrls.length ? envPhotoUrls : undefined,
contractUrl: values.contractUrl?.trim() || undefined,
contractUrls: contractUrls.length ? contractUrls : undefined,
bankAccountName: values.bankAccountName.trim(),
bankAccountNo: values.bankAccountNo.replace(/\s/g, ''),
bankBranch: values.bankBranch.trim(),
@@ -1340,8 +1355,19 @@ export default function StoresPage() {
tip="环境照支持一次选择多张批量上传"
/>
</Form.Item>
<Form.Item name="contractUrl" label="签约合同">
<OssUpload bizType="STORE_CONTRACT" mediaType="FILE" accept="image/*,.pdf" />
<Form.Item
name="contractUrls"
label="签约合同"
extra="选填;支持多张合同照片或 PDF,最多 20 个"
>
<MultiImageUpload
bizType="STORE_CONTRACT"
mediaType="FILE"
accept="image/*,.pdf"
maxCount={20}
buttonText="批量上传合同"
tip="合同支持一次选择多张照片批量上传,最多 20 个"
/>
</Form.Item>
</div>
<div style={{ display: createStep === 2 ? 'block' : 'none' }}>
+53 -1
View File
@@ -30,6 +30,13 @@ type UserOrderRow = {
createdAt: string;
};
/** 好客权益金额展示:0 与空值统一显示占位,避免整列都是 ¥0.00 干扰 */
function fmtBenefit(v: number | null | undefined) {
const n = Number(v ?? 0);
if (!Number.isFinite(n) || n <= 0) return '—';
return `¥${n.toFixed(2)}`;
}
type UserBehaviorLog = {
id: string;
eventName: string;
@@ -315,6 +322,39 @@ export default function UsersPage() {
render: (v) => (v ? <Tag color="blue"></Tag> : '—'),
},
{ title: '订单数', dataIndex: 'orderCount', width: 80 },
{
title: '剩余权益',
dataIndex: 'benefitBalance',
width: 110,
align: 'right',
sorter: (a, b) => Number(a.benefitBalance ?? 0) - Number(b.benefitBalance ?? 0),
render: (v: number | undefined) =>
Number(v ?? 0) > 0 ? (
<Typography.Text strong style={{ color: '#cf1322' }}>
{fmtBenefit(v)}
</Typography.Text>
) : (
<Typography.Text type="secondary"></Typography.Text>
),
},
{
title: '已用权益',
dataIndex: 'benefitUsedAmount',
width: 110,
align: 'right',
sorter: (a, b) => Number(a.benefitUsedAmount ?? 0) - Number(b.benefitUsedAmount ?? 0),
render: (v: number | undefined) => (
<Typography.Text type="secondary">{fmtBenefit(v)}</Typography.Text>
),
},
{
title: '累计权益',
dataIndex: 'benefitTotalAmount',
width: 110,
align: 'right',
sorter: (a, b) => Number(a.benefitTotalAmount ?? 0) - Number(b.benefitTotalAmount ?? 0),
render: (v: number | undefined) => fmtBenefit(v),
},
{
title: '注册时间',
dataIndex: 'createdAt',
@@ -389,7 +429,7 @@ export default function UsersPage() {
loading={loading}
columns={columns}
dataSource={data?.items ?? []}
scroll={{ x: 1500 }}
scroll={{ x: 1830 }}
rowSelection={canDeleteUsers ? {
selectedRowKeys,
preserveSelectedRowKeys: true,
@@ -461,6 +501,18 @@ export default function UsersPage() {
</Descriptions.Item>
<Descriptions.Item label="被合并访客数">{detail.mergedFromCount ?? 0}</Descriptions.Item>
<Descriptions.Item label="订单/地址">{detail.orderCount} / {detail.addressCount}</Descriptions.Item>
<Descriptions.Item label="好客权益">
<Space size={16} wrap>
<span>
{' '}
<Typography.Text strong style={{ color: '#cf1322' }}>
{fmtBenefit(detail.benefitBalance)}
</Typography.Text>
</span>
<span> {fmtBenefit(detail.benefitUsedAmount)}</span>
<span> {fmtBenefit(detail.benefitTotalAmount)}</span>
</Space>
</Descriptions.Item>
<Descriptions.Item label="注册时间">
{new Date(detail.createdAt).toLocaleString('zh-CN')}
</Descriptions.Item>