225 lines
9.4 KiB
TypeScript
225 lines
9.4 KiB
TypeScript
import { useEffect, useState } from 'react';
|
|
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 { usePartnerSession } from '../contexts/PartnerSessionContext';
|
|
import { isSubAccount } from '../lib/partnerAccess';
|
|
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 { account } = usePartnerSession();
|
|
const subReadonly = isSubAccount(account);
|
|
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<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) return;
|
|
setLoadError('');
|
|
request<Record<string, unknown>>('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;
|
|
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 envPhotos = Array.isArray(store.media)
|
|
? (store.media as Array<{ url?: string; bizType?: string }>).filter((m) => m.bizType === 'ENV')
|
|
: [];
|
|
const readOnly = subReadonly || 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>}
|
|
|
|
{!subReadonly && (
|
|
<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}
|
|
onClick={() => void changeStatus(s)}
|
|
>
|
|
{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>
|
|
<div className="partner-cover">
|
|
<AppImage
|
|
src={store.coverUrl ? String(store.coverUrl) : null}
|
|
alt={form.name}
|
|
wrapperClassName="app-image--fill"
|
|
/>
|
|
</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">call</span>
|
|
<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 disabled={readOnly} rows={2} value={form.address} onChange={(e) => setForm({ ...form, address: e.target.value })} />
|
|
</div>
|
|
<div className="partner-field">
|
|
<label>门店简介</label>
|
|
<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>
|
|
</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">{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)' }}>
|
|
<h3 className="headline-md" style={{ borderLeft: '4px solid var(--color-subtle-gray)', paddingLeft: 12, marginBottom: 16 }}>管理信息</h3>
|
|
<div>
|
|
<label className="label-md text-muted">状态</label>
|
|
<p className="body-md" style={{ fontWeight: 500 }}>{storeStatusLabel(status)}</p>
|
|
</div>
|
|
</section>
|
|
</main>
|
|
|
|
<footer className="partner-save-footer">
|
|
<button type="button" className="partner-save-cancel" onClick={() => navigate('/stores')}>返回</button>
|
|
{!subReadonly && (
|
|
<button type="button" className="partner-save-submit" onClick={() => void saveBasic()} disabled={readOnly || saving}>
|
|
<span className="material-symbols-outlined">save</span>
|
|
{saving ? '保存中…' : '保存修改'}
|
|
</button>
|
|
)}
|
|
</footer>
|
|
|
|
{toast && <div className="partner-toast">{toast}</div>}
|
|
</div>
|
|
);
|
|
}
|