diff --git a/apps/admin-web/src/components/AdminStorePackagesSection.tsx b/apps/admin-web/src/components/AdminStorePackagesSection.tsx
index 9a38790..0a01a41 100644
--- a/apps/admin-web/src/components/AdminStorePackagesSection.tsx
+++ b/apps/admin-web/src/components/AdminStorePackagesSection.tsx
@@ -1,5 +1,5 @@
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 { STORE_PACKAGE_MAX_COUNT } from '@dukang/shared-types';
import { request } from '../lib/api';
@@ -87,68 +87,101 @@ export default function AdminStorePackagesSection({ storeId }: { storeId: string
}
}
- if (loading) return 加载套餐中…;
+ if (loading) {
+ return 加载套餐中…;
+ }
return (
-
-
- 总部直存立即生效,无需审核。最多 {STORE_PACKAGE_MAX_COUNT} 条。
-
+
);
}
diff --git a/apps/h5-partner/src/components/StorePackagesForm.tsx b/apps/h5-partner/src/components/StorePackagesForm.tsx
index c48f0b3..53e01bc 100644
--- a/apps/h5-partner/src/components/StorePackagesForm.tsx
+++ b/apps/h5-partner/src/components/StorePackagesForm.tsx
@@ -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
) {
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 (
-
+
{list.map((item, index) => (
-
-
+
+
+
+
+ schedule
+ updateAt(index, { usableTime: e.target.value })}
+ />
+
+
+
+
+
+
+ info
+ updateAt(index, { otherNotes: e.target.value })}
+ />
+
+
+
))}
+
{!disabled && list.length < STORE_PACKAGE_MAX_COUNT ? (
-
diff --git a/apps/h5-partner/src/pages/StoreCreatePage.tsx b/apps/h5-partner/src/pages/StoreCreatePage.tsx
index 1a40210..14ebd80 100644
--- a/apps/h5-partner/src/pages/StoreCreatePage.tsx
+++ b/apps/h5-partner/src/pages/StoreCreatePage.tsx
@@ -102,6 +102,7 @@ export default function StoreCreatePage() {
const [categoryTree, setCategoryTree] = useState
([]);
const [mapPickerOpen, setMapPickerOpen] = useState(false);
+ const [stepErrorModal, setStepErrorModal] = useState(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 && (
<>
-
- 门店套餐(可选)
- 可不填;提交后将进入总部审核。也可跳过此步。
+
+
+
+ 可选填写;提交后将进入总部审核。也可在最后一页点击「跳过」。
+
setForm((prev) => ({ ...prev, packages }))}
disabled={submitting}
+ embedded
/>
>
@@ -1181,6 +1194,22 @@ export default function StoreCreatePage() {
+ {stepErrorModal ? (
+ setStepErrorModal(null)}>
+
e.stopPropagation()}>
+
请完善信息
+
+ {stepErrorModal}
+
+
+ setStepErrorModal(null)}>
+ 我知道了
+
+
+
+
+ ) : null}
+
setMapPickerOpen(false)}
diff --git a/apps/h5-partner/src/pages/StorePackagesPage.tsx b/apps/h5-partner/src/pages/StorePackagesPage.tsx
index 14dc4a0..d49c7d3 100644
--- a/apps/h5-partner/src/pages/StorePackagesPage.tsx
+++ b/apps/h5-partner/src/pages/StorePackagesPage.tsx
@@ -69,36 +69,66 @@ export default function StorePackagesPage() {
if (loading) {
return (
-
-
加载中…
+
);
}
return (
-
-
- navigate(`/stores/${id}`)}>
- 返回
+
+
+ navigate(`/stores/${id}`)}>
+ arrow_back
- 门店套餐
+ 门店套餐
- {error ?
{error}
: null}
- {pending?.status === 'PENDING' ? (
-
套餐变更审核中,C 端仍展示上一版生效套餐。
- ) : null}
- {pending?.status === 'REJECTED' && pending.rejectReason ? (
-
上次驳回:{pending.rejectReason}
- ) : null}
+
+ {error ? {error}
: null}
-
+ {pending?.status === 'PENDING' ? (
+
+
hourglass_top
+
套餐变更审核中,C 端仍展示上一版生效套餐。
+
+ ) : null}
-
+ {pending?.status === 'REJECTED' && pending.rejectReason ? (
+
+
error
+
上次驳回:{pending.rejectReason}
+
+ ) : null}
+
+
+
+
+ 提交后将由总部审核,通过后 C 端展示生效套餐。
+
+
+
+
+
+
+
);
}
diff --git a/apps/h5-partner/src/styles.css b/apps/h5-partner/src/styles.css
index 47a1f7b..ae097c1 100644
--- a/apps/h5-partner/src/styles.css
+++ b/apps/h5-partner/src/styles.css
@@ -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);
+}
diff --git a/apps/h5-shop/src/components/ShopPackagesForm.tsx b/apps/h5-shop/src/components/ShopPackagesForm.tsx
new file mode 100644
index 0000000..0efa6cb
--- /dev/null
+++ b/apps/h5-shop/src/components/ShopPackagesForm.tsx
@@ -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) {
+ 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 (
+
+ {list.map((item, index) => (
+
+ ))}
+
+ {!disabled && list.length < STORE_PACKAGE_MAX_COUNT ? (
+
+ add_circle
+ 添加套餐
+
+ ) : null}
+
+ );
+}
diff --git a/apps/h5-shop/src/pages/PackagesPage.tsx b/apps/h5-shop/src/pages/PackagesPage.tsx
index 7a43a20..41af591 100644
--- a/apps/h5-shop/src/pages/PackagesPage.tsx
+++ b/apps/h5-shop/src/pages/PackagesPage.tsx
@@ -1,7 +1,8 @@
import { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
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 {
emptyPackage,
@@ -16,49 +17,43 @@ export default function PackagesPage() {
const [pending, setPending] = useState(null);
const [loading, setLoading] = useState(true);
const [submitting, setSubmitting] = useState(false);
- const [error, setError] = useState('');
+ const [msg, setMsg] = useState('');
+
+ async function load() {
+ setLoading(true);
+ try {
+ const data = await request('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(() => {
- setLoading(true);
- request('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));
+ void load();
}, []);
- function updateAt(index: number, patch: Partial) {
- 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() {
- const msg = validatePackageFormItems(items);
- if (msg) {
- setError(msg);
+ const validationMsg = validatePackageFormItems(items);
+ if (validationMsg) {
+ setMsg(validationMsg);
return;
}
if (pending?.status === 'PENDING') {
- setError('已有套餐变更审核中,请等待总部处理');
+ setMsg('已有套餐变更审核中,请等待总部处理');
return;
}
setSubmitting(true);
- setError('');
+ setMsg('');
try {
const packages = normalizePackageFormItems(items).map((p) => ({
...p,
@@ -68,105 +63,79 @@ export default function PackagesPage() {
method: 'PUT',
body: JSON.stringify({ packages }),
});
- navigate('/mine');
+ setMsg('已提交审核,请等待总部处理');
+ await load();
} catch (e) {
- setError(e instanceof Error ? e.message : '提交失败');
+ setMsg(e instanceof Error ? e.message : '提交失败');
} finally {
setSubmitting(false);
}
}
- const list = items.length ? items : [emptyPackage()];
const disabled = pending?.status === 'PENDING' || submitting;
return (
-
-
- navigate('/mine')}>
- 返回
+
+
+ navigate('/mine')}
+ aria-label="返回"
+ >
+
+ arrow_back
+
- 门店套餐
+
+ 门店套餐
+
- {loading ? 加载中…
: null}
- {error ? {error}
: null}
- {pending?.status === 'PENDING' ? (
- 套餐变更审核中,用户端仍展示上一版生效套餐。
- ) : null}
+
+ {loading ?
加载中…
: null}
- {!loading
- ? list.map((item, index) => (
-
- ))
- : null}
+ {!loading && pending?.status === 'PENDING' ? (
+
+
+ hourglass_top
+
+ 套餐变更审核中,用户端仍展示上一版生效套餐。
+
+ ) : null}
- {!disabled && list.length < STORE_PACKAGE_MAX_COUNT ? (
-
- + 添加套餐
-
- ) : null}
+ {!loading && pending?.status === 'REJECTED' && pending.rejectReason ? (
+
+
+ error
+
+ 上次驳回:{pending.rejectReason}
+
+ ) : null}
-
void submitAudit()}
- >
- {submitting ? '提交中…' : '提交审核'}
-
-
+ {!loading ? (
+ <>
+
+ 维护门店可核销套餐信息,提交后由总部审核通过后在用户端展示。
+
+
+ >
+ ) : null}
+
+ {msg ? {msg}
: null}
+
+ {!loading ? (
+ void submitAudit()}
+ >
+ {submitting ? '提交中…' : '提交审核'}
+
+ ) : null}
+
+
);
}
diff --git a/apps/h5-shop/src/styles.css b/apps/h5-shop/src/styles.css
index ca8afff..deedb7c 100644
--- a/apps/h5-shop/src/styles.css
+++ b/apps/h5-shop/src/styles.css
@@ -1920,6 +1920,141 @@
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 {
display: grid;
grid-template-columns: 1fr 1fr;