v3.5.3版本更新1
CI / verify (pull_request) Has been cancelled

This commit is contained in:
2026-08-20 18:54:15 +08:00
parent ee493823bf
commit fc2e5b65de
65 changed files with 2297 additions and 875 deletions
@@ -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>
+125 -75
View File
@@ -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>
+9
View File
@@ -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 {