Merge commit 'a0ee3a21cd01229a555384ad5e1f4b3ccdcdd620' into dev_jacy
This commit is contained in:
@@ -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 />} />
|
||||
|
||||
@@ -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 },
|
||||
];
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user