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:
@@ -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' }}>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user