门店开通流程

This commit is contained in:
2026-07-03 14:53:21 +08:00
parent c7dc6a2274
commit f2175b83f7
24 changed files with 1451 additions and 186 deletions
+126 -37
View File
@@ -3,59 +3,143 @@ import { useNavigate, useParams } from 'react-router-dom';
import PageHeader from '@dukang/shared-ui/PageHeader';
import AppImage from '@dukang/shared-ui/AppImage';
import { request } from '../lib/api';
import { storeStatusLabel, storeStatusPillClass, type StoreStatusValue } from '../lib/storeStatus';
const STATUS_OPTIONS: StoreStatusValue[] = ['OPEN', 'PAUSED', 'CLOSED'];
export default function StoreDetailPage() {
const { id } = useParams();
const navigate = useNavigate();
const [store, setStore] = useState<Record<string, unknown> | null>(null);
const [loadError, setLoadError] = useState('');
const [form, setForm] = useState({ name: '', phone: '', address: '', intro: '' });
const [status, setStatus] = useState('OPEN');
const [status, setStatus] = useState<StoreStatusValue>('OPEN');
const [statusSaving, setStatusSaving] = useState(false);
const [saving, setSaving] = useState(false);
const [error, setError] = useState('');
const [toast, setToast] = useState('');
function showToast(message: string) {
setToast(message);
window.setTimeout(() => setToast(''), 2000);
}
function applyStore(data: Record<string, unknown>) {
setStore(data);
setForm({
name: String(data.name || ''),
phone: String(data.phone || ''),
address: String(data.address || ''),
intro: String(data.intro || ''),
});
setStatus(String(data.status || 'OPEN').toUpperCase() as StoreStatusValue);
}
useEffect(() => {
if (id) {
request('PARTNER_H5', `/stores/${id}`).then((data) => {
setStore(data);
setForm({
name: String(data.name || ''),
phone: String(data.phone || ''),
address: String(data.address || ''),
intro: String(data.intro || ''),
});
setStatus(String(data.status || 'OPEN').toUpperCase());
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 : '加载失败');
});
}
}, [id]);
function save() {
setToast('preV1:保存成功(演示)');
setTimeout(() => setToast(''), 2000);
async function changeStatus(next: StoreStatusValue) {
if (!id || next === status || statusSaving) return;
if (status === 'CLOSED') return;
if (next === 'CLOSED') {
const ok = window.confirm('关闭后不可恢复营业,确认关闭该门店?');
if (!ok) return;
}
setStatusSaving(true);
setError('');
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));
showToast('状态已更新');
} catch (e) {
setError(e instanceof Error ? e.message : '状态更新失败');
} finally {
setStatusSaving(false);
}
}
async function saveBasic() {
if (!id || saving || status === 'CLOSED') return;
setSaving(true);
setError('');
try {
const data = await request<Record<string, unknown>>('PARTNER_H5', `/partner/stores/${id}/basic`, {
method: 'PUT',
body: JSON.stringify({
name: form.name.trim(),
phone: form.phone.trim(),
address: form.address.trim(),
intro: form.intro.trim(),
}),
});
applyStore(data);
showToast('已保存');
} catch (e) {
setError(e instanceof Error ? e.message : '保存失败');
} finally {
setSaving(false);
}
}
if (loadError) {
return (
<div className="partner-detail-page">
<PageHeader title="门店详情" onBack={() => navigate('/stores')} />
<div className="empty">{loadError}</div>
</div>
);
}
if (!store) return <div className="empty">...</div>;
const statusLabel = status === 'OPEN' ? '营业中' : status === 'PAUSED' ? '暂停服务' : '已下线';
const envPhotos = Array.isArray(store.media)
? (store.media as Array<{ url?: string; bizType?: string }>).filter((m) => m.bizType === 'ENV')
: [];
const readOnly = status === 'CLOSED';
return (
<div className="partner-detail-page">
<PageHeader title="门店详情" onBack={() => navigate('/stores')} />
<main style={{ padding: '16px 20px' }}>
{error && <p className="partner-form-error" role="alert" style={{ marginBottom: 12 }}>{error}</p>}
<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 partner-status-pill--open">
<span className="partner-dot partner-dot--green" style={{ width: 8, height: 8, display: 'inline-block', marginRight: 4 }} />
{statusLabel}
<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">
{(['OPEN', 'PAUSED', 'CLOSED'] as const).map((s) => (
<button key={s} type="button" className={status === s ? 'active' : ''} onClick={() => setStatus(s)}>
{s === 'OPEN' ? '营业中' : s === 'PAUSED' ? '暂停服务' : '已下线'}
{STATUS_OPTIONS.map((s) => (
<button
key={s}
type="button"
className={status === s ? 'active' : ''}
disabled={readOnly || statusSaving}
onClick={() => void changeStatus(s)}
>
{storeStatusLabel(s)}
</button>
))}
</div>
{readOnly && (
<p className="label-md text-muted" style={{ marginTop: 12 }}></p>
)}
</section>
<section className="partner-form-card" style={{ margin: '0 0 16px' }}>
@@ -69,22 +153,22 @@ export default function StoreDetailPage() {
</div>
<div className="partner-field">
<label></label>
<input 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 }} />
<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">call</span>
<input type="tel" value={form.phone} onChange={(e) => setForm({ ...form, phone: e.target.value })} />
<input disabled={readOnly} type="tel" value={form.phone} onChange={(e) => setForm({ ...form, phone: e.target.value })} />
</div>
</div>
<div className="partner-field">
<label></label>
<textarea rows={2} value={form.address} onChange={(e) => setForm({ ...form, address: e.target.value })} />
<textarea disabled={readOnly} rows={2} value={form.address} onChange={(e) => setForm({ ...form, address: e.target.value })} />
</div>
<div className="partner-field">
<label></label>
<textarea rows={4} placeholder="请输入门店简介(10-500字)" value={form.intro} onChange={(e) => setForm({ ...form, intro: e.target.value })} />
<textarea disabled={readOnly} rows={4} placeholder="请输入门店简介(10-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>
@@ -94,14 +178,19 @@ export default function StoreDetailPage() {
<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">12</span>
</div>
<div className="partner-photo-grid">
<button type="button" className="partner-photo-add">
<span className="material-symbols-outlined">add</span>
<span className="label-md"></span>
</button>
<span className="label-md text-muted">{envPhotos.length ? `已上传 ${envPhotos.length}` : '暂无照片'}</span>
</div>
{envPhotos.length > 0 ? (
<div className="partner-photo-grid">
{envPhotos.map((photo, index) => (
<div key={index} className="partner-cover" style={{ aspectRatio: '1' }}>
<AppImage src={photo.url ? String(photo.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)' }}>
@@ -109,7 +198,7 @@ export default function StoreDetailPage() {
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 16 }}>
<div>
<label className="label-md text-muted"></label>
<p className="body-md" style={{ fontWeight: 500 }}>{String(store.status)}</p>
<p className="body-md" style={{ fontWeight: 500 }}>{storeStatusLabel(status)}</p>
</div>
<div>
<label className="label-md text-muted">ID</label>
@@ -120,10 +209,10 @@ export default function StoreDetailPage() {
</main>
<footer className="partner-save-footer">
<button type="button" className="partner-save-cancel" onClick={() => navigate('/stores')}></button>
<button type="button" className="partner-save-submit" onClick={save}>
<button type="button" className="partner-save-cancel" onClick={() => navigate('/stores')}></button>
<button type="button" className="partner-save-submit" onClick={() => void saveBasic()} disabled={readOnly || saving}>
<span className="material-symbols-outlined">save</span>
{saving ? '保存中…' : '保存修改'}
</button>
</footer>