@@ -17,6 +17,11 @@ type Props = {
|
||||
accept?: string;
|
||||
/** 计量单位文案,如「张」「个」 */
|
||||
unit?: string;
|
||||
/**
|
||||
* stack:缩略图 + 下方独立上传按钮(合同等)
|
||||
* grid:九宫格,末尾「+」格上传,无独立大按钮(环境图)
|
||||
*/
|
||||
variant?: 'stack' | 'grid';
|
||||
};
|
||||
|
||||
function isCancelError(msg: string): boolean {
|
||||
@@ -42,6 +47,7 @@ export default function MultiOssUploadField({
|
||||
label,
|
||||
accept = 'image/*',
|
||||
unit = '张',
|
||||
variant = 'stack',
|
||||
}: Props) {
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const pickingRef = useRef(false);
|
||||
@@ -53,6 +59,7 @@ export default function MultiOssUploadField({
|
||||
const onChangeRef = useRef(onChange);
|
||||
const remaining = Math.max(0, maxCount - urls.length);
|
||||
const inWechat = isWechatEnv();
|
||||
const isGrid = variant === 'grid';
|
||||
|
||||
useEffect(() => {
|
||||
urlsRef.current = urls;
|
||||
@@ -126,11 +133,78 @@ export default function MultiOssUploadField({
|
||||
}
|
||||
}
|
||||
|
||||
function openPicker() {
|
||||
if (inWechat) void pickWechat();
|
||||
else inputRef.current?.click();
|
||||
}
|
||||
|
||||
function removeAt(index: number) {
|
||||
if (disabled) return;
|
||||
onChange?.(urls.filter((_, i) => i !== index));
|
||||
}
|
||||
|
||||
const thumb = (url: string, index: number) => (
|
||||
<div key={`${url}-${index}`} className={isGrid ? 'partner-upload-grid-thumb' : undefined} style={isGrid ? undefined : { position: 'relative', width: 88, height: 88 }}>
|
||||
{isPdf(url) ? (
|
||||
<a
|
||||
href={url}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
style={{
|
||||
display: 'flex',
|
||||
width: isGrid ? '100%' : 88,
|
||||
height: isGrid ? '100%' : 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: isGrid ? '100%' : 88,
|
||||
height: isGrid ? '100%' : 88,
|
||||
objectFit: 'cover',
|
||||
borderRadius: isGrid ? 12 : 8,
|
||||
display: 'block',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{!disabled ? (
|
||||
<button
|
||||
type="button"
|
||||
className="partner-packages-remove"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 2,
|
||||
right: 2,
|
||||
margin: 0,
|
||||
padding: '2px 6px',
|
||||
fontSize: 12,
|
||||
background: 'rgba(0,0,0,0.55)',
|
||||
color: '#fff',
|
||||
borderRadius: 4,
|
||||
}}
|
||||
onClick={() => removeAt(index)}
|
||||
>
|
||||
删
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="partner-oss-upload">
|
||||
<input
|
||||
@@ -146,86 +220,49 @@ export default function MultiOssUploadField({
|
||||
}}
|
||||
/>
|
||||
|
||||
{urls.length > 0 ? (
|
||||
<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 }}>
|
||||
{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"
|
||||
className="partner-packages-remove"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 2,
|
||||
right: 2,
|
||||
margin: 0,
|
||||
padding: '2px 6px',
|
||||
fontSize: 12,
|
||||
background: 'rgba(0,0,0,0.55)',
|
||||
color: '#fff',
|
||||
borderRadius: 4,
|
||||
}}
|
||||
onClick={() => removeAt(index)}
|
||||
>
|
||||
删
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
))}
|
||||
{isGrid ? (
|
||||
<div className="partner-upload-grid">
|
||||
{urls.map((url, index) => thumb(url, index))}
|
||||
{remaining > 0 && !disabled ? (
|
||||
<button
|
||||
type="button"
|
||||
className="partner-upload-dashed partner-upload-dashed--compact"
|
||||
disabled={uploading}
|
||||
onClick={openPicker}
|
||||
aria-label={uploading ? '上传中' : `添加${unit}(${urls.length}/${maxCount})`}
|
||||
>
|
||||
<span className="material-symbols-outlined text-primary" style={{ fontSize: 28 }}>
|
||||
{uploading ? 'hourglass_top' : 'add'}
|
||||
</span>
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="partner-upload-dashed partner-upload-dashed--compact"
|
||||
disabled={disabled || uploading || remaining <= 0}
|
||||
onClick={() => {
|
||||
if (inWechat) void pickWechat();
|
||||
else inputRef.current?.click();
|
||||
}}
|
||||
>
|
||||
<span className="material-symbols-outlined text-primary" style={{ fontSize: 28 }}>
|
||||
{uploading ? 'hourglass_top' : 'add_a_photo'}
|
||||
</span>
|
||||
<span className="text-primary" style={{ fontWeight: 500 }}>
|
||||
{uploading
|
||||
? '上传中…'
|
||||
: remaining <= 0
|
||||
? `已达上限 ${maxCount}${unit}`
|
||||
: label ?? `批量上传(${urls.length}/${maxCount})`}
|
||||
</span>
|
||||
</button>
|
||||
) : (
|
||||
<>
|
||||
{urls.length > 0 ? (
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 8, marginBottom: 8 }}>
|
||||
{urls.map((url, index) => thumb(url, index))}
|
||||
</div>
|
||||
) : null}
|
||||
<button
|
||||
type="button"
|
||||
className="partner-upload-dashed partner-upload-dashed--compact"
|
||||
disabled={disabled || uploading || remaining <= 0}
|
||||
onClick={openPicker}
|
||||
>
|
||||
<span className="material-symbols-outlined text-primary" style={{ fontSize: 28 }}>
|
||||
{uploading ? 'hourglass_top' : 'add_a_photo'}
|
||||
</span>
|
||||
<span className="text-primary" style={{ fontWeight: 500 }}>
|
||||
{uploading
|
||||
? '上传中…'
|
||||
: remaining <= 0
|
||||
? `已达上限 ${maxCount}${unit}`
|
||||
: label ?? `批量上传(${urls.length}/${maxCount})`}
|
||||
</span>
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
{error ? (
|
||||
<p className="partner-form-error" role="alert">
|
||||
{error}
|
||||
|
||||
@@ -1007,9 +1007,9 @@ export default function StoreCreatePage() {
|
||||
<MultiOssUploadField
|
||||
bizType="STORE_ENV"
|
||||
maxCount={20}
|
||||
variant="grid"
|
||||
value={form.envPhotoUrls.map((u) => u.trim()).filter(Boolean)}
|
||||
onChange={(urls) => setForm((prev) => ({ ...prev, envPhotoUrls: urls.length ? urls : [''] }))}
|
||||
label={`批量上传(${form.envPhotoUrls.map((u) => u.trim()).filter(Boolean).length}/20)`}
|
||||
/>
|
||||
</section>
|
||||
|
||||
|
||||
@@ -56,7 +56,6 @@ export default function StoreDetailPage() {
|
||||
const [status, setStatus] = useState<StoreStatusValue>('OPEN');
|
||||
const [statusSaving, setStatusSaving] = useState(false);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [mediaSaving, setMediaSaving] = useState(false);
|
||||
const [mapPickerOpen, setMapPickerOpen] = useState(false);
|
||||
const [actionError, setActionError] = useState('');
|
||||
const [closeConfirmOpen, setCloseConfirmOpen] = useState(false);
|
||||
@@ -159,32 +158,133 @@ export default function StoreDetailPage() {
|
||||
}
|
||||
}
|
||||
|
||||
async function saveBasic() {
|
||||
function mediaChanged(): boolean {
|
||||
if (!store) return false;
|
||||
const nextCover = coverUrl.trim();
|
||||
const origCover = String(store.coverUrl || '').trim();
|
||||
if (nextCover !== origCover) return true;
|
||||
const nextEnv = uniqueEnvUrls(envPhotoUrls);
|
||||
const origEnv = uniqueEnvUrls(
|
||||
Array.isArray(store.media)
|
||||
? (store.media as Array<{ url?: string; bizType?: string }>)
|
||||
.filter((m) => m.bizType === 'ENV')
|
||||
.map((m) => String(m.url || ''))
|
||||
: [],
|
||||
);
|
||||
if (nextEnv.length !== origEnv.length) return true;
|
||||
return nextEnv.some((u, i) => u !== origEnv[i]);
|
||||
}
|
||||
|
||||
function basicChanged(): boolean {
|
||||
if (!store) return false;
|
||||
const liveContact = String(store.contactPhone || store.phone || '').trim();
|
||||
const liveIntro = String(store.intro || '').trim();
|
||||
const liveRuleRaw = String(store.benefitUsageRule || '').trim();
|
||||
const liveRule = liveRuleRaw && !/^null$/i.test(liveRuleRaw) ? liveRuleRaw : '';
|
||||
const liveLat =
|
||||
store.latitude != null && store.latitude !== '' ? String(store.latitude) : '';
|
||||
const liveLng =
|
||||
store.longitude != null && store.longitude !== '' ? String(store.longitude) : '';
|
||||
return (
|
||||
form.name.trim() !== String(store.name || '').trim() ||
|
||||
form.contactPhone.trim() !== liveContact ||
|
||||
form.address.trim() !== String(store.address || '').trim() ||
|
||||
form.intro.trim() !== liveIntro ||
|
||||
form.benefitUsageRule.trim() !== liveRule ||
|
||||
form.latitude.trim() !== liveLat ||
|
||||
form.longitude.trim() !== liveLng
|
||||
);
|
||||
}
|
||||
|
||||
async function saveChanges() {
|
||||
if (!id || saving || status === 'CLOSED') return;
|
||||
const auditStatus = String(store?.auditStatus || 'APPROVED').toUpperCase();
|
||||
if (auditStatus === 'PENDING') {
|
||||
setActionError('门店审核中,暂不可修改资料');
|
||||
return;
|
||||
}
|
||||
|
||||
const wantMedia = mediaChanged();
|
||||
const wantBasic = basicChanged();
|
||||
if (!wantMedia && !wantBasic) {
|
||||
setActionError('没有检测到需要变更的字段');
|
||||
return;
|
||||
}
|
||||
|
||||
const nextCover = coverUrl.trim();
|
||||
const nextEnv = uniqueEnvUrls(envPhotoUrls);
|
||||
if (wantMedia) {
|
||||
if (!nextCover) {
|
||||
setActionError('请上传门头照');
|
||||
return;
|
||||
}
|
||||
if (nextEnv.length < MIN_ENV_PHOTO_COUNT) {
|
||||
setActionError(`请上传至少 ${MIN_ENV_PHOTO_COUNT} 张环境照片`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
setSaving(true);
|
||||
setActionError('');
|
||||
try {
|
||||
// v3.5.1 #5:基本信息变更走「提交变更」审核流,由总部审核通过后覆盖门店
|
||||
await submitStoreInfoChangeRequest(id, {
|
||||
name: form.name.trim(),
|
||||
contactPhone: form.contactPhone.trim(),
|
||||
address: form.address.trim(),
|
||||
intro: form.intro.trim(),
|
||||
benefitUsageRule: form.benefitUsageRule.trim() || null,
|
||||
...(form.latitude.trim() && form.longitude.trim()
|
||||
? {
|
||||
latitude: Number(form.latitude),
|
||||
longitude: Number(form.longitude),
|
||||
}
|
||||
: {}),
|
||||
});
|
||||
// 入驻被驳回:直写 media/basic 并重提门店审核;已通过门店:统一走信息变更审核
|
||||
if (auditStatus === 'REJECTED') {
|
||||
if (wantMedia) {
|
||||
const data = await request<Record<string, unknown>>('PARTNER_H5', `/partner/stores/${id}/media`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({
|
||||
coverUrl: nextCover,
|
||||
envPhotoUrls: nextEnv,
|
||||
}),
|
||||
});
|
||||
applyStore(data);
|
||||
}
|
||||
if (wantBasic) {
|
||||
const data = await request<Record<string, unknown>>('PARTNER_H5', `/partner/stores/${id}/basic`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({
|
||||
name: form.name.trim(),
|
||||
contactPhone: form.contactPhone.trim(),
|
||||
address: form.address.trim(),
|
||||
intro: form.intro.trim(),
|
||||
benefitUsageRule: form.benefitUsageRule.trim() || null,
|
||||
...(form.latitude.trim() && form.longitude.trim()
|
||||
? {
|
||||
latitude: Number(form.latitude),
|
||||
longitude: Number(form.longitude),
|
||||
}
|
||||
: {}),
|
||||
}),
|
||||
});
|
||||
applyStore(data);
|
||||
}
|
||||
toastSuccess('已重新提交审核');
|
||||
return;
|
||||
}
|
||||
|
||||
const fields: Record<string, unknown> = {};
|
||||
if (wantBasic) {
|
||||
fields.name = form.name.trim();
|
||||
fields.contactPhone = form.contactPhone.trim();
|
||||
fields.address = form.address.trim();
|
||||
fields.intro = form.intro.trim();
|
||||
fields.benefitUsageRule = form.benefitUsageRule.trim() || null;
|
||||
if (form.latitude.trim() && form.longitude.trim()) {
|
||||
fields.latitude = Number(form.latitude);
|
||||
fields.longitude = Number(form.longitude);
|
||||
}
|
||||
}
|
||||
if (wantMedia) {
|
||||
fields.coverUrl = nextCover;
|
||||
fields.envPhotoUrls = nextEnv;
|
||||
}
|
||||
await submitStoreInfoChangeRequest(id, fields);
|
||||
setPendingInfoChange(true);
|
||||
toastSuccess('变更已提交,等待总部审核');
|
||||
toastSuccess(
|
||||
wantMedia
|
||||
? '变更已提交(含门头照/环境图),总部审核通过后生效'
|
||||
: '变更已提交,等待总部审核',
|
||||
);
|
||||
} catch (e) {
|
||||
setActionError(e instanceof Error ? e.message : '提交失败');
|
||||
} finally {
|
||||
@@ -192,42 +292,6 @@ export default function StoreDetailPage() {
|
||||
}
|
||||
}
|
||||
|
||||
async function saveMedia() {
|
||||
if (!id || mediaSaving || status === 'CLOSED') return;
|
||||
const auditStatus = String(store?.auditStatus || 'APPROVED').toUpperCase();
|
||||
if (auditStatus === 'PENDING') {
|
||||
setActionError('门店审核中,暂不可修改资料');
|
||||
return;
|
||||
}
|
||||
const nextCover = coverUrl.trim();
|
||||
const nextEnv = uniqueEnvUrls(envPhotoUrls);
|
||||
if (!nextCover) {
|
||||
setActionError('请上传门头照');
|
||||
return;
|
||||
}
|
||||
if (nextEnv.length < MIN_ENV_PHOTO_COUNT) {
|
||||
setActionError(`请上传至少 ${MIN_ENV_PHOTO_COUNT} 张环境照片`);
|
||||
return;
|
||||
}
|
||||
setMediaSaving(true);
|
||||
setActionError('');
|
||||
try {
|
||||
const data = await request<Record<string, unknown>>('PARTNER_H5', `/partner/stores/${id}/media`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({
|
||||
coverUrl: nextCover,
|
||||
envPhotoUrls: nextEnv,
|
||||
}),
|
||||
});
|
||||
applyStore(data);
|
||||
toastSuccess(auditStatus === 'REJECTED' ? '照片已更新并重新提交审核' : '照片已更新');
|
||||
} catch (e) {
|
||||
setActionError(e instanceof Error ? e.message : '照片更新失败');
|
||||
} finally {
|
||||
setMediaSaving(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (loadError) {
|
||||
return (
|
||||
<div className="partner-detail-page partner-home--flush-top">
|
||||
@@ -445,27 +509,13 @@ export default function StoreDetailPage() {
|
||||
</span>
|
||||
</div>
|
||||
{canMutate && !readOnly ? (
|
||||
<>
|
||||
<MultiOssUploadField
|
||||
bizType="STORE_ENV"
|
||||
maxCount={20}
|
||||
value={uniqueEnvUrls(envPhotoUrls)}
|
||||
onChange={(urls) => setEnvPhotoUrls(urls.length ? urls : [''])}
|
||||
label={`批量上传环境照(${uniqueEnvUrls(envPhotoUrls).length}/20)`}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="partner-btn-outline"
|
||||
style={{ width: '100%', marginTop: 16 }}
|
||||
disabled={mediaSaving}
|
||||
onClick={() => void saveMedia()}
|
||||
>
|
||||
<span className="material-symbols-outlined" style={{ fontSize: 18, verticalAlign: 'middle', marginRight: 4 }}>
|
||||
upload
|
||||
</span>
|
||||
{mediaSaving ? '上传中…' : '重新上传照片'}
|
||||
</button>
|
||||
</>
|
||||
<MultiOssUploadField
|
||||
bizType="STORE_ENV"
|
||||
maxCount={20}
|
||||
variant="grid"
|
||||
value={uniqueEnvUrls(envPhotoUrls)}
|
||||
onChange={(urls) => setEnvPhotoUrls(urls.length ? urls : [''])}
|
||||
/>
|
||||
) : envPhotos.length > 0 ? (
|
||||
<div className="partner-photo-grid">
|
||||
{envPhotos.map((url, index) => (
|
||||
@@ -495,7 +545,7 @@ export default function StoreDetailPage() {
|
||||
<footer className="partner-save-footer">
|
||||
<button type="button" className="partner-save-cancel" onClick={() => navigate('/stores')}>返回</button>
|
||||
{canMutate && (
|
||||
<button type="button" className="partner-save-submit" onClick={() => void saveBasic()} disabled={readOnly || saving}>
|
||||
<button type="button" className="partner-save-submit" onClick={() => void saveChanges()} disabled={readOnly || saving}>
|
||||
<span className="material-symbols-outlined">send</span>
|
||||
{saving ? '提交中…' : '提交变更'}
|
||||
</button>
|
||||
|
||||
@@ -1622,10 +1622,19 @@ nav.app-tabbar .app-tabbar-label {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.partner-upload-grid-thumb {
|
||||
position: relative;
|
||||
aspect-ratio: 1;
|
||||
width: 100%;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.partner-upload-grid .partner-upload-dashed {
|
||||
aspect-ratio: 1;
|
||||
padding: 12px;
|
||||
font-size: 10px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.partner-upload-dashed--compact {
|
||||
|
||||
Reference in New Issue
Block a user