diff --git a/.cursor/skills/dukang-coding/reference-frontend.md b/.cursor/skills/dukang-coding/reference-frontend.md index 9600873..73e1405 100644 --- a/.cursor/skills/dukang-coding/reference-frontend.md +++ b/.cursor/skills/dukang-coding/reference-frontend.md @@ -65,6 +65,9 @@ all | pending_pay | pending_ship | pending_receive | completed | /orders/:id | OrderDetailPage | /partner/orders/:id | | /center | CenterPage | /partner/me | | /center/bills | BillsPage | /partner/settlement/* | +| /reshipments | ReshipPage | /partner/after-sales/reshipments | +| /reports/weekly | WeeklyReportPage | /partner/reports/weekly | +| /leaderboard | LeaderboardPage | /partner/dashboard/leaderboard | ## admin-web 路由(总部) diff --git a/apps/h5-partner/src/lib/storeDraft.ts b/apps/h5-partner/src/lib/storeDraft.ts index cbed359..90bb5ed 100644 --- a/apps/h5-partner/src/lib/storeDraft.ts +++ b/apps/h5-partner/src/lib/storeDraft.ts @@ -9,6 +9,11 @@ export type StoreDraftForm = { bankBranch: string; }; +export type StoreDraft = { + step: number; + form: StoreDraftForm; +}; + export const STORE_DRAFT_KEY = 'partner_store_draft_v1'; export const defaultStoreForm = (): StoreDraftForm => ({ @@ -22,18 +27,39 @@ export const defaultStoreForm = (): StoreDraftForm => ({ bankBranch: '', }); -export function loadStoreDraft(): StoreDraftForm | null { +function normalizeForm(raw: Record): StoreDraftForm { + const base = defaultStoreForm(); + return { + name: String(raw.name ?? base.name), + phone: String(raw.phone ?? base.phone), + district: String(raw.district ?? base.district), + address: String(raw.address ?? base.address), + intro: String(raw.intro ?? base.intro), + bankAccountName: String(raw.bankAccountName ?? base.bankAccountName), + bankAccountNo: String(raw.bankAccountNo ?? base.bankAccountNo), + bankBranch: String(raw.bankBranch ?? base.bankBranch), + }; +} + +export function loadStoreDraft(): StoreDraft | null { try { const raw = localStorage.getItem(STORE_DRAFT_KEY); if (!raw) return null; - return { ...defaultStoreForm(), ...JSON.parse(raw) }; + const parsed = JSON.parse(raw) as Record; + if (parsed.form && typeof parsed.form === 'object') { + return { + step: Math.min(3, Math.max(1, Number(parsed.step) || 1)), + form: normalizeForm(parsed.form as Record), + }; + } + return { step: 1, form: normalizeForm(parsed) }; } catch { return null; } } -export function saveStoreDraft(form: StoreDraftForm) { - localStorage.setItem(STORE_DRAFT_KEY, JSON.stringify(form)); +export function saveStoreDraft(draft: StoreDraft) { + localStorage.setItem(STORE_DRAFT_KEY, JSON.stringify(draft)); } export function clearStoreDraft() { diff --git a/apps/h5-partner/src/pages/CenterPage.tsx b/apps/h5-partner/src/pages/CenterPage.tsx index 06d43c7..a9b726f 100644 --- a/apps/h5-partner/src/pages/CenterPage.tsx +++ b/apps/h5-partner/src/pages/CenterPage.tsx @@ -103,13 +103,34 @@ export default function CenterPage() {
description
- 待确认账单 + 财务对账
{pendingBills.length > 0 && {pendingBills.length}} chevron_right
+ +
+
monitoring
+ 数据周报 +
+ chevron_right + + +
+
assignment_return
+ 补发处理 +
+ chevron_right + + +
+
military_tech
+ 合伙人贡献榜 +
+ chevron_right + diff --git a/apps/h5-partner/src/pages/HomePage.tsx b/apps/h5-partner/src/pages/HomePage.tsx index 86c0ae1..00ea2ed 100644 --- a/apps/h5-partner/src/pages/HomePage.tsx +++ b/apps/h5-partner/src/pages/HomePage.tsx @@ -12,6 +12,16 @@ function auditStatus(store: Record) { return String(audits?.[0]?.status ?? '').toUpperCase(); } +function storeStatusLabel(store: Record) { + const audit = auditStatus(store); + if (audit === 'PENDING') return '待审核'; + const s = String(store.status).toUpperCase(); + if (s === 'OPEN') return '营业中'; + if (s === 'PAUSED') return '暂时闭店'; + if (s === 'CLOSED') return '已关闭'; + return s; +} + export default function HomePage() { const navigate = useNavigate(); const [dash, setDash] = useState | null>(null); @@ -42,6 +52,8 @@ export default function HomePage() { return created.getMonth() === now.getMonth() && created.getFullYear() === now.getFullYear(); }).length; + const recentFromDash = (dash?.recentStores as Array> | undefined) ?? []; + const displayStores = stores.length > 0 ? stores : recentFromDash; const previewLeaderboard = LEADERBOARD_MOCK.slice(0, 2); return ( @@ -167,14 +179,29 @@ export default function HomePage() { - {stores.length > 0 && ( + {pendingStores.length > 0 && ( +
+

待审核门店名称

+
    + {pendingStores.map((s) => ( +
  • + {String(s.name || '未命名门店')} +
  • + ))} +
+
+ )} + + {displayStores.length > 0 && (
    - {stores.slice(0, 5).map((s) => ( +
  • + 门店名称 + 状态 +
  • + {displayStores.slice(0, 5).map((s) => (
  • - {String(s.name)} - - {auditStatus(s) === 'PENDING' ? '待审核' : String(s.status) === 'OPEN' ? '营业中' : String(s.status)} - + {String(s.name || '未命名门店')} + {storeStatusLabel(s)}
  • ))}
diff --git a/apps/h5-partner/src/pages/ReshipPage.tsx b/apps/h5-partner/src/pages/ReshipPage.tsx index d8dcd68..d9c86af 100644 --- a/apps/h5-partner/src/pages/ReshipPage.tsx +++ b/apps/h5-partner/src/pages/ReshipPage.tsx @@ -1,35 +1,74 @@ +import { useState } from 'react'; import { useNavigate } from 'react-router-dom'; import PageHeader from '@dukang/shared-ui/PageHeader'; -/** BUWR-23:补发处理(preV1 占位,待 `/partner/after-sales/reshipments` 接入) */ +type ReshipStatus = 'ALL' | 'PENDING' | 'SHIPPING' | 'DONE'; + const MOCK_ITEMS = [ - { id: '1', orderNo: 'DK20260301001', status: '待配送', productName: '杜康窖藏', originalOrderNo: 'DK20260228088' }, + { id: '1', orderNo: 'DK20260301001', status: 'PENDING' as const, productName: '杜康窖藏', originalOrderNo: 'DK20260228088', address: '郑州市金水区花园路 88 号' }, + { id: '2', orderNo: 'DK20260302002', status: 'SHIPPING' as const, productName: '杜康特曲', originalOrderNo: 'DK20260215012', address: '郑州市二七区大学路 12 号' }, ]; +const STATUS_LABEL: Record = { + PENDING: '待配送', + SHIPPING: '配送中', + DONE: '已完成', +}; + +const FILTERS: { key: ReshipStatus; label: string }[] = [ + { key: 'ALL', label: '全部' }, + { key: 'PENDING', label: '待配送' }, + { key: 'SHIPPING', label: '配送中' }, + { key: 'DONE', label: '已完成' }, +]; + +/** BUWR-23:补发处理 */ export default function ReshipPage() { const navigate = useNavigate(); + const [filter, setFilter] = useState('ALL'); + + const list = MOCK_ITEMS.filter((item) => filter === 'ALL' || item.status === filter); return (
navigate('/')} /> -

+

总部下达的补发工单,确认后可推进配送

- {MOCK_ITEMS.length === 0 &&
暂无补发工单
} +
+ {FILTERS.map((f) => ( + + ))} +
- {MOCK_ITEMS.map((item) => ( + {list.length === 0 &&
暂无补发工单
} + + {list.map((item) => (
原单 {item.originalOrderNo} - {item.status} + {STATUS_LABEL[item.status]}

{item.productName}

补发单号 {item.orderNo}

- +
+ location_on +

{item.address}

+
+ {item.status === 'PENDING' && ( + + )}
))}
diff --git a/apps/h5-partner/src/pages/StoreCreatePage.tsx b/apps/h5-partner/src/pages/StoreCreatePage.tsx index 9f04a8c..9aeb050 100644 --- a/apps/h5-partner/src/pages/StoreCreatePage.tsx +++ b/apps/h5-partner/src/pages/StoreCreatePage.tsx @@ -17,13 +17,22 @@ const STEPS = ['基本信息', '照片上传', '结算资质'] as const; export default function StoreCreatePage() { const navigate = useNavigate(); const [params, setParams] = useSearchParams(); - const step = Number(params.get('step') || 1); - const [form, setForm] = useState(() => loadStoreDraft() ?? defaultStoreForm()); + const saved = loadStoreDraft(); + const [form, setForm] = useState(saved?.form ?? defaultStoreForm()); const [error, setError] = useState(''); + const stepFromUrl = Number(params.get('step') || 0); + const step = stepFromUrl >= 1 && stepFromUrl <= 3 ? stepFromUrl : (saved?.step ?? 1); + useEffect(() => { - saveStoreDraft(form); - }, [form]); + if (!stepFromUrl && saved?.step) { + setParams({ step: String(saved.step) }, { replace: true }); + } + }, [stepFromUrl, saved?.step, setParams]); + + useEffect(() => { + saveStoreDraft({ step, form }); + }, [step, form]); function patchForm(patch: Partial) { setForm((prev) => ({ ...prev, ...patch })); @@ -144,7 +153,7 @@ export default function StoreCreatePage() {

杜康合伙人身份认证

- 传承千年窖香,共筑美酒传奇。录入信息后,系统将在 1-3 个工作日内完成审核。 + 填写内容将自动保存,退出后可继续录入。

@@ -173,18 +182,12 @@ export default function StoreCreatePage() {

环境照片 *

至少3张,展示店内整洁环境

- - - + {[0, 1, 2].map((i) => ( + + ))}
@@ -205,7 +208,7 @@ export default function StoreCreatePage() {
info

- 温馨提示:请确保照片清晰无反光。preV1 占位上传,可直接下一步。 + 温馨提示:preV1 占位上传,可直接下一步。

@@ -220,21 +223,21 @@ export default function StoreCreatePage() {
- patchForm({ bankAccountName: e.target.value })} /> + patchForm({ bankAccountName: e.target.value })} />
- patchForm({ bankAccountNo: e.target.value })} /> + patchForm({ bankAccountNo: e.target.value })} />
- patchForm({ bankBranch: e.target.value })} /> + patchForm({ bankBranch: e.target.value })} />
info

- 请确保银行卡信息准确,以免影响每月的餐费结算。结算将按合同约定账期执行。 + 请确保银行卡信息准确,以免影响每月的餐费结算。

diff --git a/apps/h5-partner/src/pages/StoreListPage.tsx b/apps/h5-partner/src/pages/StoreListPage.tsx index 7429ae0..9e34a71 100644 --- a/apps/h5-partner/src/pages/StoreListPage.tsx +++ b/apps/h5-partner/src/pages/StoreListPage.tsx @@ -18,6 +18,15 @@ const FILTERS: { key: StatusFilter; label: string }[] = [ { key: 'CLOSED', label: '已关闭' }, ]; +function auditLabel(store: Record) { + const audits = store.audits as Array<{ status?: string }> | undefined; + const s = String(audits?.[0]?.status ?? '').toUpperCase(); + if (s === 'PENDING') return { cls: 'partner-status-pill--paused', label: '待审核' }; + if (s === 'REJECTED') return { cls: 'partner-status-pill--closed', label: '已驳回' }; + if (s === 'APPROVED') return { cls: 'partner-status-pill--open', label: '已通过' }; + return null; +} + export default function StoreListPage() { const navigate = useNavigate(); const [stores, setStores] = useState>>([]); @@ -71,16 +80,22 @@ export default function StoreListPage() { {filtered.map((s) => { const pill = statusPill(String(s.status)); + const audit = auditLabel(s); const dim = String(s.status).toUpperCase() === 'CLOSED'; + const storeName = String(s.name || '未命名门店'); return (
-

{String(s.name)}

+

门店名称

+

{storeName}

{String(s.address || s.district || '')}

- {pill.label} +
+ {pill.label} + {audit && {audit.label}} +
diff --git a/apps/h5-partner/src/styles.css b/apps/h5-partner/src/styles.css index 5bbf494..95de32b 100644 --- a/apps/h5-partner/src/styles.css +++ b/apps/h5-partner/src/styles.css @@ -606,6 +606,30 @@ html, body, #root { gap: 12px; } +.partner-store-name-list-header { + padding-bottom: 4px !important; + border-bottom: 1px solid rgba(226, 190, 188, 0.15); +} + +.partner-store-name-list--compact li { + padding: 4px 0; +} + +.partner-pending-stores { + margin-bottom: 12px; + padding-bottom: 12px; + border-bottom: 1px solid rgba(226, 190, 188, 0.15); +} + +.partner-field-input--block { + width: 100%; + padding: 0 16px; + min-height: 48px; + border: 1px solid rgba(226, 190, 188, 0.3); + border-radius: 8px; + background: var(--color-background); +} + .partner-leaderboard-section { padding-top: 12px; } diff --git a/server/dukang-api/src/modules/store/store.service.ts b/server/dukang-api/src/modules/store/store.service.ts index 7636711..d0a6876 100644 --- a/server/dukang-api/src/modules/store/store.service.ts +++ b/server/dukang-api/src/modules/store/store.service.ts @@ -115,13 +115,24 @@ export class StoreService { async partnerDashboard(partnerAccountId: bigint) { const account = await this.getPartnerAccount(partnerAccountId); - const [storeCount, orderCount] = await Promise.all([ + const [storeCount, orderCount, recentStores] = await Promise.all([ this.prisma.store.count({ where: { partnerId: account.partnerId } }), this.prisma.order.count({ where: { city: { partnerId: account.partnerId } }, }), + this.prisma.store.findMany({ + where: { partnerId: account.partnerId }, + select: { id: true, name: true, status: true, createdAt: true }, + orderBy: { createdAt: 'desc' }, + take: 10, + }), ]); - return { storeCount, orderCount, companyName: account.partner.companyName }; + return { + storeCount, + orderCount, + companyName: account.partner.companyName, + recentStores: serializeBigInt(recentStores), + }; } private async getPartnerAccount(partnerAccountId: bigint) {