style(store): 统一门店套餐页 UI 与拓店校验弹窗
管理后台套餐 Tab 对齐 Ant Design 表单;合伙人/门店端套餐表单与基本信息页风格一致;拓店下一步校验增加弹窗提示。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { Button, Space, Typography, message } from 'antd';
|
import { Alert, Button, Form, Input, InputNumber, Space, Typography, message } from 'antd';
|
||||||
import type { StorePackageItemDto } from '@dukang/shared-types';
|
import type { StorePackageItemDto } from '@dukang/shared-types';
|
||||||
import { STORE_PACKAGE_MAX_COUNT } from '@dukang/shared-types';
|
import { STORE_PACKAGE_MAX_COUNT } from '@dukang/shared-types';
|
||||||
import { request } from '../lib/api';
|
import { request } from '../lib/api';
|
||||||
@@ -87,68 +87,101 @@ export default function AdminStorePackagesSection({ storeId }: { storeId: string
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (loading) return <Typography.Text type="secondary">加载套餐中…</Typography.Text>;
|
if (loading) {
|
||||||
|
return <Typography.Text type="secondary">加载套餐中…</Typography.Text>;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<Form layout="vertical" requiredMark={false}>
|
||||||
<Typography.Paragraph type="secondary">
|
<Alert
|
||||||
总部直存立即生效,无需审核。最多 {STORE_PACKAGE_MAX_COUNT} 条。
|
type="info"
|
||||||
</Typography.Paragraph>
|
showIcon
|
||||||
|
style={{ marginBottom: 16 }}
|
||||||
|
message={`总部直存立即生效,无需审核。同一门店最多 ${STORE_PACKAGE_MAX_COUNT} 条套餐。`}
|
||||||
|
/>
|
||||||
|
|
||||||
{items.map((item, index) => (
|
{items.map((item, index) => (
|
||||||
<div key={index} style={{ border: '1px solid #f0f0f0', borderRadius: 8, padding: 12, marginBottom: 12 }}>
|
<div
|
||||||
<Space style={{ width: '100%', justifyContent: 'space-between', marginBottom: 8 }}>
|
key={index}
|
||||||
<Typography.Text strong>套餐 {index + 1}</Typography.Text>
|
style={{
|
||||||
|
marginBottom: 16,
|
||||||
|
padding: 16,
|
||||||
|
border: '1px solid #f0f0f0',
|
||||||
|
borderRadius: 8,
|
||||||
|
background: '#fafafa',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Space style={{ width: '100%', justifyContent: 'space-between', marginBottom: 12 }}>
|
||||||
|
<Typography.Title level={5} style={{ margin: 0 }}>
|
||||||
|
套餐 {index + 1}
|
||||||
|
</Typography.Title>
|
||||||
{items.length > 1 ? (
|
{items.length > 1 ? (
|
||||||
<Button type="link" danger onClick={() => removeAt(index)}>
|
<Button type="link" danger onClick={() => removeAt(index)}>
|
||||||
删除
|
删除
|
||||||
</Button>
|
</Button>
|
||||||
) : null}
|
) : null}
|
||||||
</Space>
|
</Space>
|
||||||
<Space direction="vertical" style={{ width: '100%' }}>
|
|
||||||
<input
|
<Form.Item label="套餐名称" required style={{ marginBottom: 12 }}>
|
||||||
placeholder="套餐名称"
|
<Input
|
||||||
|
placeholder="如:套餐A"
|
||||||
value={item.name}
|
value={item.name}
|
||||||
onChange={(e) => updateAt(index, { name: e.target.value })}
|
onChange={(e) => updateAt(index, { name: e.target.value })}
|
||||||
style={{ width: '100%', padding: 8 }}
|
|
||||||
/>
|
/>
|
||||||
<input
|
</Form.Item>
|
||||||
placeholder="价格(元)"
|
|
||||||
type="number"
|
<Form.Item label="价格(元)" required style={{ marginBottom: 12 }}>
|
||||||
|
<InputNumber
|
||||||
min={0}
|
min={0}
|
||||||
value={item.price}
|
precision={2}
|
||||||
onChange={(e) => updateAt(index, { price: e.target.value })}
|
style={{ width: '100%' }}
|
||||||
style={{ width: '100%', padding: 8 }}
|
addonAfter="元"
|
||||||
|
placeholder="198"
|
||||||
|
value={item.price === '' ? undefined : Number(item.price)}
|
||||||
|
onChange={(v) => updateAt(index, { price: v != null ? String(v) : '' })}
|
||||||
/>
|
/>
|
||||||
<textarea
|
</Form.Item>
|
||||||
placeholder="菜品"
|
|
||||||
|
<Form.Item label="菜品" required style={{ marginBottom: 12 }}>
|
||||||
|
<Input.TextArea
|
||||||
rows={2}
|
rows={2}
|
||||||
|
placeholder="红烧肉、红烧鱼、油焖茄子"
|
||||||
value={item.dishes}
|
value={item.dishes}
|
||||||
onChange={(e) => updateAt(index, { dishes: e.target.value })}
|
onChange={(e) => updateAt(index, { dishes: e.target.value })}
|
||||||
style={{ width: '100%', padding: 8 }}
|
|
||||||
/>
|
/>
|
||||||
<input
|
</Form.Item>
|
||||||
placeholder="使用时间"
|
|
||||||
|
<Form.Item label="使用时间" style={{ marginBottom: 12 }}>
|
||||||
|
<Input
|
||||||
|
placeholder="节假日除外"
|
||||||
value={item.usableTime || ''}
|
value={item.usableTime || ''}
|
||||||
onChange={(e) => updateAt(index, { usableTime: e.target.value })}
|
onChange={(e) => updateAt(index, { usableTime: e.target.value })}
|
||||||
style={{ width: '100%', padding: 8 }}
|
|
||||||
/>
|
/>
|
||||||
<input
|
</Form.Item>
|
||||||
placeholder="其他说明"
|
|
||||||
|
<Form.Item label="其他说明" style={{ marginBottom: 0 }}>
|
||||||
|
<Input
|
||||||
|
placeholder="不可叠加"
|
||||||
value={item.otherNotes || ''}
|
value={item.otherNotes || ''}
|
||||||
onChange={(e) => updateAt(index, { otherNotes: e.target.value })}
|
onChange={(e) => updateAt(index, { otherNotes: e.target.value })}
|
||||||
style={{ width: '100%', padding: 8 }}
|
|
||||||
/>
|
/>
|
||||||
</Space>
|
</Form.Item>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
{items.length < STORE_PACKAGE_MAX_COUNT ? (
|
{items.length < STORE_PACKAGE_MAX_COUNT ? (
|
||||||
<Button onClick={addRow} style={{ marginBottom: 12 }}>
|
<Button onClick={addRow} style={{ marginBottom: 16 }}>
|
||||||
添加套餐
|
添加套餐
|
||||||
</Button>
|
</Button>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
|
<Typography.Paragraph type="secondary" style={{ marginBottom: 12 }}>
|
||||||
|
修改后点击下方按钮保存,C 端将立即展示生效套餐。
|
||||||
|
</Typography.Paragraph>
|
||||||
|
|
||||||
<Button type="primary" loading={saving} onClick={() => void save()}>
|
<Button type="primary" loading={saving} onClick={() => void save()}>
|
||||||
保存套餐
|
保存套餐
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</Form>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { useState } from 'react';
|
|
||||||
import type { PackageFormItem } from '../lib/storePackages';
|
import type { PackageFormItem } from '../lib/storePackages';
|
||||||
import { STORE_PACKAGE_MAX_COUNT } from '@dukang/shared-types';
|
import { STORE_PACKAGE_MAX_COUNT } from '@dukang/shared-types';
|
||||||
import { emptyPackage } from '../lib/storePackages';
|
import { emptyPackage } from '../lib/storePackages';
|
||||||
@@ -7,9 +6,11 @@ type Props = {
|
|||||||
items: PackageFormItem[];
|
items: PackageFormItem[];
|
||||||
onChange: (items: PackageFormItem[]) => void;
|
onChange: (items: PackageFormItem[]) => void;
|
||||||
disabled?: boolean;
|
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>) {
|
function updateAt(index: number, patch: Partial<PackageFormItem>) {
|
||||||
const next = items.map((item, i) => (i === index ? { ...item, ...patch } : item));
|
const next = items.map((item, i) => (i === index ? { ...item, ...patch } : item));
|
||||||
onChange(next);
|
onChange(next);
|
||||||
@@ -27,76 +28,97 @@ export default function StorePackagesForm({ items, onChange, disabled }: Props)
|
|||||||
const list = items.length ? items : [emptyPackage(0)];
|
const list = items.length ? items : [emptyPackage(0)];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="partner-packages-form">
|
<div className={`partner-packages-form${embedded ? ' partner-packages-form--embedded' : ''}`}>
|
||||||
{list.map((item, index) => (
|
{list.map((item, index) => (
|
||||||
<div key={index} className="partner-card" style={{ marginBottom: 12 }}>
|
<section
|
||||||
<div className="partner-card-title" style={{ display: 'flex', justifyContent: 'space-between' }}>
|
key={index}
|
||||||
<span>套餐 {index + 1}</span>
|
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 ? (
|
{!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>
|
</button>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
<label className="partner-field">
|
|
||||||
<span className="partner-label">套餐名称</span>
|
<div className="partner-field">
|
||||||
<input
|
<label>套餐名称 <span className="text-primary">*</span></label>
|
||||||
className="partner-input"
|
<div className="partner-field-input">
|
||||||
value={item.name}
|
<span className="material-symbols-outlined">restaurant</span>
|
||||||
disabled={disabled}
|
<input
|
||||||
placeholder="如:套餐A"
|
placeholder="如:套餐A"
|
||||||
onChange={(e) => updateAt(index, { name: e.target.value })}
|
value={item.name}
|
||||||
/>
|
disabled={disabled}
|
||||||
</label>
|
onChange={(e) => updateAt(index, { name: e.target.value })}
|
||||||
<label className="partner-field">
|
/>
|
||||||
<span className="partner-label">价格(元)</span>
|
</div>
|
||||||
<input
|
</div>
|
||||||
className="partner-input"
|
|
||||||
type="number"
|
<div className="partner-field">
|
||||||
min={0}
|
<label>价格(元) <span className="text-primary">*</span></label>
|
||||||
step={0.01}
|
<div className="partner-field-input">
|
||||||
value={item.price}
|
<span className="material-symbols-outlined">payments</span>
|
||||||
disabled={disabled}
|
<input
|
||||||
placeholder="198"
|
type="number"
|
||||||
onChange={(e) => updateAt(index, { price: e.target.value })}
|
min={0}
|
||||||
/>
|
step={0.01}
|
||||||
</label>
|
placeholder="198"
|
||||||
<label className="partner-field">
|
value={item.price}
|
||||||
<span className="partner-label">菜品</span>
|
disabled={disabled}
|
||||||
|
onChange={(e) => updateAt(index, { price: e.target.value })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="partner-field">
|
||||||
|
<label>菜品 <span className="text-primary">*</span></label>
|
||||||
<textarea
|
<textarea
|
||||||
className="partner-input"
|
rows={3}
|
||||||
rows={2}
|
placeholder="红烧肉、红烧鱼、油焖茄子"
|
||||||
value={item.dishes}
|
value={item.dishes}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
placeholder="红烧肉、红烧鱼、油焖茄子"
|
|
||||||
onChange={(e) => updateAt(index, { dishes: e.target.value })}
|
onChange={(e) => updateAt(index, { dishes: e.target.value })}
|
||||||
/>
|
/>
|
||||||
</label>
|
</div>
|
||||||
<label className="partner-field">
|
|
||||||
<span className="partner-label">使用时间</span>
|
<div className="partner-field">
|
||||||
<input
|
<label>使用时间</label>
|
||||||
className="partner-input"
|
<div className="partner-field-input">
|
||||||
value={item.usableTime || ''}
|
<span className="material-symbols-outlined">schedule</span>
|
||||||
disabled={disabled}
|
<input
|
||||||
placeholder="节假日除外"
|
placeholder="节假日除外"
|
||||||
onChange={(e) => updateAt(index, { usableTime: e.target.value })}
|
value={item.usableTime || ''}
|
||||||
/>
|
disabled={disabled}
|
||||||
</label>
|
onChange={(e) => updateAt(index, { usableTime: e.target.value })}
|
||||||
<label className="partner-field">
|
/>
|
||||||
<span className="partner-label">其他说明</span>
|
</div>
|
||||||
<input
|
</div>
|
||||||
className="partner-input"
|
|
||||||
value={item.otherNotes || ''}
|
<div className="partner-field">
|
||||||
disabled={disabled}
|
<label>其他说明</label>
|
||||||
placeholder="不可叠加"
|
<div className="partner-field-input">
|
||||||
onChange={(e) => updateAt(index, { otherNotes: e.target.value })}
|
<span className="material-symbols-outlined">info</span>
|
||||||
/>
|
<input
|
||||||
</label>
|
placeholder="不可叠加"
|
||||||
</div>
|
value={item.otherNotes || ''}
|
||||||
|
disabled={disabled}
|
||||||
|
onChange={(e) => updateAt(index, { otherNotes: e.target.value })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
{!disabled && list.length < STORE_PACKAGE_MAX_COUNT ? (
|
{!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>
|
</button>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -102,6 +102,7 @@ export default function StoreCreatePage() {
|
|||||||
const [categoryTree, setCategoryTree] = useState<StoreCategoryNode[]>([]);
|
const [categoryTree, setCategoryTree] = useState<StoreCategoryNode[]>([]);
|
||||||
|
|
||||||
const [mapPickerOpen, setMapPickerOpen] = useState(false);
|
const [mapPickerOpen, setMapPickerOpen] = useState(false);
|
||||||
|
const [stepErrorModal, setStepErrorModal] = useState<string | null>(null);
|
||||||
|
|
||||||
const draftSaveDisabledRef = useRef(false);
|
const draftSaveDisabledRef = useRef(false);
|
||||||
|
|
||||||
@@ -109,6 +110,11 @@ export default function StoreCreatePage() {
|
|||||||
setSubmitError(message);
|
setSubmitError(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function reportStepError(message: string) {
|
||||||
|
setSubmitError(message);
|
||||||
|
setStepErrorModal(message);
|
||||||
|
}
|
||||||
|
|
||||||
const stepFromUrl = Number(params.get('step') || 0);
|
const stepFromUrl = Number(params.get('step') || 0);
|
||||||
|
|
||||||
const step = stepFromUrl >= 1 && stepFromUrl <= 4 ? stepFromUrl : (saved?.step ?? 1);
|
const step = stepFromUrl >= 1 && stepFromUrl <= 4 ? stepFromUrl : (saved?.step ?? 1);
|
||||||
@@ -301,7 +307,7 @@ export default function StoreCreatePage() {
|
|||||||
const msg = validateStoreStep1(form);
|
const msg = validateStoreStep1(form);
|
||||||
|
|
||||||
if (msg) {
|
if (msg) {
|
||||||
reportFormError(msg);
|
reportStepError(msg);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -312,7 +318,7 @@ export default function StoreCreatePage() {
|
|||||||
const msg = validateStoreStep2(form);
|
const msg = validateStoreStep2(form);
|
||||||
|
|
||||||
if (msg) {
|
if (msg) {
|
||||||
reportFormError(msg);
|
reportStepError(msg);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -323,9 +329,10 @@ export default function StoreCreatePage() {
|
|||||||
if (msg) {
|
if (msg) {
|
||||||
if (isPhoneValidationMessage(msg)) {
|
if (isPhoneValidationMessage(msg)) {
|
||||||
setFieldErrors({ phone: msg });
|
setFieldErrors({ phone: msg });
|
||||||
|
reportStepError(msg);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
reportFormError(msg);
|
reportStepError(msg);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1128,13 +1135,19 @@ export default function StoreCreatePage() {
|
|||||||
|
|
||||||
{step === 4 && (
|
{step === 4 && (
|
||||||
<>
|
<>
|
||||||
<section className="partner-section">
|
<section className="partner-form-card">
|
||||||
<h2 className="partner-section-title">门店套餐(可选)</h2>
|
<div className="partner-section-title">
|
||||||
<p className="partner-form-hint">可不填;提交后将进入总部审核。也可跳过此步。</p>
|
<div className="partner-section-bar" />
|
||||||
|
<h2 className="headline-md">门店套餐</h2>
|
||||||
|
</div>
|
||||||
|
<p className="label-md text-muted" style={{ marginBottom: 16 }}>
|
||||||
|
可选填写;提交后将进入总部审核。也可在最后一页点击「跳过」。
|
||||||
|
</p>
|
||||||
<StorePackagesForm
|
<StorePackagesForm
|
||||||
items={form.packages?.length ? form.packages : [{ name: '', price: '', dishes: '', usableTime: '', otherNotes: '', sortOrder: 0 }]}
|
items={form.packages?.length ? form.packages : [{ name: '', price: '', dishes: '', usableTime: '', otherNotes: '', sortOrder: 0 }]}
|
||||||
onChange={(packages) => setForm((prev) => ({ ...prev, packages }))}
|
onChange={(packages) => setForm((prev) => ({ ...prev, packages }))}
|
||||||
disabled={submitting}
|
disabled={submitting}
|
||||||
|
embedded
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
</>
|
</>
|
||||||
@@ -1181,6 +1194,22 @@ export default function StoreCreatePage() {
|
|||||||
|
|
||||||
</footer>
|
</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
|
<TencentLocPickerOverlay
|
||||||
open={mapPickerOpen}
|
open={mapPickerOpen}
|
||||||
onClose={() => setMapPickerOpen(false)}
|
onClose={() => setMapPickerOpen(false)}
|
||||||
|
|||||||
@@ -69,36 +69,66 @@ export default function StorePackagesPage() {
|
|||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className="partner-page">
|
<div className="partner-detail-page partner-home--flush-top">
|
||||||
<p className="partner-muted">加载中…</p>
|
<main style={{ padding: '24px 20px' }}>
|
||||||
|
<p className="label-md text-muted">加载中…</p>
|
||||||
|
</main>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="partner-page partner-page-sticky">
|
<div className="partner-page-sticky partner-home--flush-top">
|
||||||
<header className="partner-subheader">
|
<header className="partner-packages-page-header">
|
||||||
<button type="button" className="partner-back" onClick={() => navigate(`/stores/${id}`)}>
|
<button type="button" className="partner-packages-back" onClick={() => navigate(`/stores/${id}`)}>
|
||||||
返回
|
<span className="material-symbols-outlined">arrow_back</span>
|
||||||
</button>
|
</button>
|
||||||
<h1 className="partner-subheader-title">门店套餐</h1>
|
<h1 className="headline-md">门店套餐</h1>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
{error ? <p className="partner-form-error">{error}</p> : null}
|
<main className="partner-packages-page-main">
|
||||||
{pending?.status === 'PENDING' ? (
|
{error ? <p className="partner-form-error" role="alert">{error}</p> : null}
|
||||||
<p className="partner-form-hint">套餐变更审核中,C 端仍展示上一版生效套餐。</p>
|
|
||||||
) : null}
|
|
||||||
{pending?.status === 'REJECTED' && pending.rejectReason ? (
|
|
||||||
<p className="partner-form-error">上次驳回:{pending.rejectReason}</p>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
<StorePackagesForm
|
{pending?.status === 'PENDING' ? (
|
||||||
items={items}
|
<div className="partner-info-banner">
|
||||||
onChange={setItems}
|
<span className="material-symbols-outlined" style={{ color: 'var(--color-heritage-red)' }}>hourglass_top</span>
|
||||||
disabled={pending?.status === 'PENDING' || submitting}
|
<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
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="partner-btn-primary"
|
className="partner-btn-primary"
|
||||||
@@ -107,7 +137,7 @@ export default function StorePackagesPage() {
|
|||||||
>
|
>
|
||||||
{submitting ? '提交中…' : '提交审核'}
|
{submitting ? '提交中…' : '提交审核'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3634,3 +3634,79 @@ header:has(> .app-page-title:only-child),
|
|||||||
border: none !important;
|
border: none !important;
|
||||||
box-shadow: 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);
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,108 @@
|
|||||||
|
import { STORE_PACKAGE_MAX_COUNT } from '@dukang/shared-types';
|
||||||
|
import { emptyPackage, type PackageFormItem } from '../lib/storePackages';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
items: PackageFormItem[];
|
||||||
|
onChange: (items: PackageFormItem[]) => void;
|
||||||
|
disabled?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function ShopPackagesForm({ items, onChange, disabled }: Props) {
|
||||||
|
function updateAt(index: number, patch: Partial<PackageFormItem>) {
|
||||||
|
onChange(items.map((item, i) => (i === index ? { ...item, ...patch } : item)));
|
||||||
|
}
|
||||||
|
|
||||||
|
function addItem() {
|
||||||
|
if (items.length >= STORE_PACKAGE_MAX_COUNT) return;
|
||||||
|
onChange([...items, emptyPackage(items.length)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeAt(index: number) {
|
||||||
|
onChange(items.filter((_, i) => i !== index).map((item, i) => ({ ...item, sortOrder: i })));
|
||||||
|
}
|
||||||
|
|
||||||
|
const list = items.length ? items : [emptyPackage(0)];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="shop-packages-form">
|
||||||
|
{list.map((item, index) => (
|
||||||
|
<section key={index} className="shop-packages-card">
|
||||||
|
<div className="shop-packages-card-head">
|
||||||
|
<h3 className="shop-packages-card-title">套餐 {index + 1}</h3>
|
||||||
|
{!disabled && list.length > 1 ? (
|
||||||
|
<button type="button" className="shop-packages-remove" onClick={() => removeAt(index)}>
|
||||||
|
删除
|
||||||
|
</button>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<label className="shop-packages-field">
|
||||||
|
<span className="shop-packages-label">套餐名称 *</span>
|
||||||
|
<input
|
||||||
|
className="shop-packages-input"
|
||||||
|
placeholder="如:套餐A"
|
||||||
|
value={item.name}
|
||||||
|
disabled={disabled}
|
||||||
|
onChange={(e) => updateAt(index, { name: e.target.value })}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label className="shop-packages-field">
|
||||||
|
<span className="shop-packages-label">价格(元) *</span>
|
||||||
|
<input
|
||||||
|
className="shop-packages-input"
|
||||||
|
type="number"
|
||||||
|
min={0}
|
||||||
|
step={0.01}
|
||||||
|
placeholder="198"
|
||||||
|
value={item.price}
|
||||||
|
disabled={disabled}
|
||||||
|
onChange={(e) => updateAt(index, { price: e.target.value })}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label className="shop-packages-field">
|
||||||
|
<span className="shop-packages-label">菜品 *</span>
|
||||||
|
<textarea
|
||||||
|
className="shop-packages-textarea"
|
||||||
|
rows={3}
|
||||||
|
placeholder="红烧肉、红烧鱼、油焖茄子"
|
||||||
|
value={item.dishes}
|
||||||
|
disabled={disabled}
|
||||||
|
onChange={(e) => updateAt(index, { dishes: e.target.value })}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label className="shop-packages-field">
|
||||||
|
<span className="shop-packages-label">使用时间</span>
|
||||||
|
<input
|
||||||
|
className="shop-packages-input"
|
||||||
|
placeholder="节假日除外"
|
||||||
|
value={item.usableTime || ''}
|
||||||
|
disabled={disabled}
|
||||||
|
onChange={(e) => updateAt(index, { usableTime: e.target.value })}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label className="shop-packages-field">
|
||||||
|
<span className="shop-packages-label">其他说明</span>
|
||||||
|
<input
|
||||||
|
className="shop-packages-input"
|
||||||
|
placeholder="不可叠加"
|
||||||
|
value={item.otherNotes || ''}
|
||||||
|
disabled={disabled}
|
||||||
|
onChange={(e) => updateAt(index, { otherNotes: e.target.value })}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</section>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{!disabled && list.length < STORE_PACKAGE_MAX_COUNT ? (
|
||||||
|
<button type="button" className="shop-packages-add" onClick={addItem}>
|
||||||
|
<span className="material-symbols-outlined">add_circle</span>
|
||||||
|
添加套餐
|
||||||
|
</button>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import type { StorePackagesResponse } from '@dukang/shared-types';
|
import type { StorePackagesResponse } from '@dukang/shared-types';
|
||||||
import { STORE_PACKAGE_MAX_COUNT } from '@dukang/shared-types';
|
import PullToRefresh from '@dukang/shared-ui/PullToRefresh';
|
||||||
|
import ShopPackagesForm from '../components/ShopPackagesForm';
|
||||||
import { request } from '../lib/api';
|
import { request } from '../lib/api';
|
||||||
import {
|
import {
|
||||||
emptyPackage,
|
emptyPackage,
|
||||||
@@ -16,49 +17,43 @@ export default function PackagesPage() {
|
|||||||
const [pending, setPending] = useState<StorePackagesResponse['pendingRequest']>(null);
|
const [pending, setPending] = useState<StorePackagesResponse['pendingRequest']>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [submitting, setSubmitting] = useState(false);
|
const [submitting, setSubmitting] = useState(false);
|
||||||
const [error, setError] = useState('');
|
const [msg, setMsg] = useState('');
|
||||||
|
|
||||||
|
async function load() {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const data = await request<StorePackagesResponse>('SHOP_H5', '/shop/store/packages');
|
||||||
|
const base = data.pendingRequest?.packages?.length
|
||||||
|
? data.pendingRequest.packages
|
||||||
|
: data.live?.length
|
||||||
|
? data.live
|
||||||
|
: [emptyPackage()];
|
||||||
|
setItems(base.map((p, i) => ({ ...p, price: String(p.price), sortOrder: i })));
|
||||||
|
setPending(data.pendingRequest ?? null);
|
||||||
|
setMsg('');
|
||||||
|
} catch (e) {
|
||||||
|
setMsg(e instanceof Error ? e.message : '加载失败');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setLoading(true);
|
void load();
|
||||||
request<StorePackagesResponse>('SHOP_H5', '/shop/store/packages')
|
|
||||||
.then((data) => {
|
|
||||||
const base = data.pendingRequest?.packages?.length
|
|
||||||
? data.pendingRequest.packages
|
|
||||||
: data.live?.length
|
|
||||||
? data.live
|
|
||||||
: [emptyPackage()];
|
|
||||||
setItems(base.map((p, i) => ({ ...p, price: String(p.price), sortOrder: i })));
|
|
||||||
setPending(data.pendingRequest ?? null);
|
|
||||||
})
|
|
||||||
.catch((e) => setError(e instanceof Error ? e.message : '加载失败'))
|
|
||||||
.finally(() => setLoading(false));
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
function updateAt(index: number, patch: Partial<PackageFormItem>) {
|
|
||||||
setItems((prev) => prev.map((item, i) => (i === index ? { ...item, ...patch } : item)));
|
|
||||||
}
|
|
||||||
|
|
||||||
function addItem() {
|
|
||||||
if (items.length >= STORE_PACKAGE_MAX_COUNT) return;
|
|
||||||
setItems((prev) => [...prev, emptyPackage(prev.length)]);
|
|
||||||
}
|
|
||||||
|
|
||||||
function removeAt(index: number) {
|
|
||||||
setItems((prev) => prev.filter((_, i) => i !== index).map((item, i) => ({ ...item, sortOrder: i })));
|
|
||||||
}
|
|
||||||
|
|
||||||
async function submitAudit() {
|
async function submitAudit() {
|
||||||
const msg = validatePackageFormItems(items);
|
const validationMsg = validatePackageFormItems(items);
|
||||||
if (msg) {
|
if (validationMsg) {
|
||||||
setError(msg);
|
setMsg(validationMsg);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (pending?.status === 'PENDING') {
|
if (pending?.status === 'PENDING') {
|
||||||
setError('已有套餐变更审核中,请等待总部处理');
|
setMsg('已有套餐变更审核中,请等待总部处理');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setSubmitting(true);
|
setSubmitting(true);
|
||||||
setError('');
|
setMsg('');
|
||||||
try {
|
try {
|
||||||
const packages = normalizePackageFormItems(items).map((p) => ({
|
const packages = normalizePackageFormItems(items).map((p) => ({
|
||||||
...p,
|
...p,
|
||||||
@@ -68,105 +63,79 @@ export default function PackagesPage() {
|
|||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
body: JSON.stringify({ packages }),
|
body: JSON.stringify({ packages }),
|
||||||
});
|
});
|
||||||
navigate('/mine');
|
setMsg('已提交审核,请等待总部处理');
|
||||||
|
await load();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setError(e instanceof Error ? e.message : '提交失败');
|
setMsg(e instanceof Error ? e.message : '提交失败');
|
||||||
} finally {
|
} finally {
|
||||||
setSubmitting(false);
|
setSubmitting(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const list = items.length ? items : [emptyPackage()];
|
|
||||||
const disabled = pending?.status === 'PENDING' || submitting;
|
const disabled = pending?.status === 'PENDING' || submitting;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="shop-page">
|
<PullToRefresh onRefresh={load} className="shop-records-page shop-packages-page">
|
||||||
<header className="shop-subheader">
|
<header className="shop-records-header" style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||||
<button type="button" className="shop-back" onClick={() => navigate('/mine')}>
|
<button
|
||||||
返回
|
type="button"
|
||||||
|
className="shop-withdraw-back"
|
||||||
|
onClick={() => navigate('/mine')}
|
||||||
|
aria-label="返回"
|
||||||
|
>
|
||||||
|
<span className="material-symbols-outlined" style={{ fontSize: 22 }}>
|
||||||
|
arrow_back
|
||||||
|
</span>
|
||||||
</button>
|
</button>
|
||||||
<h1>门店套餐</h1>
|
<h1 className="app-page-title" style={{ margin: 0 }}>
|
||||||
|
门店套餐
|
||||||
|
</h1>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
{loading ? <p className="shop-muted">加载中…</p> : null}
|
<div className="shop-records-main">
|
||||||
{error ? <p className="shop-error">{error}</p> : null}
|
{loading ? <p className="shop-records-empty">加载中…</p> : null}
|
||||||
{pending?.status === 'PENDING' ? (
|
|
||||||
<p className="shop-hint">套餐变更审核中,用户端仍展示上一版生效套餐。</p>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
{!loading
|
{!loading && pending?.status === 'PENDING' ? (
|
||||||
? list.map((item, index) => (
|
<section className="shop-packages-notice">
|
||||||
<div key={index} className="shop-card" style={{ marginBottom: 12 }}>
|
<span className="material-symbols-outlined shop-fill-icon" style={{ fontSize: 18 }}>
|
||||||
<div className="shop-card-head">
|
hourglass_top
|
||||||
<strong>套餐 {index + 1}</strong>
|
</span>
|
||||||
{!disabled && list.length > 1 ? (
|
<p>套餐变更审核中,用户端仍展示上一版生效套餐。</p>
|
||||||
<button type="button" onClick={() => removeAt(index)}>
|
</section>
|
||||||
删除
|
) : null}
|
||||||
</button>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
<label>
|
|
||||||
名称
|
|
||||||
<input
|
|
||||||
value={item.name}
|
|
||||||
disabled={disabled}
|
|
||||||
onChange={(e) => updateAt(index, { name: e.target.value })}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
<label>
|
|
||||||
价格(元)
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
min={0}
|
|
||||||
value={item.price}
|
|
||||||
disabled={disabled}
|
|
||||||
onChange={(e) => updateAt(index, { price: e.target.value })}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
<label>
|
|
||||||
菜品
|
|
||||||
<textarea
|
|
||||||
rows={2}
|
|
||||||
value={item.dishes}
|
|
||||||
disabled={disabled}
|
|
||||||
onChange={(e) => updateAt(index, { dishes: e.target.value })}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
<label>
|
|
||||||
使用时间
|
|
||||||
<input
|
|
||||||
value={item.usableTime || ''}
|
|
||||||
disabled={disabled}
|
|
||||||
onChange={(e) => updateAt(index, { usableTime: e.target.value })}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
<label>
|
|
||||||
其他说明
|
|
||||||
<input
|
|
||||||
value={item.otherNotes || ''}
|
|
||||||
disabled={disabled}
|
|
||||||
onChange={(e) => updateAt(index, { otherNotes: e.target.value })}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
))
|
|
||||||
: null}
|
|
||||||
|
|
||||||
{!disabled && list.length < STORE_PACKAGE_MAX_COUNT ? (
|
{!loading && pending?.status === 'REJECTED' && pending.rejectReason ? (
|
||||||
<button type="button" className="shop-btn-outline" onClick={addItem}>
|
<section className="shop-packages-notice shop-packages-notice--warn">
|
||||||
+ 添加套餐
|
<span className="material-symbols-outlined shop-fill-icon" style={{ fontSize: 18 }}>
|
||||||
</button>
|
error
|
||||||
) : null}
|
</span>
|
||||||
|
<p>上次驳回:{pending.rejectReason}</p>
|
||||||
|
</section>
|
||||||
|
) : null}
|
||||||
|
|
||||||
<button
|
{!loading ? (
|
||||||
type="button"
|
<>
|
||||||
className="shop-btn-primary"
|
<p className="shop-packages-intro">
|
||||||
style={{ marginTop: 16 }}
|
维护门店可核销套餐信息,提交后由总部审核通过后在用户端展示。
|
||||||
disabled={disabled}
|
</p>
|
||||||
onClick={() => void submitAudit()}
|
<ShopPackagesForm items={items} onChange={setItems} disabled={disabled} />
|
||||||
>
|
</>
|
||||||
{submitting ? '提交中…' : '提交审核'}
|
) : null}
|
||||||
</button>
|
|
||||||
</div>
|
{msg ? <p className="shop-withdraw-msg">{msg}</p> : null}
|
||||||
|
|
||||||
|
{!loading ? (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="shop-withdraw-btn"
|
||||||
|
style={{ marginTop: 16 }}
|
||||||
|
disabled={disabled}
|
||||||
|
onClick={() => void submitAudit()}
|
||||||
|
>
|
||||||
|
{submitting ? '提交中…' : '提交审核'}
|
||||||
|
</button>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</PullToRefresh>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1920,6 +1920,141 @@
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ─── 门店套餐 ─── */
|
||||||
|
.shop-packages-page .shop-records-main {
|
||||||
|
padding-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shop-packages-intro {
|
||||||
|
margin: 0 0 12px;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: var(--color-subtle-gray);
|
||||||
|
}
|
||||||
|
|
||||||
|
.shop-packages-notice {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
padding: 12px 14px;
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
background: rgba(46, 125, 50, 0.08);
|
||||||
|
border: 1px solid rgba(46, 125, 50, 0.15);
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: var(--color-on-surface);
|
||||||
|
}
|
||||||
|
|
||||||
|
.shop-packages-notice--warn {
|
||||||
|
background: rgba(180, 35, 24, 0.06);
|
||||||
|
border-color: rgba(180, 35, 24, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.shop-packages-notice p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shop-packages-form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shop-packages-card {
|
||||||
|
background: var(--color-card);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
padding: 16px;
|
||||||
|
box-shadow: var(--shadow-card);
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.shop-packages-card-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shop-packages-card-title {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--color-on-surface);
|
||||||
|
}
|
||||||
|
|
||||||
|
.shop-packages-remove {
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--color-heritage-red);
|
||||||
|
font-size: 13px;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shop-packages-field {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shop-packages-field:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shop-packages-label {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--color-subtle-gray);
|
||||||
|
}
|
||||||
|
|
||||||
|
.shop-packages-input,
|
||||||
|
.shop-packages-textarea {
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 12px;
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.08);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
background: var(--color-surface-container-low, #fafafa);
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--color-on-surface);
|
||||||
|
}
|
||||||
|
|
||||||
|
.shop-packages-textarea {
|
||||||
|
resize: vertical;
|
||||||
|
min-height: 72px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shop-packages-input:focus,
|
||||||
|
.shop-packages-textarea:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--color-heritage-red);
|
||||||
|
box-shadow: 0 0 0 1px rgba(166, 29, 36, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.shop-packages-input:disabled,
|
||||||
|
.shop-packages-textarea:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shop-packages-add {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 6px;
|
||||||
|
width: 100%;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shop-packages-add .material-symbols-outlined {
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
.shop-record-amounts {
|
.shop-record-amounts {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 1fr;
|
grid-template-columns: 1fr 1fr;
|
||||||
|
|||||||
Reference in New Issue
Block a user