From e7d39f739070e977f6fa56c5507e0f013407381d Mon Sep 17 00:00:00 2001 From: jacy <18049821889@163.com> Date: Wed, 15 Jul 2026 18:01:19 +0800 Subject: [PATCH] =?UTF-8?q?=E9=97=A8=E5=BA=97=E7=AB=AF=E5=AD=90=E8=B4=A6?= =?UTF-8?q?=E5=8F=B7=E7=AE=A1=E7=90=86=E7=95=8C=E9=9D=A2=E8=B0=83=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/h5-shop/src/pages/SelectStorePage.tsx | 128 ++++-- apps/h5-shop/src/pages/StaffPage.tsx | 302 ++++++++++--- apps/h5-shop/src/styles.css | 495 ++++++++++++++++++--- 3 files changed, 768 insertions(+), 157 deletions(-) diff --git a/apps/h5-shop/src/pages/SelectStorePage.tsx b/apps/h5-shop/src/pages/SelectStorePage.tsx index 67e945c..5326801 100644 --- a/apps/h5-shop/src/pages/SelectStorePage.tsx +++ b/apps/h5-shop/src/pages/SelectStorePage.tsx @@ -13,9 +13,12 @@ export default function SelectStorePage() { const navigate = useNavigate(); const { applySession, store, authenticated } = useStoreSession(); const [stores, setStores] = useState(store?.stores ?? []); - const [loading, setLoading] = useState(false); + const [loadingId, setLoadingId] = useState(null); const [msg, setMsg] = useState(''); + const currentStoreId = store?.storeId || ''; + const canGoBack = Boolean(currentStoreId); + useEffect(() => { if (!authenticated) { navigate('/login', { replace: true }); @@ -27,7 +30,12 @@ export default function SelectStorePage() { }, [authenticated, navigate]); async function onSelect(storeId: string) { - setLoading(true); + if (loadingId) return; + if (storeId === currentStoreId) { + navigate('/', { replace: true }); + return; + } + setLoadingId(storeId); setMsg(''); try { const session = await selectStore(storeId); @@ -36,7 +44,7 @@ export default function SelectStorePage() { } catch (e) { setMsg(e instanceof Error ? e.message : '选店失败'); } finally { - setLoading(false); + setLoadingId(null); } } @@ -48,31 +56,101 @@ export default function SelectStorePage() { // eslint-disable-next-line react-hooks/exhaustive-deps }, [stores.length]); + function statusLabel(status: string) { + if (status === 'OPEN') return '营业中'; + if (status === 'PAUSED') return '暂停营业'; + return status || '门店'; + } + return (
-
-

选择门店

-

该账号绑定了多家门店,请选择本次要进入的门店

+
+ {canGoBack ? ( + + ) : ( + + )} +

切换门店

+
- {msg ?

{msg}

: null} -
    - {stores.map((item) => ( -
  • - -
  • - ))} -
- {!stores.length && !msg ?

暂无绑定门店

: null} + +
+
+
+ store +
+
+

选择要进入的门店

+

+ 该账号绑定了 {stores.length || '多'} 家门店,进入后可直接核销与查看营业数据 +

+
+
+ + {msg ? ( +

+ {msg} +

+ ) : null} + +
+

我的门店

+
    + {stores.map((item) => { + const active = item.storeId === currentStoreId; + const busy = loadingId === item.storeId; + return ( +
  • + +
  • + ); + })} +
+ {!stores.length && !msg ? ( +
+ store +

暂无绑定门店

+
+ ) : null} +
+
); } diff --git a/apps/h5-shop/src/pages/StaffPage.tsx b/apps/h5-shop/src/pages/StaffPage.tsx index e6ea8ab..50c755e 100644 --- a/apps/h5-shop/src/pages/StaffPage.tsx +++ b/apps/h5-shop/src/pages/StaffPage.tsx @@ -16,6 +16,11 @@ type StaffItem = { type StoreOption = { storeId: string; name: string }; +function maskPhone(phone: string) { + if (!phone || phone.length < 7) return phone; + return phone.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2'); +} + export default function StaffPage() { const navigate = useNavigate(); const { store } = useStoreSession(); @@ -24,6 +29,7 @@ export default function StaffPage() { const [ownedStores, setOwnedStores] = useState([]); const [msg, setMsg] = useState(''); const [showForm, setShowForm] = useState(false); + const [submitting, setSubmitting] = useState(false); const [form, setForm] = useState({ phone: '', name: '', storeIds: [] as string[] }); async function reload() { @@ -44,13 +50,24 @@ export default function StaffPage() { }, [navigate, profile?.isPrimary]); async function createStaff() { + const phone = form.phone.trim(); + const name = form.name.trim(); + if (!/^1[3-9]\d{9}$/.test(phone)) { + setMsg('请输入正确的手机号'); + return; + } + if (!name) { + setMsg('请填写姓名'); + return; + } setMsg(''); + setSubmitting(true); try { await request('SHOP_H5', '/shop/staff', { method: 'POST', body: JSON.stringify({ - phone: form.phone.trim(), - name: form.name.trim(), + phone, + name, storeIds: form.storeIds.length ? form.storeIds : ownedStores.map((s) => s.storeId), }), }); @@ -59,6 +76,8 @@ export default function StaffPage() { await reload(); } catch (e) { setMsg(e instanceof Error ? e.message : '创建失败'); + } finally { + setSubmitting(false); } } @@ -76,78 +95,233 @@ export default function StaffPage() { } function toggleStoreId(storeId: string) { - setForm((prev) => ({ - ...prev, - storeIds: prev.storeIds.includes(storeId) + setForm((prev) => { + const allIds = ownedStores.map((s) => s.storeId); + // 空数组表示「全部」;取消某一家时转为「除该店外的全部」 + if (prev.storeIds.length === 0) { + return { ...prev, storeIds: allIds.filter((id) => id !== storeId) }; + } + const next = prev.storeIds.includes(storeId) ? prev.storeIds.filter((id) => id !== storeId) - : [...prev.storeIds, storeId], - })); + : [...prev.storeIds, storeId]; + // 又选回全部时,恢复默认空数组语义 + if (next.length === allIds.length) { + return { ...prev, storeIds: [] }; + } + return { ...prev, storeIds: next }; + }); } + const selectedCount = + form.storeIds.length === 0 ? ownedStores.length : form.storeIds.length; + return (
-
- -

子账号管理

- +

{showForm ? '添加子账号' : '子账号管理'}

+ {!showForm ? ( + + ) : ( + + )}
- {msg ?

{msg}

: null} - - {showForm ? ( -
- setForm((f) => ({ ...f, phone: e.target.value }))} - /> - setForm((f) => ({ ...f, name: e.target.value }))} - /> -

绑定门店

-
- {ownedStores.map((s) => ( - - ))} -
- -
- ) : null} - -
    - {list.map((item) => ( -
  • -
    - {item.name} - {item.phone} - - {STORE_STAFF_ROLE_LABELS[item.staffRole] ?? item.staffRole} ·{' '} - {item.status === 'ACTIVE' ? '启用' : '停用'} - - {item.stores.map((s) => s.name).join('、')} +
    + {!showForm ? ( +
    +
    + group
    -
    + ) : null} + + {msg ? ( +

    + {msg} +

    + ) : null} + + {showForm ? ( +
    +
    + +
    + smartphone + + setForm((f) => ({ ...f, phone: e.target.value.replace(/\D/g, '').slice(0, 11) })) + } + /> +
    +
    + +
    + +
    + badge + setForm((f) => ({ ...f, name: e.target.value }))} + /> +
    +
    + +
    +

    + 绑定门店 + + {form.storeIds.length === 0 + ? `(默认全部 ${ownedStores.length} 家)` + : `(已选 ${selectedCount} 家)`} + +

    +
    + {ownedStores.map((s) => { + const checked = + form.storeIds.length === 0 || form.storeIds.includes(s.storeId); + return ( + + ); + })} +
    +
    + + -
  • - ))} -
- {!list.length ?

暂无子账号

: null} + + + ) : ( +
+
+

账号列表

+
+
    + {list.map((item) => { + const active = item.status === 'ACTIVE'; + const initial = (item.name || item.phone || '?').slice(0, 1); + return ( +
  • +
    {initial}
    +
    +
    + {item.name || '未命名'} + + {active ? '启用' : '停用'} + +
    +

    {maskPhone(item.phone)}

    +

    + {STORE_STAFF_ROLE_LABELS[item.staffRole] ?? item.staffRole} + {item.stores.length + ? ` · ${item.stores.map((s) => s.name).join('、')}` + : ''} +

    +
    + +
  • + ); + })} +
+ {!list.length ? ( +
+ person_off +

暂无子账号

+ +
+ ) : null} +
+ )} +
); } diff --git a/apps/h5-shop/src/styles.css b/apps/h5-shop/src/styles.css index 940dcc5..2fedeb8 100644 --- a/apps/h5-shop/src/styles.css +++ b/apps/h5-shop/src/styles.css @@ -2187,24 +2187,152 @@ .shop-select-store-page, .shop-staff-page { min-height: 100vh; - padding: 24px 16px 40px; background: var(--color-background); + padding-bottom: 40px; } -.shop-select-store-header h1, -.shop-staff-header h1 { - margin: 0; +.shop-subpage-header { + position: sticky; + top: 0; + z-index: 50; + height: 56px; + display: flex; + align-items: center; + justify-content: center; + padding: 0 var(--space-page); + background: var(--color-surface); + box-shadow: 0 1px 0 rgba(0, 0, 0, 0.04); +} + +.shop-subpage-back, +.shop-subpage-header-action { + position: absolute; + top: 50%; + transform: translateY(-50%); + border: none; + background: none; + padding: 4px; + color: var(--color-heritage-red); + cursor: pointer; + display: inline-flex; + align-items: center; + gap: 2px; + font-family: var(--font-label); + font-size: 13px; + font-weight: 600; +} + +.shop-subpage-back { + left: var(--space-page); +} + +.shop-subpage-header-action { + right: var(--space-page); +} + +.shop-subpage-header-action .material-symbols-outlined { + font-size: 20px; +} + +.shop-subpage-header-spacer { + position: absolute; + width: 40px; + height: 40px; + left: var(--space-page); + pointer-events: none; +} + +.shop-subpage-content { + padding: 16px var(--space-page) 24px; + display: flex; + flex-direction: column; + gap: 20px; +} + +.shop-subpage-hero { + display: flex; + align-items: flex-start; + gap: 14px; + background: var(--color-heritage-red); + border-radius: var(--radius-md); + padding: 20px; + color: var(--color-on-primary); + box-shadow: 0 8px 24px rgba(166, 29, 36, 0.25); +} + +.shop-subpage-hero--compact { + padding: 16px 18px; +} + +.shop-subpage-hero-icon { + width: 44px; + height: 44px; + border-radius: 50%; + background: rgba(255, 255, 255, 0.15); + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; +} + +.shop-subpage-hero-icon .material-symbols-outlined { + font-size: 24px; +} + +.shop-subpage-hero-title { + margin: 0 0 6px; font-family: var(--font-headline); - font-size: 22px; + font-size: 17px; + font-weight: 700; } -.shop-select-store-header p { - margin: 8px 0 20px; - color: var(--color-muted); +.shop-subpage-hero-desc { + margin: 0; + font-size: 13px; + line-height: 1.5; + color: rgba(255, 255, 255, 0.82); +} + +.shop-subpage-section-title { + font-family: var(--font-label); + font-size: 12px; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--color-subtle-gray); + margin: 0 0 12px 4px; +} + +.shop-subpage-msg { + margin: 0; + padding: 10px 14px; + border-radius: 10px; + background: rgba(166, 29, 36, 0.08); + color: var(--color-heritage-red); + font-size: 13px; +} + +.shop-subpage-empty { + display: flex; + flex-direction: column; + align-items: center; + gap: 8px; + padding: 40px 16px; + color: var(--color-subtle-gray); + text-align: center; +} + +.shop-subpage-empty .material-symbols-outlined { + font-size: 40px; + opacity: 0.45; +} + +.shop-subpage-empty p { + margin: 0; font-size: 14px; } -.shop-select-store-list { +.shop-select-store-list, +.shop-staff-list { list-style: none; margin: 0; padding: 0; @@ -2215,31 +2343,97 @@ .shop-select-store-item { width: 100%; + display: flex; + align-items: center; + gap: 12px; text-align: left; padding: 16px; - border: 1px solid rgba(0, 0, 0, 0.08); - border-radius: 12px; - background: #fff; + border: 1px solid rgba(226, 190, 188, 0.3); + border-radius: var(--radius-md); + background: var(--color-card); + box-shadow: 0 4px 20px rgba(166, 29, 36, 0.03); cursor: pointer; + transition: border-color 0.15s, box-shadow 0.15s; +} + +.shop-select-store-item:disabled { + opacity: 0.65; + cursor: not-allowed; +} + +.shop-select-store-item.is-active { + border-color: rgba(166, 29, 36, 0.35); + box-shadow: 0 6px 20px rgba(166, 29, 36, 0.1); +} + +.shop-select-store-item-icon { + width: 44px; + height: 44px; + border-radius: 12px; + background: rgba(166, 29, 36, 0.08); + color: var(--color-heritage-red); + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; +} + +.shop-select-store-item-body { + flex: 1; + min-width: 0; +} + +.shop-select-store-item-top { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 4px; } .shop-select-store-name { - display: block; + font-family: var(--font-headline); font-size: 16px; + font-weight: 700; + color: var(--color-on-surface); +} + +.shop-select-store-badge { + font-size: 11px; + font-weight: 700; + padding: 2px 8px; + border-radius: 999px; + background: var(--color-heritage-red); + color: #fff; +} + +.shop-select-store-status { + font-size: 11px; font-weight: 600; + padding: 2px 8px; + border-radius: 999px; + background: rgba(45, 106, 79, 0.1); + color: var(--color-success-green); } .shop-select-store-meta { display: block; - margin-top: 4px; font-size: 13px; - color: var(--color-muted); + color: var(--color-subtle-gray); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } -.shop-select-store-msg, -.shop-staff-msg { +.shop-select-store-item-action { color: var(--color-heritage-red); - font-size: 14px; + font-size: 12px; + font-weight: 600; + flex-shrink: 0; +} + +.shop-select-store-item-action .material-symbols-outlined { + font-size: 22px; + color: var(--color-on-surface-variant); } .shop-mine-actions { @@ -2254,92 +2448,257 @@ align-items: center; gap: 8px; width: 100%; - height: 44px; - border: 1px solid rgba(0, 0, 0, 0.08); - border-radius: 10px; - background: #fff; + height: 48px; + border: 1px solid rgba(226, 190, 188, 0.35); + border-radius: var(--radius-md); + background: var(--color-card); + box-shadow: 0 2px 10px rgba(166, 29, 36, 0.04); font-size: 15px; + font-weight: 600; + color: var(--color-on-surface); cursor: pointer; padding: 0 14px; } -.shop-staff-header { - display: flex; - align-items: center; - justify-content: space-between; - margin-bottom: 16px; -} - -.shop-staff-back, -.shop-staff-add { - border: none; - background: transparent; +.shop-mine-action .material-symbols-outlined { color: var(--color-heritage-red); - font-size: 14px; - cursor: pointer; } -.shop-staff-form { +.shop-staff-form-card { + background: var(--color-card); + border-radius: var(--radius-md); + padding: 20px; + border: 1px solid rgba(226, 190, 188, 0.3); + box-shadow: 0 4px 20px rgba(166, 29, 36, 0.05); display: flex; flex-direction: column; - gap: 10px; - margin-bottom: 16px; - padding: 14px; - border-radius: 12px; - background: #fff; + gap: 16px; } -.shop-staff-form input { - height: 40px; - padding: 0 12px; - border: 1px solid rgba(0, 0, 0, 0.12); - border-radius: 8px; +.shop-staff-form-field { + display: flex; + flex-direction: column; + gap: 8px; +} + +.shop-staff-form-label { + font-family: var(--font-label); + font-size: 12px; + font-weight: 600; + letter-spacing: 0.04em; + color: var(--color-on-surface-variant); +} + +.shop-staff-form-hint { + margin-left: 6px; + font-weight: 500; + color: var(--color-subtle-gray); +} + +.shop-staff-form-input-wrap { + display: flex; + align-items: center; + gap: 8px; + height: 48px; + padding: 0 14px; + border: 1px solid var(--color-outline-variant); + border-radius: 12px; + background: var(--color-surface); +} + +.shop-staff-form-input-wrap:focus-within { + border-color: var(--color-heritage-red); +} + +.shop-staff-form-input-wrap .material-symbols-outlined { + font-size: 20px; + color: var(--color-on-surface-variant); +} + +.shop-staff-form-input-wrap input { + flex: 1; + border: none; + outline: none; + background: transparent; + font-size: 16px; + color: var(--color-on-surface); } .shop-staff-store-picks { display: flex; flex-direction: column; gap: 8px; - font-size: 14px; } -.shop-staff-list { - list-style: none; - margin: 0; - padding: 0; +.shop-staff-store-pick { display: flex; - flex-direction: column; + align-items: center; gap: 10px; + padding: 12px 14px; + border-radius: 12px; + border: 1px solid rgba(0, 0, 0, 0.08); + background: var(--color-surface); + cursor: pointer; +} + +.shop-staff-store-pick input { + position: absolute; + opacity: 0; + pointer-events: none; +} + +.shop-staff-store-pick .material-symbols-outlined:first-of-type { + font-size: 20px; + color: var(--color-on-surface-variant); +} + +.shop-staff-store-pick.is-checked { + border-color: rgba(166, 29, 36, 0.35); + background: rgba(166, 29, 36, 0.04); +} + +.shop-staff-store-pick.is-checked .material-symbols-outlined:first-of-type { + color: var(--color-heritage-red); +} + +.shop-staff-store-pick-name { + flex: 1; + font-size: 14px; + font-weight: 600; + color: var(--color-on-surface); +} + +.shop-staff-store-pick-check { + font-size: 20px !important; + color: var(--color-heritage-red) !important; +} + +.shop-staff-submit { + width: 100%; + height: 48px; + border: none; + border-radius: var(--radius-md); + background: var(--color-heritage-red); + color: var(--color-on-primary); + font-family: var(--font-headline); + font-size: 16px; + font-weight: 700; + cursor: pointer; +} + +.shop-staff-submit:disabled { + opacity: 0.6; + cursor: not-allowed; +} + +.shop-staff-cancel { + width: 100%; + height: 44px; + border: none; + border-radius: var(--radius-md); + background: transparent; + color: var(--color-on-surface-variant); + font-size: 14px; + cursor: pointer; } .shop-staff-item { display: flex; - justify-content: space-between; + align-items: center; gap: 12px; - padding: 14px; - border-radius: 12px; - background: #fff; + padding: 16px; + border-radius: var(--radius-md); + background: var(--color-card); + border: 1px solid rgba(226, 190, 188, 0.25); + box-shadow: 0 4px 20px rgba(166, 29, 36, 0.03); } -.shop-staff-item div { +.shop-staff-avatar { + width: 44px; + height: 44px; + border-radius: 50%; + background: rgba(166, 29, 36, 0.1); + color: var(--color-heritage-red); + font-family: var(--font-headline); + font-size: 18px; + font-weight: 700; display: flex; - flex-direction: column; - gap: 4px; - font-size: 13px; - color: var(--color-muted); + align-items: center; + justify-content: center; + flex-shrink: 0; } -.shop-staff-item strong { - color: #222; +.shop-staff-item-body { + flex: 1; + min-width: 0; +} + +.shop-staff-item-top { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 4px; +} + +.shop-staff-item-top strong { font-size: 15px; + font-weight: 700; + color: var(--color-on-surface); } -.shop-staff-item button { - align-self: center; - border: 1px solid rgba(0, 0, 0, 0.12); - border-radius: 8px; +.shop-staff-status-badge { + font-size: 11px; + font-weight: 700; + padding: 2px 8px; + border-radius: 999px; +} + +.shop-staff-status-badge.is-active { + background: rgba(45, 106, 79, 0.1); + color: var(--color-success-green); +} + +.shop-staff-status-badge.is-disabled { + background: rgba(153, 153, 153, 0.12); + color: var(--color-subtle-gray); +} + +.shop-staff-item-meta { + margin: 0; + font-size: 12px; + color: var(--color-subtle-gray); + line-height: 1.45; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.shop-staff-toggle { + flex-shrink: 0; + height: 32px; + padding: 0 12px; + border-radius: 999px; + border: 1px solid rgba(166, 29, 36, 0.35); background: #fff; - padding: 6px 10px; + color: var(--color-heritage-red); + font-size: 12px; + font-weight: 700; + cursor: pointer; +} + +.shop-staff-toggle.is-enable { + border: none; + background: var(--color-heritage-red); + color: #fff; +} + +.shop-staff-empty-add { + margin-top: 8px; + border: none; + background: none; + color: var(--color-heritage-red); + font-size: 14px; + font-weight: 700; cursor: pointer; }