style(store): 统一门店套餐页 UI 与拓店校验弹窗
管理后台套餐 Tab 对齐 Ant Design 表单;合伙人/门店端套餐表单与基本信息页风格一致;拓店下一步校验增加弹窗提示。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,4 +1,3 @@
|
||||
import { useState } from 'react';
|
||||
import type { PackageFormItem } from '../lib/storePackages';
|
||||
import { STORE_PACKAGE_MAX_COUNT } from '@dukang/shared-types';
|
||||
import { emptyPackage } from '../lib/storePackages';
|
||||
@@ -7,9 +6,11 @@ type Props = {
|
||||
items: PackageFormItem[];
|
||||
onChange: (items: PackageFormItem[]) => void;
|
||||
disabled?: boolean;
|
||||
/** 嵌入拓店流程时不重复外层卡片边距 */
|
||||
embedded?: boolean;
|
||||
};
|
||||
|
||||
export default function StorePackagesForm({ items, onChange, disabled }: Props) {
|
||||
export default function StorePackagesForm({ items, onChange, disabled, embedded }: Props) {
|
||||
function updateAt(index: number, patch: Partial<PackageFormItem>) {
|
||||
const next = items.map((item, i) => (i === index ? { ...item, ...patch } : item));
|
||||
onChange(next);
|
||||
@@ -27,76 +28,97 @@ export default function StorePackagesForm({ items, onChange, disabled }: Props)
|
||||
const list = items.length ? items : [emptyPackage(0)];
|
||||
|
||||
return (
|
||||
<div className="partner-packages-form">
|
||||
<div className={`partner-packages-form${embedded ? ' partner-packages-form--embedded' : ''}`}>
|
||||
{list.map((item, index) => (
|
||||
<div key={index} className="partner-card" style={{ marginBottom: 12 }}>
|
||||
<div className="partner-card-title" style={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||
<span>套餐 {index + 1}</span>
|
||||
<section
|
||||
key={index}
|
||||
className="partner-form-card partner-packages-item"
|
||||
style={embedded ? { marginLeft: 0, marginRight: 0 } : undefined}
|
||||
>
|
||||
<div className="partner-packages-item-head">
|
||||
<div className="partner-section-title" style={{ marginBottom: 0 }}>
|
||||
<div className="partner-section-bar" />
|
||||
<h2 className="headline-md">套餐 {index + 1}</h2>
|
||||
</div>
|
||||
{!disabled && list.length > 1 ? (
|
||||
<button type="button" className="partner-link-btn" onClick={() => removeAt(index)}>
|
||||
<button type="button" className="partner-packages-remove" onClick={() => removeAt(index)}>
|
||||
删除
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
<label className="partner-field">
|
||||
<span className="partner-label">套餐名称</span>
|
||||
<input
|
||||
className="partner-input"
|
||||
value={item.name}
|
||||
disabled={disabled}
|
||||
placeholder="如:套餐A"
|
||||
onChange={(e) => updateAt(index, { name: e.target.value })}
|
||||
/>
|
||||
</label>
|
||||
<label className="partner-field">
|
||||
<span className="partner-label">价格(元)</span>
|
||||
<input
|
||||
className="partner-input"
|
||||
type="number"
|
||||
min={0}
|
||||
step={0.01}
|
||||
value={item.price}
|
||||
disabled={disabled}
|
||||
placeholder="198"
|
||||
onChange={(e) => updateAt(index, { price: e.target.value })}
|
||||
/>
|
||||
</label>
|
||||
<label className="partner-field">
|
||||
<span className="partner-label">菜品</span>
|
||||
|
||||
<div className="partner-field">
|
||||
<label>套餐名称 <span className="text-primary">*</span></label>
|
||||
<div className="partner-field-input">
|
||||
<span className="material-symbols-outlined">restaurant</span>
|
||||
<input
|
||||
placeholder="如:套餐A"
|
||||
value={item.name}
|
||||
disabled={disabled}
|
||||
onChange={(e) => updateAt(index, { name: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="partner-field">
|
||||
<label>价格(元) <span className="text-primary">*</span></label>
|
||||
<div className="partner-field-input">
|
||||
<span className="material-symbols-outlined">payments</span>
|
||||
<input
|
||||
type="number"
|
||||
min={0}
|
||||
step={0.01}
|
||||
placeholder="198"
|
||||
value={item.price}
|
||||
disabled={disabled}
|
||||
onChange={(e) => updateAt(index, { price: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="partner-field">
|
||||
<label>菜品 <span className="text-primary">*</span></label>
|
||||
<textarea
|
||||
className="partner-input"
|
||||
rows={2}
|
||||
rows={3}
|
||||
placeholder="红烧肉、红烧鱼、油焖茄子"
|
||||
value={item.dishes}
|
||||
disabled={disabled}
|
||||
placeholder="红烧肉、红烧鱼、油焖茄子"
|
||||
onChange={(e) => updateAt(index, { dishes: e.target.value })}
|
||||
/>
|
||||
</label>
|
||||
<label className="partner-field">
|
||||
<span className="partner-label">使用时间</span>
|
||||
<input
|
||||
className="partner-input"
|
||||
value={item.usableTime || ''}
|
||||
disabled={disabled}
|
||||
placeholder="节假日除外"
|
||||
onChange={(e) => updateAt(index, { usableTime: e.target.value })}
|
||||
/>
|
||||
</label>
|
||||
<label className="partner-field">
|
||||
<span className="partner-label">其他说明</span>
|
||||
<input
|
||||
className="partner-input"
|
||||
value={item.otherNotes || ''}
|
||||
disabled={disabled}
|
||||
placeholder="不可叠加"
|
||||
onChange={(e) => updateAt(index, { otherNotes: e.target.value })}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="partner-field">
|
||||
<label>使用时间</label>
|
||||
<div className="partner-field-input">
|
||||
<span className="material-symbols-outlined">schedule</span>
|
||||
<input
|
||||
placeholder="节假日除外"
|
||||
value={item.usableTime || ''}
|
||||
disabled={disabled}
|
||||
onChange={(e) => updateAt(index, { usableTime: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="partner-field">
|
||||
<label>其他说明</label>
|
||||
<div className="partner-field-input">
|
||||
<span className="material-symbols-outlined">info</span>
|
||||
<input
|
||||
placeholder="不可叠加"
|
||||
value={item.otherNotes || ''}
|
||||
disabled={disabled}
|
||||
onChange={(e) => updateAt(index, { otherNotes: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
))}
|
||||
|
||||
{!disabled && list.length < STORE_PACKAGE_MAX_COUNT ? (
|
||||
<button type="button" className="partner-btn-outline" onClick={addItem}>
|
||||
+ 添加套餐
|
||||
<button type="button" className="partner-packages-add" onClick={addItem}>
|
||||
<span className="material-symbols-outlined">add_circle</span>
|
||||
添加套餐
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
@@ -102,6 +102,7 @@ export default function StoreCreatePage() {
|
||||
const [categoryTree, setCategoryTree] = useState<StoreCategoryNode[]>([]);
|
||||
|
||||
const [mapPickerOpen, setMapPickerOpen] = useState(false);
|
||||
const [stepErrorModal, setStepErrorModal] = useState<string | null>(null);
|
||||
|
||||
const draftSaveDisabledRef = useRef(false);
|
||||
|
||||
@@ -109,6 +110,11 @@ export default function StoreCreatePage() {
|
||||
setSubmitError(message);
|
||||
}
|
||||
|
||||
function reportStepError(message: string) {
|
||||
setSubmitError(message);
|
||||
setStepErrorModal(message);
|
||||
}
|
||||
|
||||
const stepFromUrl = Number(params.get('step') || 0);
|
||||
|
||||
const step = stepFromUrl >= 1 && stepFromUrl <= 4 ? stepFromUrl : (saved?.step ?? 1);
|
||||
@@ -301,7 +307,7 @@ export default function StoreCreatePage() {
|
||||
const msg = validateStoreStep1(form);
|
||||
|
||||
if (msg) {
|
||||
reportFormError(msg);
|
||||
reportStepError(msg);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -312,7 +318,7 @@ export default function StoreCreatePage() {
|
||||
const msg = validateStoreStep2(form);
|
||||
|
||||
if (msg) {
|
||||
reportFormError(msg);
|
||||
reportStepError(msg);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -323,9 +329,10 @@ export default function StoreCreatePage() {
|
||||
if (msg) {
|
||||
if (isPhoneValidationMessage(msg)) {
|
||||
setFieldErrors({ phone: msg });
|
||||
reportStepError(msg);
|
||||
return;
|
||||
}
|
||||
reportFormError(msg);
|
||||
reportStepError(msg);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -1128,13 +1135,19 @@ export default function StoreCreatePage() {
|
||||
|
||||
{step === 4 && (
|
||||
<>
|
||||
<section className="partner-section">
|
||||
<h2 className="partner-section-title">门店套餐(可选)</h2>
|
||||
<p className="partner-form-hint">可不填;提交后将进入总部审核。也可跳过此步。</p>
|
||||
<section className="partner-form-card">
|
||||
<div className="partner-section-title">
|
||||
<div className="partner-section-bar" />
|
||||
<h2 className="headline-md">门店套餐</h2>
|
||||
</div>
|
||||
<p className="label-md text-muted" style={{ marginBottom: 16 }}>
|
||||
可选填写;提交后将进入总部审核。也可在最后一页点击「跳过」。
|
||||
</p>
|
||||
<StorePackagesForm
|
||||
items={form.packages?.length ? form.packages : [{ name: '', price: '', dishes: '', usableTime: '', otherNotes: '', sortOrder: 0 }]}
|
||||
onChange={(packages) => setForm((prev) => ({ ...prev, packages }))}
|
||||
disabled={submitting}
|
||||
embedded
|
||||
/>
|
||||
</section>
|
||||
</>
|
||||
@@ -1181,6 +1194,22 @@ export default function StoreCreatePage() {
|
||||
|
||||
</footer>
|
||||
|
||||
{stepErrorModal ? (
|
||||
<div className="partner-ship-modal-backdrop" role="presentation" onClick={() => setStepErrorModal(null)}>
|
||||
<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.6, whiteSpace: 'pre-wrap' }}>
|
||||
{stepErrorModal}
|
||||
</p>
|
||||
<div className="partner-ship-actions">
|
||||
<button type="button" className="partner-btn-primary" onClick={() => setStepErrorModal(null)}>
|
||||
我知道了
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<TencentLocPickerOverlay
|
||||
open={mapPickerOpen}
|
||||
onClose={() => setMapPickerOpen(false)}
|
||||
|
||||
@@ -69,36 +69,66 @@ export default function StorePackagesPage() {
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="partner-page">
|
||||
<p className="partner-muted">加载中…</p>
|
||||
<div className="partner-detail-page partner-home--flush-top">
|
||||
<main style={{ padding: '24px 20px' }}>
|
||||
<p className="label-md text-muted">加载中…</p>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="partner-page partner-page-sticky">
|
||||
<header className="partner-subheader">
|
||||
<button type="button" className="partner-back" onClick={() => navigate(`/stores/${id}`)}>
|
||||
返回
|
||||
<div className="partner-page-sticky partner-home--flush-top">
|
||||
<header className="partner-packages-page-header">
|
||||
<button type="button" className="partner-packages-back" onClick={() => navigate(`/stores/${id}`)}>
|
||||
<span className="material-symbols-outlined">arrow_back</span>
|
||||
</button>
|
||||
<h1 className="partner-subheader-title">门店套餐</h1>
|
||||
<h1 className="headline-md">门店套餐</h1>
|
||||
</header>
|
||||
|
||||
{error ? <p className="partner-form-error">{error}</p> : null}
|
||||
{pending?.status === 'PENDING' ? (
|
||||
<p className="partner-form-hint">套餐变更审核中,C 端仍展示上一版生效套餐。</p>
|
||||
) : null}
|
||||
{pending?.status === 'REJECTED' && pending.rejectReason ? (
|
||||
<p className="partner-form-error">上次驳回:{pending.rejectReason}</p>
|
||||
) : null}
|
||||
<main className="partner-packages-page-main">
|
||||
{error ? <p className="partner-form-error" role="alert">{error}</p> : null}
|
||||
|
||||
<StorePackagesForm
|
||||
items={items}
|
||||
onChange={setItems}
|
||||
disabled={pending?.status === 'PENDING' || submitting}
|
||||
/>
|
||||
{pending?.status === 'PENDING' ? (
|
||||
<div className="partner-info-banner">
|
||||
<span className="material-symbols-outlined" style={{ color: 'var(--color-heritage-red)' }}>hourglass_top</span>
|
||||
<p className="body-md">套餐变更审核中,C 端仍展示上一版生效套餐。</p>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="partner-sticky-footer">
|
||||
{pending?.status === 'REJECTED' && pending.rejectReason ? (
|
||||
<div className="partner-info-banner" style={{ background: 'rgba(180,35,24,0.06)', borderColor: 'rgba(180,35,24,0.15)' }}>
|
||||
<span className="material-symbols-outlined" style={{ color: 'var(--color-heritage-red)' }}>error</span>
|
||||
<p className="body-md">上次驳回:{pending.rejectReason}</p>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<section className="partner-form-card">
|
||||
<div className="partner-section-title">
|
||||
<div className="partner-section-bar" />
|
||||
<h2 className="headline-md">套餐信息</h2>
|
||||
</div>
|
||||
<p className="label-md text-muted" style={{ marginBottom: 16 }}>
|
||||
提交后将由总部审核,通过后 C 端展示生效套餐。
|
||||
</p>
|
||||
<StorePackagesForm
|
||||
items={items}
|
||||
onChange={setItems}
|
||||
disabled={pending?.status === 'PENDING' || submitting}
|
||||
embedded
|
||||
/>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<footer className="partner-sticky-footer">
|
||||
<button
|
||||
type="button"
|
||||
className="partner-btn-outline"
|
||||
onClick={() => navigate(`/stores/${id}`)}
|
||||
disabled={submitting}
|
||||
>
|
||||
返回
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="partner-btn-primary"
|
||||
@@ -107,7 +137,7 @@ export default function StorePackagesPage() {
|
||||
>
|
||||
{submitting ? '提交中…' : '提交审核'}
|
||||
</button>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3634,3 +3634,79 @@ header:has(> .app-page-title:only-child),
|
||||
border: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
/* ─── 门店套餐 ─── */
|
||||
.partner-packages-page-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 12px var(--space-page) 8px;
|
||||
background: var(--color-surface);
|
||||
border-bottom: 1px solid rgba(226, 190, 188, 0.12);
|
||||
}
|
||||
|
||||
.partner-packages-back {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--color-on-surface);
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.partner-packages-page-main {
|
||||
padding-bottom: 88px;
|
||||
}
|
||||
|
||||
.partner-packages-form--embedded .partner-packages-item {
|
||||
margin-bottom: var(--space-md);
|
||||
box-shadow: none;
|
||||
border: 1px solid rgba(226, 190, 188, 0.2);
|
||||
}
|
||||
|
||||
.partner-packages-form--embedded .partner-packages-item:last-of-type {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.partner-packages-item-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: var(--space-md);
|
||||
}
|
||||
|
||||
.partner-packages-remove {
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--color-heritage-red);
|
||||
font-size: 13px;
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
.partner-packages-add {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
width: calc(100% - var(--space-page) * 2);
|
||||
margin: 0 var(--space-page) var(--space-md);
|
||||
min-height: 48px;
|
||||
border: 1px dashed rgba(166, 29, 36, 0.35);
|
||||
border-radius: var(--radius-md);
|
||||
background: rgba(166, 29, 36, 0.04);
|
||||
color: var(--color-heritage-red);
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.partner-packages-add .material-symbols-outlined {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.partner-packages-form:not(.partner-packages-form--embedded) .partner-packages-item {
|
||||
margin-left: var(--space-page);
|
||||
margin-right: var(--space-page);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user