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:
@@ -1,6 +1,6 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { Button, Image, Space, Typography, Upload, message } from 'antd';
|
||||
import { UploadOutlined, DeleteOutlined } from '@ant-design/icons';
|
||||
import { UploadOutlined, DeleteOutlined, FilePdfOutlined } from '@ant-design/icons';
|
||||
import type { UploadProps } from 'antd';
|
||||
import { uploadFileToOss, type OssMediaType } from '../lib/upload';
|
||||
|
||||
@@ -13,12 +13,18 @@ type Props = {
|
||||
maxCount?: number;
|
||||
tip?: string;
|
||||
accept?: string;
|
||||
/** 上传按钮文案,默认「批量上传图片」 */
|
||||
buttonText?: string;
|
||||
};
|
||||
|
||||
function normalizeUrls(value?: string[]) {
|
||||
return (value ?? []).map((u) => String(u || '').trim()).filter(Boolean);
|
||||
}
|
||||
|
||||
function isPdf(url: string) {
|
||||
return /\.pdf(\?|$)/i.test(url);
|
||||
}
|
||||
|
||||
/**
|
||||
* 多图批量上传(一次可选多张),用于套餐图 / 环境照 / 商品详情图等。
|
||||
* Form.Item 直接绑定 string[]。
|
||||
@@ -31,6 +37,7 @@ export default function MultiImageUpload({
|
||||
maxCount,
|
||||
tip,
|
||||
accept = 'image/*',
|
||||
buttonText = '批量上传图片',
|
||||
}: Props) {
|
||||
const urls = normalizeUrls(value);
|
||||
const urlsRef = useRef(urls);
|
||||
@@ -143,12 +150,36 @@ export default function MultiImageUpload({
|
||||
<Space wrap size={12}>
|
||||
{urls.map((url, index) => (
|
||||
<div key={`${url}-${index}`} style={{ position: 'relative', width: 96 }}>
|
||||
<Image
|
||||
src={url}
|
||||
width={96}
|
||||
height={96}
|
||||
style={{ objectFit: 'cover', borderRadius: 6, border: '1px solid #f0f0f0' }}
|
||||
/>
|
||||
{isPdf(url) ? (
|
||||
<a
|
||||
href={url}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
style={{
|
||||
display: 'flex',
|
||||
width: 96,
|
||||
height: 96,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
flexDirection: 'column',
|
||||
gap: 4,
|
||||
borderRadius: 6,
|
||||
border: '1px solid #f0f0f0',
|
||||
background: '#fafafa',
|
||||
fontSize: 12,
|
||||
}}
|
||||
>
|
||||
<FilePdfOutlined style={{ fontSize: 24, color: '#cf1322' }} />
|
||||
<span>PDF</span>
|
||||
</a>
|
||||
) : (
|
||||
<Image
|
||||
src={url}
|
||||
width={96}
|
||||
height={96}
|
||||
style={{ objectFit: 'cover', borderRadius: 6, border: '1px solid #f0f0f0' }}
|
||||
/>
|
||||
)}
|
||||
<Button
|
||||
type="text"
|
||||
danger
|
||||
@@ -176,7 +207,7 @@ export default function MultiImageUpload({
|
||||
disabled={uploading || !canAdd}
|
||||
>
|
||||
<Button icon={<UploadOutlined />} loading={uploading} disabled={!canAdd}>
|
||||
{canAdd ? '批量上传图片' : '已达上限'}
|
||||
{canAdd ? buttonText : '已达上限'}
|
||||
</Button>
|
||||
</Upload>
|
||||
</Space>
|
||||
|
||||
@@ -168,6 +168,12 @@ export type AdminUserRow = {
|
||||
createdAt: string;
|
||||
orderCount: number;
|
||||
isTest?: boolean;
|
||||
/** 好客权益·累计获得(含已使用,不含退款作废) */
|
||||
benefitTotalAmount?: number;
|
||||
/** 好客权益·已使用(已核销) */
|
||||
benefitUsedAmount?: number;
|
||||
/** 好客权益·剩余未使用 */
|
||||
benefitBalance?: number;
|
||||
};
|
||||
|
||||
export type AdminOrderItem = {
|
||||
|
||||
@@ -22,7 +22,8 @@ export type StoreCreateForm = {
|
||||
avgPrice?: number | null;
|
||||
coverUrl?: string;
|
||||
envPhotoUrls?: string[];
|
||||
contractUrl?: string;
|
||||
/** 签约合同,支持多张照片 / PDF */
|
||||
contractUrls?: string[];
|
||||
bankAccountName: string;
|
||||
bankAccountNo: string;
|
||||
bankBranch: string;
|
||||
|
||||
@@ -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