Merge commit 'a0ee3a21cd01229a555384ad5e1f4b3ccdcdd620' into dev_jacy

This commit is contained in:
2026-07-01 19:38:05 +08:00
23 changed files with 1080 additions and 171 deletions
+6
View File
@@ -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() {
</Route>
<Route path="/stores/new" element={<StoreCreatePage />} />
<Route path="/center/bills" element={<BillsPage />} />
<Route path="/reshipments" element={<ReshipPage />} />
<Route path="/reports/weekly" element={<WeeklyReportPage />} />
<Route path="/leaderboard" element={<LeaderboardPage />} />
<Route path="/stores/:id" element={<StoreDetailPage />} />
<Route path="/orders/:id" element={<OrderDetailPage />} />
<Route path="*" element={<Navigate to="/" replace />} />
+15
View File
@@ -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 },
];
+91
View File
@@ -0,0 +1,91 @@
export type StoreDraftForm = {
name: string;
phone: string;
district: string;
address: string;
intro: string;
bankAccountName: string;
bankAccountNo: string;
bankBranch: string;
};
export type StoreDraft = {
step: number;
form: StoreDraftForm;
};
export const STORE_DRAFT_KEY = 'partner_store_draft_v1';
export const defaultStoreForm = (): StoreDraftForm => ({
name: '',
phone: '',
district: '金水区',
address: '',
intro: '',
bankAccountName: '',
bankAccountNo: '',
bankBranch: '',
});
function normalizeForm(raw: Record<string, unknown>): 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;
const parsed = JSON.parse(raw) as Record<string, unknown>;
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<string, unknown>),
};
}
return { step: 1, form: normalizeForm(parsed) };
} catch {
return null;
}
}
export function saveStoreDraft(draft: StoreDraft) {
localStorage.setItem(STORE_DRAFT_KEY, JSON.stringify(draft));
}
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;
}
+22 -1
View File
@@ -103,13 +103,34 @@ export default function CenterPage() {
<Link to="/center/bills" className="partner-menu-item">
<div className="partner-menu-item-left">
<div className="partner-menu-icon"><span className="material-symbols-outlined">description</span></div>
<span className="body-md" style={{ fontSize: 16 }}></span>
<span className="body-md" style={{ fontSize: 16 }}></span>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
{pendingBills.length > 0 && <span className="label-md text-primary" style={{ background: 'rgba(166,29,36,0.1)', padding: '2px 8px', borderRadius: 999 }}>{pendingBills.length}</span>}
<span className="material-symbols-outlined text-muted">chevron_right</span>
</div>
</Link>
<Link to="/reports/weekly" className="partner-menu-item">
<div className="partner-menu-item-left">
<div className="partner-menu-icon"><span className="material-symbols-outlined">monitoring</span></div>
<span className="body-md" style={{ fontSize: 16 }}></span>
</div>
<span className="material-symbols-outlined text-muted">chevron_right</span>
</Link>
<Link to="/reshipments" className="partner-menu-item">
<div className="partner-menu-item-left">
<div className="partner-menu-icon"><span className="material-symbols-outlined">assignment_return</span></div>
<span className="body-md" style={{ fontSize: 16 }}></span>
</div>
<span className="material-symbols-outlined text-muted">chevron_right</span>
</Link>
<Link to="/leaderboard" className="partner-menu-item">
<div className="partner-menu-item-left">
<div className="partner-menu-icon"><span className="material-symbols-outlined">military_tech</span></div>
<span className="body-md" style={{ fontSize: 16 }}></span>
</div>
<span className="material-symbols-outlined text-muted">chevron_right</span>
</Link>
</div>
</section>
+95 -12
View File
@@ -1,29 +1,60 @@
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<string, unknown>) {
const audits = store.audits as Array<{ status?: string }> | undefined;
return String(audits?.[0]?.status ?? '').toUpperCase();
}
function storeStatusLabel(store: Record<string, unknown>) {
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<Record<string, unknown> | null>(null);
const [stores, setStores] = useState<Array<Record<string, unknown>>>([]);
useEffect(() => {
if (!isLoggedIn()) { navigate('/login'); return; }
request('PARTNER_H5', '/partner/dashboard').then(setDash);
request<Record<string, unknown>>('PARTNER_H5', '/partner/dashboard').then(setDash);
request<Array<Record<string, unknown>>>('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 recentFromDash = (dash?.recentStores as Array<Record<string, unknown>> | undefined) ?? [];
const displayStores = stores.length > 0 ? stores : recentFromDash;
const previewLeaderboard = LEADERBOARD_MOCK.slice(0, 2);
return (
<div className="page partner-home">
@@ -87,7 +118,7 @@ export default function HomePage() {
</div>
<span className="partner-quick-action-label"></span>
</Link>
<Link to="/orders" className="partner-quick-action">
<Link to="/reshipments" className="partner-quick-action">
<div className="partner-quick-action-icon partner-quick-action-icon--amber">
<span className="material-symbols-outlined">assignment_return</span>
{pendingShip > 0 && <span className="partner-quick-badge-count">{pendingShip}</span>}
@@ -100,7 +131,7 @@ export default function HomePage() {
</div>
<span className="partner-quick-action-label"></span>
</Link>
<Link to="/orders" className="partner-quick-action">
<Link to="/reports/weekly" className="partner-quick-action">
<div className="partner-quick-action-icon partner-quick-action-icon--blue">
<span className="material-symbols-outlined">monitoring</span>
</div>
@@ -139,17 +170,69 @@ export default function HomePage() {
<div>
<p className="label-md text-muted" style={{ marginBottom: 4 }}></p>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<span className="headline-lg" style={{ fontSize: 24 }}>{Math.ceil(storeCount * 0.08)}</span>
<span className="label-md text-success" style={{ display: 'flex', alignItems: 'center', fontSize: 10 }}>
<span className="material-symbols-outlined" style={{ fontSize: 12 }}>trending_up</span> 18%
</span>
<span className="headline-lg" style={{ fontSize: 24 }}>{monthNew}</span>
</div>
</div>
<div>
<p className="label-md text-muted" style={{ marginBottom: 4 }}></p>
<p className="headline-lg" style={{ fontSize: 24 }}>{Math.ceil(storeCount * 0.03)}</p>
<p className="headline-lg" style={{ fontSize: 24 }}>{pendingStores.length}</p>
</div>
</div>
{pendingStores.length > 0 && (
<div className="partner-pending-stores">
<p className="label-md text-muted" style={{ marginBottom: 8 }}></p>
<ul className="partner-store-name-list partner-store-name-list--compact">
{pendingStores.map((s) => (
<li key={String(s.id)}>
<span className="headline-md">{String(s.name || '未命名门店')}</span>
</li>
))}
</ul>
</div>
)}
{displayStores.length > 0 && (
<ul className="partner-store-name-list">
<li className="partner-store-name-list-header">
<span className="label-md text-muted"></span>
<span className="label-md text-muted"></span>
</li>
{displayStores.slice(0, 5).map((s) => (
<li key={String(s.id)}>
<span className="headline-md">{String(s.name || '未命名门店')}</span>
<span className="label-md text-muted">{storeStatusLabel(s)}</span>
</li>
))}
</ul>
)}
<div className="partner-leaderboard-section">
<div className="partner-leaderboard-header">
<h3 className="headline-md" style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<span className="material-symbols-outlined text-primary" style={{ fontSize: 18 }}>military_tech</span>
</h3>
<Link to="/leaderboard" className="label-md text-primary"></Link>
</div>
{previewLeaderboard.map((entry) => (
<div key={entry.rank} className="partner-leaderboard-row partner-leaderboard-row--compact">
<div className="partner-leaderboard-rank">{entry.rank}</div>
<div className="partner-leaderboard-info">
<p className="body-md" style={{ fontWeight: 600 }}>
{entry.name}
<span className="label-md text-muted" style={{ marginLeft: 4 }}>({entry.role})</span>
</p>
<p className="label-md text-muted"> {entry.totalStores} </p>
</div>
<div className="partner-leaderboard-stat">
<p className="headline-md text-primary">{entry.monthStores} </p>
<p className="label-md text-muted"></p>
</div>
</div>
))}
</div>
<p className="label-md text-muted" style={{ paddingTop: 16, borderTop: '1px solid rgba(226,190,188,0.1)' }}>
{String(dash?.companyName || '郑州合伙人')} ·
</p>
@@ -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 (
<div className="page-no-tab">
<PageHeader title="合伙人贡献榜" onBack={() => navigate('/')} />
<p className="label-md text-muted" style={{ padding: '0 20px 12px' }}>
</p>
<div style={{ padding: '0 20px 24px' }}>
{LEADERBOARD_MOCK.map((entry) => (
<div key={entry.rank} className="partner-leaderboard-row">
<div className="partner-leaderboard-rank">{entry.rank}</div>
<div className="partner-leaderboard-info">
<p className="headline-md">
{entry.name}
<span className="label-md text-muted" style={{ marginLeft: 6, fontWeight: 400 }}>({entry.role})</span>
</p>
<p className="label-md text-muted"> {entry.totalStores} </p>
</div>
<div className="partner-leaderboard-stat">
<p className="headline-md text-primary">{entry.monthStores} </p>
<p className="label-md text-muted"></p>
</div>
</div>
))}
</div>
</div>
);
}
+76
View File
@@ -0,0 +1,76 @@
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import PageHeader from '@dukang/shared-ui/PageHeader';
type ReshipStatus = 'ALL' | 'PENDING' | 'SHIPPING' | 'DONE';
const MOCK_ITEMS = [
{ 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<string, string> = {
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<ReshipStatus>('ALL');
const list = MOCK_ITEMS.filter((item) => filter === 'ALL' || item.status === filter);
return (
<div className="page-no-tab partner-orders-page">
<PageHeader title="补发处理" onBack={() => navigate('/')} />
<p className="label-md text-muted" style={{ padding: '0 20px 8px' }}>
</p>
<div className="partner-filter-row" style={{ padding: '0 20px 12px' }}>
{FILTERS.map((f) => (
<button
key={f.key}
type="button"
className={`partner-chip${filter === f.key ? ' active' : ''}`}
onClick={() => setFilter(f.key)}
>
{f.label}
</button>
))}
</div>
{list.length === 0 && <div className="empty"></div>}
{list.map((item) => (
<div key={item.id} className="partner-order-card" style={{ margin: '0 20px 12px' }}>
<div className="partner-order-card-top">
<span className="label-md text-muted"> {item.originalOrderNo}</span>
<span className="label-md text-primary" style={{ fontWeight: 600 }}>{STATUS_LABEL[item.status]}</span>
</div>
<h3 className="headline-md" style={{ marginTop: 8 }}>{item.productName}</h3>
<p className="label-md text-muted" style={{ marginTop: 4 }}> {item.orderNo}</p>
<div className="partner-order-address" style={{ marginTop: 12 }}>
<span className="material-symbols-outlined" style={{ fontSize: 18 }}>location_on</span>
<p className="line-2-clamp">{item.address}</p>
</div>
{item.status === 'PENDING' && (
<button type="button" className="partner-btn-primary" style={{ marginTop: 16, width: '100%' }}>
</button>
)}
</div>
))}
</div>
);
}
+78 -47
View File
@@ -1,36 +1,77 @@
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;
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 saved = loadStoreDraft();
const [form, setForm] = useState<StoreDraftForm>(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(() => {
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<StoreDraftForm>) {
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 +102,10 @@ export default function StoreCreatePage() {
</div>
</nav>
{error && (
<p className="partner-form-error" role="alert">{error}</p>
)}
{step === 1 && (
<>
<section className="partner-form-card">
@@ -72,30 +117,30 @@ export default function StoreCreatePage() {
<label> <span className="text-primary">*</span></label>
<div className="partner-field-input">
<span className="material-symbols-outlined">store</span>
<input placeholder="请输入门店名称" value={form.name} onChange={(e) => setForm({ ...form, name: e.target.value })} />
<input placeholder="请输入门店名称" value={form.name} onChange={(e) => patchForm({ name: e.target.value })} />
</div>
</div>
<div className="partner-field">
<label> <span className="text-primary">*</span></label>
<div className="partner-field-input">
<span className="material-symbols-outlined">call</span>
<input type="tel" placeholder="请输入联系电话" value={form.phone} onChange={(e) => setForm({ ...form, phone: e.target.value })} />
<input type="tel" placeholder="请输入联系电话" value={form.phone} onChange={(e) => patchForm({ phone: e.target.value })} />
</div>
</div>
<div className="partner-field">
<label> <span className="text-primary">*</span></label>
<div className="partner-field-input">
<span className="material-symbols-outlined">location_on</span>
<input placeholder="请选择区县" value={form.district} onChange={(e) => setForm({ ...form, district: e.target.value })} />
<input placeholder="请选择区县" value={form.district} onChange={(e) => patchForm({ district: e.target.value })} />
</div>
</div>
<div className="partner-field">
<label> <span className="text-primary">*</span></label>
<textarea rows={2} placeholder="请输入详细门牌号" value={form.address} onChange={(e) => setForm({ ...form, address: e.target.value })} />
<textarea rows={2} placeholder="请输入详细门牌号" value={form.address} onChange={(e) => patchForm({ address: e.target.value })} />
</div>
<div className="partner-field">
<label></label>
<textarea rows={4} placeholder="请输入门店简介 (10-500字)" value={form.intro} onChange={(e) => setForm({ ...form, intro: e.target.value })} />
<textarea rows={4} placeholder="请输入门店简介 (10-500字)" value={form.intro} onChange={(e) => patchForm({ intro: e.target.value })} />
<div style={{ textAlign: 'right', marginTop: 4 }}>
<span className="label-md text-muted">{form.intro.length} / 500</span>
</div>
@@ -108,7 +153,7 @@ export default function StoreCreatePage() {
<div>
<h3 className="body-md text-primary" style={{ fontWeight: 700 }}></h3>
<p className="label-md text-variant" style={{ marginTop: 4, lineHeight: 1.4 }}>
1-3
退
</p>
</div>
</div>
@@ -137,18 +182,12 @@ export default function StoreCreatePage() {
<h3 className="headline-md"> *</h3>
<p className="label-md text-muted" style={{ margin: '4px 0 12px' }}>3</p>
<div className="partner-upload-grid">
<button type="button" className="partner-upload-dashed">
<span className="material-symbols-outlined text-primary">add_a_photo</span>
<span></span>
</button>
<button type="button" className="partner-upload-dashed">
<span className="material-symbols-outlined text-primary">add_a_photo</span>
<span></span>
</button>
<button type="button" className="partner-upload-dashed">
<span className="material-symbols-outlined text-primary">add_a_photo</span>
<span></span>
</button>
{[0, 1, 2].map((i) => (
<button key={i} type="button" className="partner-upload-dashed">
<span className="material-symbols-outlined text-primary">add_a_photo</span>
<span></span>
</button>
))}
</div>
</section>
@@ -169,7 +208,7 @@ export default function StoreCreatePage() {
<div className="partner-info-banner" style={{ background: 'rgba(254,214,91,0.2)', borderColor: 'rgba(254,214,91,0.5)' }}>
<span className="material-symbols-outlined text-secondary" style={{ fontSize: 18 }}>info</span>
<p className="label-md" style={{ lineHeight: 1.5, color: 'var(--color-on-secondary-container)' }}>
preV1
preV1
</p>
</div>
</>
@@ -184,29 +223,21 @@ export default function StoreCreatePage() {
<section className="partner-form-card">
<div className="partner-field">
<label> *</label>
<input className="partner-field-input" style={{ width: '100%', padding: '0 16px', minHeight: 48, border: '1px solid rgba(226,190,188,0.3)', borderRadius: 8, background: 'var(--color-background)' }} placeholder="请输入银行卡实名姓名" value={form.bankAccountName} onChange={(e) => setForm({ ...form, bankAccountName: e.target.value })} />
<input className="partner-field-input partner-field-input--block" placeholder="请输入银行卡实名姓名" value={form.bankAccountName} onChange={(e) => patchForm({ bankAccountName: e.target.value })} />
</div>
<div className="partner-field">
<label> *</label>
<input className="partner-field-input" style={{ width: '100%', padding: '0 16px', minHeight: 48, border: '1px solid rgba(226,190,188,0.3)', borderRadius: 8, background: 'var(--color-background)' }} placeholder="请输入16-19位银行卡号" value={form.bankAccountNo} onChange={(e) => setForm({ ...form, bankAccountNo: e.target.value })} />
<input className="partner-field-input partner-field-input--block" placeholder="请输入16-19位银行卡号" value={form.bankAccountNo} onChange={(e) => patchForm({ bankAccountNo: e.target.value })} />
</div>
<div className="partner-field">
<label> *</label>
<input className="partner-field-input" style={{ width: '100%', padding: '0 16px', minHeight: 48, border: '1px solid rgba(226,190,188,0.3)', borderRadius: 8, background: 'var(--color-background)' }} placeholder="例如:中国工商银行洛阳分行" value={form.bankBranch} onChange={(e) => setForm({ ...form, bankBranch: e.target.value })} />
</div>
<div className="partner-field">
<label></label>
<input className="partner-field-input" style={{ width: '100%', padding: '0 16px', minHeight: 48, border: '1px solid rgba(226,190,188,0.3)', borderRadius: 8, background: 'var(--color-background)' }} value={form.accountPhone} onChange={(e) => setForm({ ...form, accountPhone: e.target.value })} />
</div>
<div className="partner-field">
<label></label>
<input className="partner-field-input" style={{ width: '100%', padding: '0 16px', minHeight: 48, border: '1px solid rgba(226,190,188,0.3)', borderRadius: 8, background: 'var(--color-background)' }} value={form.accountName} onChange={(e) => setForm({ ...form, accountName: e.target.value })} />
<input className="partner-field-input partner-field-input--block" placeholder="例如:中国工商银行洛阳分行" value={form.bankBranch} onChange={(e) => patchForm({ bankBranch: e.target.value })} />
</div>
</section>
<div className="partner-info-banner" style={{ background: 'rgba(254,214,91,0.15)', borderColor: 'rgba(254,214,91,0.3)' }}>
<span className="material-symbols-outlined text-secondary">info</span>
<p className="body-md" style={{ color: 'var(--color-on-secondary-container)' }}>
</p>
</div>
</>
@@ -217,7 +248,7 @@ export default function StoreCreatePage() {
<button type="button" className="partner-btn-outline" onClick={() => goStep(step - 1)}></button>
)}
{step < 3 ? (
<button type="button" className="partner-btn-primary" onClick={() => goStep(step + 1)}>
<button type="button" className="partner-btn-primary" onClick={handleNext}>
<span></span>
<span className="material-symbols-outlined" style={{ fontSize: 20 }}>navigate_next</span>
</button>
+17 -2
View File
@@ -18,6 +18,15 @@ const FILTERS: { key: StatusFilter; label: string }[] = [
{ key: 'CLOSED', label: '已关闭' },
];
function auditLabel(store: Record<string, unknown>) {
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<Array<Record<string, unknown>>>([]);
@@ -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 (
<div key={String(s.id)} className={`partner-store-card${dim ? ' partner-store-card--dim' : ''}`}>
<Link to={`/stores/${s.id}`} style={{ color: 'inherit', textDecoration: 'none' }}>
<div className="partner-store-card-header">
<div>
<h3 className="headline-md">{String(s.name)}</h3>
<p className="label-md text-muted" style={{ marginBottom: 2 }}></p>
<h3 className="headline-md">{storeName}</h3>
<p className="label-md text-muted" style={{ marginTop: 4 }}>{String(s.address || s.district || '')}</p>
</div>
<span className={`partner-status-pill ${pill.cls}`}>{pill.label}</span>
<div style={{ display: 'flex', flexDirection: 'column', gap: 4, alignItems: 'flex-end' }}>
<span className={`partner-status-pill ${pill.cls}`}>{pill.label}</span>
{audit && <span className={`partner-status-pill ${audit.cls}`}>{audit.label}</span>}
</div>
</div>
</Link>
<div className="partner-store-card-actions">
@@ -0,0 +1,43 @@
import { useNavigate } from 'react-router-dom';
import PageHeader from '@dukang/shared-ui/PageHeader';
function fmtMoney(n: number) {
return n.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
}
/** BUWR-25:经营周报(preV1 占位,待 `/partner/reports/weekly` 接入) */
export default function WeeklyReportPage() {
const navigate = useNavigate();
return (
<div className="page-no-tab">
<PageHeader title="数据周报" onBack={() => navigate('/')} />
<main style={{ padding: '0 20px 24px' }}>
<section className="partner-revenue-card" style={{ marginBottom: 16 }}>
<p className="partner-revenue-label"> 7 GMV (CNY)</p>
<div className="partner-revenue-amount">{fmtMoney(128450)}</div>
</section>
<section className="partner-bento-card" style={{ marginBottom: 12 }}>
<div className="partner-expansion-split">
<div>
<p className="label-md text-muted"></p>
<p className="headline-lg" style={{ fontSize: 24, marginTop: 4 }}>86</p>
</div>
<div>
<p className="label-md text-muted"></p>
<p className="headline-lg" style={{ fontSize: 24, marginTop: 4 }}>12</p>
</div>
</div>
</section>
<section className="partner-bento-card">
<h2 className="headline-md" style={{ marginBottom: 12 }}></h2>
<p className="headline-lg text-primary">3 </p>
<p className="label-md text-muted" style={{ marginTop: 8 }}> API </p>
</section>
</main>
</div>
);
}
+100
View File
@@ -581,6 +581,106 @@ html, body, #root {
border-left: 1px solid rgba(226, 190, 188, 0.2);
}
.partner-form-error {
margin: 0 20px 12px;
padding: 10px 12px;
border-radius: 8px;
background: var(--color-error-container);
color: var(--color-error);
font-size: 14px;
}
.partner-store-name-list {
list-style: none;
margin: 0 0 16px;
padding: 0;
border-bottom: 1px solid rgba(226, 190, 188, 0.15);
padding-bottom: 12px;
}
.partner-store-name-list li {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 0;
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;
}
.partner-leaderboard-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.partner-leaderboard-row {
display: flex;
align-items: center;
gap: 12px;
padding: 10px 0;
border-bottom: 1px solid rgba(226, 190, 188, 0.1);
}
.partner-leaderboard-row--compact {
padding: 8px 0;
}
.partner-leaderboard-rank {
width: 28px;
height: 28px;
border-radius: 50%;
background: var(--color-surface-container-high);
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
font-weight: 700;
flex-shrink: 0;
}
.partner-leaderboard-row:first-of-type .partner-leaderboard-rank {
background: var(--color-aged-amber);
color: #fff;
}
.partner-leaderboard-info {
flex: 1;
min-width: 0;
}
.partner-leaderboard-stat {
text-align: right;
flex-shrink: 0;
}
/* ── Partner bottom tabbar ── */
.app-tabbar {
display: grid;