diff --git a/apps/h5-partner/src/App.tsx b/apps/h5-partner/src/App.tsx index 37ef4df..a549bad 100644 --- a/apps/h5-partner/src/App.tsx +++ b/apps/h5-partner/src/App.tsx @@ -9,6 +9,9 @@ import OrderListPage from './pages/OrderListPage'; import OrderDetailPage from './pages/OrderDetailPage'; import CenterPage from './pages/CenterPage'; import BillsPage from './pages/BillsPage'; +import ReshipPage from './pages/ReshipPage'; +import WeeklyReportPage from './pages/WeeklyReportPage'; +import LeaderboardPage from './pages/LeaderboardPage'; export default function App() { return ( @@ -22,6 +25,9 @@ export default function App() { } /> } /> + } /> + } /> + } /> } /> } /> } /> diff --git a/apps/h5-partner/src/lib/leaderboard.ts b/apps/h5-partner/src/lib/leaderboard.ts new file mode 100644 index 0000000..bf9f7c9 --- /dev/null +++ b/apps/h5-partner/src/lib/leaderboard.ts @@ -0,0 +1,15 @@ +export type LeaderboardEntry = { + rank: number; + name: string; + role: string; + totalStores: number; + monthStores: number; +}; + +/** preV1:贡献榜 Mock,待 `/partner/dashboard/leaderboard` 接入后替换 */ +export const LEADERBOARD_MOCK: LeaderboardEntry[] = [ + { rank: 1, name: '陈经理', role: '内部员工', totalStores: 45, monthStores: 8 }, + { rank: 2, name: '李华', role: '城市合伙人', totalStores: 32, monthStores: 5 }, + { rank: 3, name: '王拓', role: '推广员', totalStores: 18, monthStores: 3 }, + { rank: 4, name: '赵敏', role: '内部员工', totalStores: 12, monthStores: 2 }, +]; diff --git a/apps/h5-partner/src/lib/storeDraft.ts b/apps/h5-partner/src/lib/storeDraft.ts new file mode 100644 index 0000000..cbed359 --- /dev/null +++ b/apps/h5-partner/src/lib/storeDraft.ts @@ -0,0 +1,65 @@ +export type StoreDraftForm = { + name: string; + phone: string; + district: string; + address: string; + intro: string; + bankAccountName: string; + bankAccountNo: string; + bankBranch: string; +}; + +export const STORE_DRAFT_KEY = 'partner_store_draft_v1'; + +export const defaultStoreForm = (): StoreDraftForm => ({ + name: '', + phone: '', + district: '金水区', + address: '', + intro: '', + bankAccountName: '', + bankAccountNo: '', + bankBranch: '', +}); + +export function loadStoreDraft(): StoreDraftForm | null { + try { + const raw = localStorage.getItem(STORE_DRAFT_KEY); + if (!raw) return null; + return { ...defaultStoreForm(), ...JSON.parse(raw) }; + } catch { + return null; + } +} + +export function saveStoreDraft(form: StoreDraftForm) { + localStorage.setItem(STORE_DRAFT_KEY, JSON.stringify(form)); +} + +export function clearStoreDraft() { + localStorage.removeItem(STORE_DRAFT_KEY); +} + +const PHONE_RE = /^1\d{10}$/; +const BANK_RE = /^\d{16,19}$/; + +export function validateStoreStep1(form: StoreDraftForm): string | null { + if (!form.name.trim()) return '请填写门店名称'; + if (!form.phone.trim()) return '请填写联系电话'; + if (!PHONE_RE.test(form.phone.trim())) return '联系电话须为11位手机号'; + if (!form.district.trim()) return '请填写区县'; + if (!form.address.trim()) return '请填写详细地址'; + if (form.intro.trim()) { + const len = form.intro.trim().length; + if (len < 10 || len > 500) return '门店简介须为 10~500 字'; + } + return null; +} + +export function validateStoreStep3(form: StoreDraftForm): string | null { + if (!form.bankAccountName.trim()) return '请填写户主姓名'; + if (!form.bankAccountNo.trim()) return '请填写银行卡号'; + if (!BANK_RE.test(form.bankAccountNo.replace(/\s/g, ''))) return '银行卡号须为 16~19 位数字'; + if (!form.bankBranch.trim()) return '请填写开户支行'; + return null; +} diff --git a/apps/h5-partner/src/pages/HomePage.tsx b/apps/h5-partner/src/pages/HomePage.tsx index 367680f..86c0ae1 100644 --- a/apps/h5-partner/src/pages/HomePage.tsx +++ b/apps/h5-partner/src/pages/HomePage.tsx @@ -1,29 +1,48 @@ -import { useEffect, useState } from 'react'; +import { useEffect, useMemo, useState } from 'react'; import { Link, useNavigate } from 'react-router-dom'; import { isLoggedIn, request } from '../lib/api'; +import { LEADERBOARD_MOCK } from '../lib/leaderboard'; function fmtMoney(n: number) { return n.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 }); } +function auditStatus(store: Record) { + const audits = store.audits as Array<{ status?: string }> | undefined; + return String(audits?.[0]?.status ?? '').toUpperCase(); +} + export default function HomePage() { const navigate = useNavigate(); const [dash, setDash] = useState | null>(null); + const [stores, setStores] = useState>>([]); useEffect(() => { if (!isLoggedIn()) { navigate('/login'); return; } - request('PARTNER_H5', '/partner/dashboard').then(setDash); + request>('PARTNER_H5', '/partner/dashboard').then(setDash); + request>>('PARTNER_H5', '/partner/stores').then(setStores); }, [navigate]); - const storeCount = Number(dash?.storeCount || 0); + const storeCount = Number(dash?.storeCount || stores.length || 0); const orderCount = Number(dash?.orderCount || 0); - const activeStores = Math.max(0, storeCount - Math.ceil(storeCount * 0.05)); - const abnormalStores = storeCount - activeStores; + const activeStores = stores.filter((s) => String(s.status).toUpperCase() === 'OPEN').length; + const abnormalStores = Math.max(0, storeCount - activeStores); + const pendingStores = useMemo( + () => stores.filter((s) => auditStatus(s) === 'PENDING'), + [stores], + ); const revenue = orderCount * 128.45; const profit = revenue * 0.25; const pendingShip = Math.ceil(orderCount * 0.04); const shipping = Math.ceil(orderCount * 0.12); const completed = Math.max(0, orderCount - pendingShip - shipping); + const monthNew = stores.filter((s) => { + const created = new Date(String(s.createdAt || '')); + const now = new Date(); + return created.getMonth() === now.getMonth() && created.getFullYear() === now.getFullYear(); + }).length; + + const previewLeaderboard = LEADERBOARD_MOCK.slice(0, 2); return (
@@ -87,7 +106,7 @@ export default function HomePage() {
录入新店 - +
assignment_return {pendingShip > 0 && {pendingShip}} @@ -100,7 +119,7 @@ export default function HomePage() {
财务对账 - +
monitoring
@@ -139,17 +158,54 @@ export default function HomePage() {

本月新增签约

- {Math.ceil(storeCount * 0.08)} - - trending_up 18% - + {monthNew}

待审核门店

-

{Math.ceil(storeCount * 0.03)}

+

{pendingStores.length}

+ + {stores.length > 0 && ( +
    + {stores.slice(0, 5).map((s) => ( +
  • + {String(s.name)} + + {auditStatus(s) === 'PENDING' ? '待审核' : String(s.status) === 'OPEN' ? '营业中' : String(s.status)} + +
  • + ))} +
+ )} + +
+
+

+ military_tech + 合伙人贡献榜 +

+ 查看全部 +
+ {previewLeaderboard.map((entry) => ( +
+
{entry.rank}
+
+

+ {entry.name} + ({entry.role}) +

+

累计拓店 {entry.totalStores} 间

+
+
+

{entry.monthStores} 间

+

本月新增

+
+
+ ))} +
+

{String(dash?.companyName || '郑州合伙人')} · 辖区管理

diff --git a/apps/h5-partner/src/pages/LeaderboardPage.tsx b/apps/h5-partner/src/pages/LeaderboardPage.tsx new file mode 100644 index 0000000..e2f2885 --- /dev/null +++ b/apps/h5-partner/src/pages/LeaderboardPage.tsx @@ -0,0 +1,36 @@ +import { useNavigate } from 'react-router-dom'; +import PageHeader from '@dukang/shared-ui/PageHeader'; +import { LEADERBOARD_MOCK } from '../lib/leaderboard'; + +export default function LeaderboardPage() { + const navigate = useNavigate(); + + return ( +
+ navigate('/')} /> + +

+ 按拓店数排行,数据每周更新 +

+ +
+ {LEADERBOARD_MOCK.map((entry) => ( +
+
{entry.rank}
+
+

+ {entry.name} + ({entry.role}) +

+

累计拓店 {entry.totalStores} 间

+
+
+

{entry.monthStores} 间

+

本月新增

+
+
+ ))} +
+
+ ); +} diff --git a/apps/h5-partner/src/pages/ReshipPage.tsx b/apps/h5-partner/src/pages/ReshipPage.tsx new file mode 100644 index 0000000..d8dcd68 --- /dev/null +++ b/apps/h5-partner/src/pages/ReshipPage.tsx @@ -0,0 +1,37 @@ +import { useNavigate } from 'react-router-dom'; +import PageHeader from '@dukang/shared-ui/PageHeader'; + +/** BUWR-23:补发处理(preV1 占位,待 `/partner/after-sales/reshipments` 接入) */ +const MOCK_ITEMS = [ + { id: '1', orderNo: 'DK20260301001', status: '待配送', productName: '杜康窖藏', originalOrderNo: 'DK20260228088' }, +]; + +export default function ReshipPage() { + const navigate = useNavigate(); + + return ( +
+ navigate('/')} /> + +

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

+ + {MOCK_ITEMS.length === 0 &&
暂无补发工单
} + + {MOCK_ITEMS.map((item) => ( +
+
+ 原单 {item.originalOrderNo} + {item.status} +
+

{item.productName}

+

补发单号 {item.orderNo}

+ +
+ ))} +
+ ); +} diff --git a/apps/h5-partner/src/pages/StoreCreatePage.tsx b/apps/h5-partner/src/pages/StoreCreatePage.tsx index e6c9ec7..9f04a8c 100644 --- a/apps/h5-partner/src/pages/StoreCreatePage.tsx +++ b/apps/h5-partner/src/pages/StoreCreatePage.tsx @@ -1,7 +1,16 @@ -import { useState } from 'react'; +import { useEffect, useState } from 'react'; import { useNavigate, useSearchParams } from 'react-router-dom'; import PageHeader from '@dukang/shared-ui/PageHeader'; import { request } from '../lib/api'; +import { + clearStoreDraft, + defaultStoreForm, + loadStoreDraft, + saveStoreDraft, + type StoreDraftForm, + validateStoreStep1, + validateStoreStep3, +} from '../lib/storeDraft'; const STEPS = ['基本信息', '照片上传', '结算资质'] as const; @@ -9,28 +18,51 @@ export default function StoreCreatePage() { const navigate = useNavigate(); const [params, setParams] = useSearchParams(); const step = Number(params.get('step') || 1); - const [form, setForm] = useState({ - name: '', - phone: '', - district: '金水区', - address: '', - intro: '', - accountPhone: '', - accountName: '', - bankAccountName: '', - bankAccountNo: '', - bankBranch: '', - }); + const [form, setForm] = useState(() => loadStoreDraft() ?? defaultStoreForm()); + const [error, setError] = useState(''); + + useEffect(() => { + saveStoreDraft(form); + }, [form]); + + function patchForm(patch: Partial) { + setForm((prev) => ({ ...prev, ...patch })); + setError(''); + } function goStep(n: number) { setParams({ step: String(n) }); + setError(''); + } + + function handleNext() { + if (step === 1) { + const msg = validateStoreStep1(form); + if (msg) { setError(msg); return; } + } + goStep(step + 1); } async function submit() { + const msg = validateStoreStep3(form); + if (msg) { setError(msg); return; } + const step1Msg = validateStoreStep1(form); + if (step1Msg) { setError(step1Msg); return; } + await request('PARTNER_H5', '/partner/stores', { method: 'POST', - body: JSON.stringify(form), + body: JSON.stringify({ + name: form.name.trim(), + phone: form.phone.trim(), + district: form.district.trim(), + address: form.address.trim(), + intro: form.intro.trim() || undefined, + bankAccountName: form.bankAccountName.trim(), + bankAccountNo: form.bankAccountNo.replace(/\s/g, ''), + bankBranch: form.bankBranch.trim(), + }), }); + clearStoreDraft(); navigate('/stores'); } @@ -61,6 +93,10 @@ export default function StoreCreatePage() { + {error && ( +

{error}

+ )} + {step === 1 && ( <>
@@ -72,30 +108,30 @@ export default function StoreCreatePage() {
store - setForm({ ...form, name: e.target.value })} /> + patchForm({ name: e.target.value })} />
call - setForm({ ...form, phone: e.target.value })} /> + patchForm({ phone: e.target.value })} />
location_on - setForm({ ...form, district: e.target.value })} /> + patchForm({ district: e.target.value })} />
-