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
@@ -13,12 +13,20 @@ type Props = {
maxCount?: number;
disabled?: boolean;
label?: string;
/** 系统文件选择器的 accept,默认仅图片 */
accept?: string;
/** 计量单位文案,如「张」「个」 */
unit?: string;
};
function isCancelError(msg: string): boolean {
return /cancel|取消/i.test(msg);
}
function isPdf(url: string): boolean {
return /\.pdf(\?|$)/i.test(url);
}
function normalizeUrls(value?: string[]) {
return (value ?? []).map((u) => String(u || '').trim()).filter(Boolean);
}
@@ -32,6 +40,8 @@ export default function MultiOssUploadField({
maxCount = 20,
disabled,
label,
accept = 'image/*',
unit = '张',
}: Props) {
const inputRef = useRef<HTMLInputElement>(null);
const pickingRef = useRef(false);
@@ -67,7 +77,7 @@ export default function MultiOssUploadField({
const room = Math.max(0, maxCount - current.length);
const picked = files.slice(0, room);
if (!picked.length) {
showUploadError(`最多 ${maxCount} `);
showUploadError(`最多 ${maxCount} ${unit}`);
return;
}
setUploading(true);
@@ -83,7 +93,7 @@ export default function MultiOssUploadField({
const next = [...urlsRef.current, ...appended];
urlsRef.current = next;
onChangeRef.current?.(next);
toastSuccess(`已上传 ${appended.length} `);
toastSuccess(`已上传 ${appended.length} ${unit}`);
}
} catch (e) {
showUploadError(e instanceof Error ? e.message : '上传失败');
@@ -126,7 +136,7 @@ export default function MultiOssUploadField({
<input
ref={inputRef}
type="file"
accept="image/*"
accept={accept}
multiple
className="partner-oss-upload-input"
disabled={disabled || uploading || remaining <= 0}
@@ -140,11 +150,37 @@ export default function MultiOssUploadField({
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 8, marginBottom: 8 }}>
{urls.map((url, index) => (
<div key={`${url}-${index}`} style={{ position: 'relative', width: 88, height: 88 }}>
<img
src={url}
alt=""
style={{ width: 88, height: 88, objectFit: 'cover', borderRadius: 8 }}
/>
{isPdf(url) ? (
<a
href={url}
target="_blank"
rel="noreferrer"
style={{
display: 'flex',
width: 88,
height: 88,
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
gap: 2,
borderRadius: 8,
border: '1px solid rgba(0,0,0,0.08)',
background: '#f7f7f7',
fontSize: 12,
}}
>
<span className="material-symbols-outlined text-primary" style={{ fontSize: 26 }}>
description
</span>
<span className="text-muted">PDF</span>
</a>
) : (
<img
src={url}
alt=""
style={{ width: 88, height: 88, objectFit: 'cover', borderRadius: 8 }}
/>
)}
{!disabled ? (
<button
type="button"
@@ -186,7 +222,7 @@ export default function MultiOssUploadField({
{uploading
? '上传中…'
: remaining <= 0
? `已达上限 ${maxCount}`
? `已达上限 ${maxCount}${unit}`
: label ?? `批量上传(${urls.length}/${maxCount}`}
</span>
</button>