592 lines
25 KiB
TypeScript
592 lines
25 KiB
TypeScript
import { useEffect, useState } from 'react';
|
||
import { useNavigate, useParams } from 'react-router-dom';
|
||
import AppImage from '@dukang/shared-ui/AppImage';
|
||
import { request, submitStoreInfoChangeRequest, listStoreInfoChangeRequests } from '../lib/api';
|
||
import { toastError, toastSuccess } from '../lib/toast';
|
||
import { usePartnerSession } from '../contexts/PartnerSessionContext';
|
||
import { canManagePartnerStore } from '../lib/partnerAccess';
|
||
import { MIN_ENV_PHOTO_COUNT, normalizeStringArray } from '../lib/storeDraft';
|
||
import OssUploadField from '../components/OssUploadField';
|
||
import MultiOssUploadField from '../components/MultiOssUploadField';
|
||
import TencentLocPickerOverlay from '../components/TencentLocPickerOverlay';
|
||
import {
|
||
canPartnerOpenStore,
|
||
storeAuditLabel,
|
||
storeAuditPillClass,
|
||
storeStatusLabel,
|
||
storeStatusPillClass,
|
||
type StoreStatusValue,
|
||
} from '../lib/storeStatus';
|
||
import { usePartnerPageView } from '../lib/usePageView';
|
||
|
||
const STATUS_OPTIONS: StoreStatusValue[] = ['OPEN', 'PAUSED', 'CLOSED'];
|
||
|
||
function uniqueEnvUrls(urls: string[]): string[] {
|
||
const seen = new Set<string>();
|
||
const out: string[] = [];
|
||
for (const raw of urls) {
|
||
const url = raw.trim();
|
||
if (!url || seen.has(url)) continue;
|
||
seen.add(url);
|
||
out.push(url);
|
||
}
|
||
return out;
|
||
}
|
||
|
||
export default function StoreDetailPage() {
|
||
const { id } = useParams();
|
||
usePartnerPageView('partner_store_detail_view', id ? { storeId: id } : undefined);
|
||
const navigate = useNavigate();
|
||
const { account } = usePartnerSession();
|
||
const canMutate = canManagePartnerStore(account);
|
||
const [store, setStore] = useState<Record<string, unknown> | null>(null);
|
||
const [loadError, setLoadError] = useState('');
|
||
const [form, setForm] = useState({
|
||
name: '',
|
||
loginPhone: '',
|
||
contactPhone: '',
|
||
address: '',
|
||
intro: '',
|
||
benefitUsageRule: '',
|
||
latitude: '',
|
||
longitude: '',
|
||
});
|
||
const [coverUrl, setCoverUrl] = useState('');
|
||
const [envPhotoUrls, setEnvPhotoUrls] = useState<string[]>(['', '', '']);
|
||
const [status, setStatus] = useState<StoreStatusValue>('OPEN');
|
||
const [statusSaving, setStatusSaving] = useState(false);
|
||
const [saving, setSaving] = useState(false);
|
||
const [mapPickerOpen, setMapPickerOpen] = useState(false);
|
||
const [actionError, setActionError] = useState('');
|
||
const [closeConfirmOpen, setCloseConfirmOpen] = useState(false);
|
||
const [pendingInfoChange, setPendingInfoChange] = useState(false);
|
||
|
||
function applyStore(data: Record<string, unknown>) {
|
||
setStore(data);
|
||
setForm({
|
||
name: String(data.name || ''),
|
||
loginPhone: String(data.phone || ''),
|
||
contactPhone: String(data.contactPhone || data.phone || ''),
|
||
address: String(data.address || ''),
|
||
intro: String(data.intro || ''),
|
||
benefitUsageRule: (() => {
|
||
const raw = String(data.benefitUsageRule || '').trim();
|
||
return raw && !/^null$/i.test(raw) ? raw : '';
|
||
})(),
|
||
latitude: data.latitude != null && data.latitude !== '' ? String(data.latitude) : '',
|
||
longitude: data.longitude != null && data.longitude !== '' ? String(data.longitude) : '',
|
||
});
|
||
setStatus(String(data.status || 'OPEN').toUpperCase() as StoreStatusValue);
|
||
setCoverUrl(String(data.coverUrl || ''));
|
||
const envFromMedia = Array.isArray(data.media)
|
||
? uniqueEnvUrls(
|
||
(data.media as Array<{ url?: string; bizType?: string }>)
|
||
.filter((m) => m.bizType === 'ENV')
|
||
.map((m) => String(m.url || '')),
|
||
)
|
||
: [];
|
||
setEnvPhotoUrls(normalizeStringArray(envFromMedia, MIN_ENV_PHOTO_COUNT));
|
||
}
|
||
|
||
useEffect(() => {
|
||
if (!id) return;
|
||
setLoadError('');
|
||
request<Record<string, unknown>>('PARTNER_H5', `/partner/stores/${id}`)
|
||
.then(applyStore)
|
||
.catch((e) => {
|
||
setStore(null);
|
||
setLoadError(e instanceof Error ? e.message : '加载失败');
|
||
});
|
||
// 拉取该门店的信息变更审核记录,若有 PENDING 则展示「审核中」横幅
|
||
listStoreInfoChangeRequests(id)
|
||
.then((reqs) =>
|
||
setPendingInfoChange(Array.isArray(reqs) && reqs.some((r) => r.status === 'PENDING')),
|
||
)
|
||
.catch(() => {});
|
||
}, [id]);
|
||
|
||
async function changeStatus(next: StoreStatusValue) {
|
||
if (!id || next === status || statusSaving) return;
|
||
if (status === 'CLOSED') return;
|
||
const auditStatus = String(store?.auditStatus || 'APPROVED').toUpperCase();
|
||
if (next === 'OPEN' && !canPartnerOpenStore(auditStatus)) {
|
||
setActionError(
|
||
auditStatus === 'REJECTED'
|
||
? `门店审核未通过${store?.rejectReason ? `:${String(store.rejectReason)}` : ''}`
|
||
: '门店尚在总部审核中,通过后方可开门',
|
||
);
|
||
return;
|
||
}
|
||
if (next === 'CLOSED') {
|
||
setCloseConfirmOpen(true);
|
||
return;
|
||
}
|
||
setStatusSaving(true);
|
||
setActionError('');
|
||
try {
|
||
const updated = await request<Record<string, unknown>>('PARTNER_H5', `/partner/stores/${id}/status`, {
|
||
method: 'PUT',
|
||
body: JSON.stringify({ status: next }),
|
||
});
|
||
setStatus(next);
|
||
setStore((prev) => (prev ? { ...prev, ...updated, status: next } : prev));
|
||
toastSuccess(next === 'OPEN' ? '开店成功' : '状态已更新');
|
||
} catch (e) {
|
||
setActionError(e instanceof Error ? e.message : '状态更新失败');
|
||
} finally {
|
||
setStatusSaving(false);
|
||
}
|
||
}
|
||
|
||
async function confirmCloseStore() {
|
||
if (!id || statusSaving) return;
|
||
setCloseConfirmOpen(false);
|
||
setStatusSaving(true);
|
||
setActionError('');
|
||
try {
|
||
const updated = await request<Record<string, unknown>>('PARTNER_H5', `/partner/stores/${id}/status`, {
|
||
method: 'PUT',
|
||
body: JSON.stringify({ status: 'CLOSED' }),
|
||
});
|
||
setStatus('CLOSED');
|
||
setStore((prev) => (prev ? { ...prev, ...updated, status: 'CLOSED' } : prev));
|
||
toastSuccess('门店已关闭');
|
||
} catch (e) {
|
||
setActionError(e instanceof Error ? e.message : '状态更新失败');
|
||
} finally {
|
||
setStatusSaving(false);
|
||
}
|
||
}
|
||
|
||
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 {
|
||
// 入驻被驳回:直写 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(
|
||
wantMedia
|
||
? '变更已提交(含门头照/环境图),总部审核通过后生效'
|
||
: '变更已提交,等待总部审核',
|
||
);
|
||
} catch (e) {
|
||
setActionError(e instanceof Error ? e.message : '提交失败');
|
||
} finally {
|
||
setSaving(false);
|
||
}
|
||
}
|
||
|
||
if (loadError) {
|
||
return (
|
||
<div className="partner-detail-page partner-home--flush-top">
|
||
<div className="empty">{loadError}</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
if (!store) return <div className="empty">加载中...</div>;
|
||
|
||
const envPhotos = uniqueEnvUrls(
|
||
Array.isArray(store.media)
|
||
? (store.media as Array<{ url?: string; bizType?: string }>)
|
||
.filter((m) => m.bizType === 'ENV')
|
||
.map((m) => String(m.url || ''))
|
||
: [],
|
||
);
|
||
const auditStatus = String(store.auditStatus || 'APPROVED').toUpperCase();
|
||
const auditPending = auditStatus === 'PENDING';
|
||
const auditRejected = auditStatus === 'REJECTED';
|
||
const readOnly = !canMutate || status === 'CLOSED' || auditPending;
|
||
const canOpen = canPartnerOpenStore(auditStatus);
|
||
|
||
return (
|
||
<div className="partner-detail-page partner-home--flush-top">
|
||
<main style={{ padding: '12px 20px 16px' }}>
|
||
{actionError && <p className="partner-form-error" role="alert" style={{ marginBottom: 12 }}>{actionError}</p>}
|
||
|
||
<section className="partner-form-card" style={{ margin: '0 0 16px' }}>
|
||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 12 }}>
|
||
<h3 className="label-md text-muted" style={{ textTransform: 'uppercase', letterSpacing: '0.1em' }}>审核状态</h3>
|
||
<span className={`partner-status-pill ${storeAuditPillClass(auditStatus)}`}>
|
||
{storeAuditLabel(auditStatus)}
|
||
</span>
|
||
</div>
|
||
{auditPending && (
|
||
<p className="body-md" style={{ color: 'var(--color-secondary)' }}>
|
||
资料已提交总部审核,通过后即可开门营业。审核期间不可修改资料。
|
||
</p>
|
||
)}
|
||
{auditRejected && (
|
||
<div>
|
||
<p className="body-md" style={{ color: 'var(--color-heritage-red)', fontWeight: 600, marginBottom: 8 }}>
|
||
审核未通过
|
||
</p>
|
||
<p className="body-md" style={{ background: 'rgba(180,35,24,0.06)', padding: 12, borderRadius: 8 }}>
|
||
{String(store.rejectReason || '未填写驳回原因')}
|
||
</p>
|
||
<p className="label-md text-muted" style={{ marginTop: 8 }}>
|
||
请按驳回原因修改资料并保存,将自动重新提交审核。
|
||
</p>
|
||
</div>
|
||
)}
|
||
{auditStatus === 'APPROVED' && (
|
||
<p className="label-md text-muted">总部审核已通过,可将门店设为营业中。</p>
|
||
)}
|
||
{pendingInfoChange && (
|
||
<div style={{ marginTop: 12, background: 'rgba(245,166,35,0.08)', border: '1px solid rgba(245,166,35,0.3)', borderRadius: 8, padding: 12 }}>
|
||
<p className="body-md" style={{ fontWeight: 600, marginBottom: 4 }}>
|
||
基础信息变更审核中
|
||
</p>
|
||
<p className="label-md text-muted" style={{ margin: 0 }}>
|
||
已提交的资料变更正在等待总部审核,审核通过后才会覆盖门店线上信息。审核期间可继续修改并重新提交。
|
||
</p>
|
||
</div>
|
||
)}
|
||
</section>
|
||
|
||
{canMutate && !auditPending && status !== 'CLOSED' && (
|
||
<section className="partner-form-card" style={{ margin: '0 0 16px' }}>
|
||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||
<h3 className="label-md text-muted" style={{ textTransform: 'uppercase', letterSpacing: '0.1em' }}>门店套餐</h3>
|
||
<button type="button" className="partner-btn-outline partner-btn-outline--compact" onClick={() => navigate(`/stores/${id}/packages`)}>
|
||
编辑套餐
|
||
</button>
|
||
</div>
|
||
<p className="label-md text-muted" style={{ marginTop: 8 }}>维护可核销套餐信息,提交后由总部审核。</p>
|
||
</section>
|
||
)}
|
||
|
||
{canMutate && (
|
||
<section className="partner-form-card" style={{ margin: '0 0 16px' }}>
|
||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 }}>
|
||
<h3 className="label-md text-muted" style={{ textTransform: 'uppercase', letterSpacing: '0.1em' }}>运营状态</h3>
|
||
<span className={`partner-status-pill ${storeStatusPillClass(status)}`}>
|
||
{status === 'OPEN' && <span className="partner-dot partner-dot--green" style={{ width: 8, height: 8, display: 'inline-block', marginRight: 4 }} />}
|
||
{storeStatusLabel(status)}
|
||
</span>
|
||
</div>
|
||
<div className="partner-status-toggle">
|
||
{STATUS_OPTIONS.map((s) => (
|
||
<button
|
||
key={s}
|
||
type="button"
|
||
className={status === s ? 'active' : ''}
|
||
disabled={readOnly || statusSaving || (s === 'OPEN' && !canOpen) || status === 'CLOSED'}
|
||
onClick={() => void changeStatus(s)}
|
||
>
|
||
{s === 'OPEN' && !canOpen ? '待审核' : storeStatusLabel(s)}
|
||
</button>
|
||
))}
|
||
</div>
|
||
{status === 'CLOSED' && (
|
||
<p className="label-md text-muted" style={{ marginTop: 12 }}>门店已关闭,不可再变更状态或编辑资料</p>
|
||
)}
|
||
</section>
|
||
)}
|
||
|
||
<section className="partner-form-card" style={{ margin: '0 0 16px' }}>
|
||
<h3 className="headline-md" style={{ borderLeft: '4px solid var(--color-heritage-red)', paddingLeft: 12, marginBottom: 16 }}>基本信息</h3>
|
||
{!canMutate || readOnly ? (
|
||
<div className="partner-cover">
|
||
<AppImage
|
||
src={coverUrl || null}
|
||
alt={form.name}
|
||
wrapperClassName="app-image--fill"
|
||
/>
|
||
</div>
|
||
) : (
|
||
<div style={{ marginBottom: 16 }}>
|
||
<p className="label-md text-muted" style={{ marginBottom: 8 }}>门头照</p>
|
||
<OssUploadField
|
||
wide
|
||
bizType="STORE_TITLE"
|
||
mediaType="IMAGE"
|
||
value={coverUrl}
|
||
onChange={setCoverUrl}
|
||
label="拍照 / 从相册选择"
|
||
/>
|
||
</div>
|
||
)}
|
||
<div className="partner-field">
|
||
<label>门店名称</label>
|
||
<input disabled={readOnly} value={form.name} onChange={(e) => setForm({ ...form, name: e.target.value })} style={{ width: '100%', padding: '12px 16px', border: '1px solid rgba(226,190,188,0.5)', borderRadius: 8, fontSize: 16, fontWeight: 500 }} />
|
||
</div>
|
||
<div className="partner-field">
|
||
<label>门店登录手机号</label>
|
||
<div className="partner-field-input">
|
||
<span className="material-symbols-outlined">lock</span>
|
||
<input disabled type="tel" value={form.loginPhone} readOnly />
|
||
</div>
|
||
<p className="label-md text-muted" style={{ marginTop: 8 }}>
|
||
老板主账号,用于门店端登录;如需变更请联系总部。
|
||
</p>
|
||
</div>
|
||
<div className="partner-field">
|
||
<label>联系电话</label>
|
||
<div className="partner-field-input">
|
||
<span className="material-symbols-outlined">phone_in_talk</span>
|
||
<input
|
||
disabled={readOnly}
|
||
type="tel"
|
||
placeholder="手机号或座机,如 0379-8888888"
|
||
value={form.contactPhone}
|
||
onChange={(e) => setForm({ ...form, contactPhone: e.target.value })}
|
||
/>
|
||
</div>
|
||
<p className="label-md text-muted" style={{ marginTop: 8 }}>
|
||
店长或对外展示号码,用户端拨号使用此号码,支持座机。
|
||
</p>
|
||
</div>
|
||
<div className="partner-field">
|
||
<label>门店地址</label>
|
||
<textarea disabled={readOnly} rows={2} value={form.address} onChange={(e) => setForm({ ...form, address: e.target.value })} />
|
||
{!readOnly ? (
|
||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginTop: 8, flexWrap: 'wrap' }}>
|
||
<button
|
||
type="button"
|
||
className="partner-btn-outline"
|
||
style={{ padding: '8px 12px', fontSize: 13 }}
|
||
onClick={() => setMapPickerOpen(true)}
|
||
>
|
||
地图选点
|
||
</button>
|
||
<span className="label-md text-muted">
|
||
{form.latitude.trim() && form.longitude.trim() ? '已选点' : '未选点'}
|
||
</span>
|
||
</div>
|
||
) : null}
|
||
<input type="hidden" name="latitude" value={form.latitude} readOnly />
|
||
<input type="hidden" name="longitude" value={form.longitude} readOnly />
|
||
</div>
|
||
<div className="partner-field">
|
||
<label>门店简介</label>
|
||
<textarea disabled={readOnly} rows={4} placeholder="请输入门店简介(2-500字)" value={form.intro} onChange={(e) => setForm({ ...form, intro: e.target.value })} />
|
||
<div style={{ textAlign: 'right', marginTop: 4 }}>
|
||
<span className="label-md text-muted">{form.intro.length} / 500</span>
|
||
</div>
|
||
</div>
|
||
<div className="partner-field">
|
||
<label>好客权益券使用规则</label>
|
||
<textarea
|
||
disabled={readOnly}
|
||
rows={4}
|
||
placeholder="选填,展示在用户端门店详情,最多1000字"
|
||
maxLength={1000}
|
||
value={form.benefitUsageRule}
|
||
onChange={(e) => setForm({ ...form, benefitUsageRule: e.target.value })}
|
||
/>
|
||
<div style={{ textAlign: 'right', marginTop: 4 }}>
|
||
<span className="label-md text-muted">{form.benefitUsageRule.length} / 1000</span>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<section className="partner-form-card" style={{ margin: '0 0 16px' }}>
|
||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-end', marginBottom: 16 }}>
|
||
<h3 className="headline-md" style={{ borderLeft: '4px solid var(--color-heritage-red)', paddingLeft: 12 }}>店内环境</h3>
|
||
<span className="label-md text-muted">
|
||
{canMutate && !readOnly
|
||
? `至少 ${MIN_ENV_PHOTO_COUNT} 张 · 已选 ${uniqueEnvUrls(envPhotoUrls).length} 张`
|
||
: envPhotos.length
|
||
? `已上传 ${envPhotos.length} 张`
|
||
: '暂无照片'}
|
||
</span>
|
||
</div>
|
||
{canMutate && !readOnly ? (
|
||
<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) => (
|
||
<div key={`${url}-${index}`} className="partner-cover" style={{ aspectRatio: '1', marginBottom: 0 }}>
|
||
<AppImage src={url || null} alt={`环境图 ${index + 1}`} wrapperClassName="app-image--fill" />
|
||
</div>
|
||
))}
|
||
</div>
|
||
) : (
|
||
<p className="label-md text-muted">录入门店时上传的环境照片将显示在这里</p>
|
||
)}
|
||
</section>
|
||
|
||
<section className="partner-form-card" style={{ margin: 0, background: 'var(--color-surface-container)' }}>
|
||
<h3 className="headline-md" style={{ borderLeft: '4px solid var(--color-subtle-gray)', paddingLeft: 12, marginBottom: 16 }}>管理信息</h3>
|
||
<div style={{ marginBottom: 12 }}>
|
||
<label className="label-md text-muted">营业状态</label>
|
||
<p className="body-md" style={{ fontWeight: 500 }}>{storeStatusLabel(status)}</p>
|
||
</div>
|
||
<div>
|
||
<label className="label-md text-muted">审核状态</label>
|
||
<p className="body-md" style={{ fontWeight: 500 }}>{storeAuditLabel(auditStatus)}</p>
|
||
</div>
|
||
</section>
|
||
</main>
|
||
|
||
<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 saveChanges()} disabled={readOnly || saving}>
|
||
<span className="material-symbols-outlined">send</span>
|
||
{saving ? '提交中…' : '提交变更'}
|
||
</button>
|
||
)}
|
||
</footer>
|
||
|
||
{closeConfirmOpen && (
|
||
<div className="partner-ship-modal-backdrop" role="presentation" onClick={() => setCloseConfirmOpen(false)}>
|
||
<div className="partner-ship-modal" role="dialog" aria-modal="true" onClick={(e) => e.stopPropagation()}>
|
||
<h3 className="headline-md" style={{ marginBottom: 8 }}>确认关闭门店?</h3>
|
||
<p className="body-md text-muted" style={{ lineHeight: 1.5 }}>
|
||
关闭后不可恢复营业,确认关闭该门店?
|
||
</p>
|
||
<div className="partner-ship-actions">
|
||
<button type="button" className="partner-btn-secondary" onClick={() => setCloseConfirmOpen(false)}>
|
||
取消
|
||
</button>
|
||
<button type="button" className="partner-btn-primary" onClick={() => void confirmCloseStore()}>
|
||
确认关闭
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
<TencentLocPickerOverlay
|
||
open={mapPickerOpen}
|
||
onClose={() => setMapPickerOpen(false)}
|
||
latitude={form.latitude ? Number(form.latitude) : undefined}
|
||
longitude={form.longitude ? Number(form.longitude) : undefined}
|
||
onPick={(loc) => {
|
||
setForm((prev) => ({
|
||
...prev,
|
||
latitude: String(loc.latitude),
|
||
longitude: String(loc.longitude),
|
||
...(loc.address && !prev.address.trim() ? { address: loc.address } : {}),
|
||
}));
|
||
toastSuccess(loc.name ? `已选定:${loc.name}` : '位置已选定');
|
||
}}
|
||
/>
|
||
</div>
|
||
);
|
||
}
|