import { useEffect, useState } from 'react'; import { useNavigate, useParams } from 'react-router-dom'; import AppImage from '@dukang/shared-ui/AppImage'; import { request } 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(); 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 | 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(['', '', '']); const [status, setStatus] = useState('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); function applyStore(data: Record) { 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>('PARTNER_H5', `/partner/stores/${id}`) .then(applyStore) .catch((e) => { setStore(null); setLoadError(e instanceof Error ? e.message : '加载失败'); }); }, [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>('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>('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); } } async function saveBasic() { if (!id || saving || status === 'CLOSED') return; const auditStatus = String(store?.auditStatus || 'APPROVED').toUpperCase(); if (auditStatus === 'PENDING') { setActionError('门店审核中,暂不可修改资料'); return; } setSaving(true); setActionError(''); try { const data = await request>('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(auditStatus === 'REJECTED' ? '已保存并重新提交审核' : '已保存'); } catch (e) { setActionError(e instanceof Error ? e.message : '保存失败'); } finally { setSaving(false); } } 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>('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 (
{loadError}
); } if (!store) return
加载中...
; 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 (
{actionError &&

{actionError}

}

审核状态

{storeAuditLabel(auditStatus)}
{auditPending && (

资料已提交总部审核,通过后即可开门营业。审核期间不可修改资料。

)} {auditRejected && (

审核未通过

{String(store.rejectReason || '未填写驳回原因')}

请按驳回原因修改资料并保存,将自动重新提交审核。

)} {auditStatus === 'APPROVED' && (

总部审核已通过,可将门店设为营业中。

)}
{canMutate && !auditPending && status !== 'CLOSED' && (

门店套餐

维护可核销套餐信息,提交后由总部审核。

)} {canMutate && (

运营状态

{status === 'OPEN' && } {storeStatusLabel(status)}
{STATUS_OPTIONS.map((s) => ( ))}
{status === 'CLOSED' && (

门店已关闭,不可再变更状态或编辑资料

)}
)}

基本信息

{!canMutate || readOnly ? (
) : (

门头照

)}
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 }} />
lock

老板主账号,用于门店端登录;如需变更请联系总部。

phone_in_talk setForm({ ...form, contactPhone: e.target.value })} />

店长或对外展示号码,用户端拨号使用此号码,支持座机。