This commit is contained in:
2026-06-30 10:33:56 +08:00
commit 6e047dc0a5
607 changed files with 65966 additions and 0 deletions
+30
View File
@@ -0,0 +1,30 @@
import { Routes, Route, Navigate } from 'react-router-dom';
import TabLayout from './layouts/TabLayout';
import LoginPage from './pages/LoginPage';
import HomePage from './pages/HomePage';
import StoreListPage from './pages/StoreListPage';
import StoreDetailPage from './pages/StoreDetailPage';
import StoreCreatePage from './pages/StoreCreatePage';
import OrderListPage from './pages/OrderListPage';
import OrderDetailPage from './pages/OrderDetailPage';
import CenterPage from './pages/CenterPage';
import BillsPage from './pages/BillsPage';
export default function App() {
return (
<Routes>
<Route path="/login" element={<LoginPage />} />
<Route element={<TabLayout />}>
<Route path="/" element={<HomePage />} />
<Route path="/stores" element={<StoreListPage />} />
<Route path="/orders" element={<OrderListPage />} />
<Route path="/center" element={<CenterPage />} />
</Route>
<Route path="/stores/new" element={<StoreCreatePage />} />
<Route path="/center/bills" element={<BillsPage />} />
<Route path="/stores/:id" element={<StoreDetailPage />} />
<Route path="/orders/:id" element={<OrderDetailPage />} />
<Route path="*" element={<Navigate to="/" replace />} />
</Routes>
);
}
+34
View File
@@ -0,0 +1,34 @@
import { NavLink, Outlet, useNavigate } from 'react-router-dom';
import { isLoggedIn } from '../lib/api';
const TABS = [
{ to: '/', end: true, icon: 'dashboard', label: '首页' },
{ to: '/stores', icon: 'store', label: '门店管理' },
{ to: '/center', icon: 'account_circle', label: '合伙人中心' },
] as const;
export default function TabLayout() {
const navigate = useNavigate();
if (!isLoggedIn()) {
navigate('/login');
return null;
}
return (
<>
<Outlet />
<nav className="app-tabbar">
{TABS.map((tab) => (
<NavLink
key={tab.to}
to={tab.to}
end={tab.end}
className={({ isActive }) => `app-tabbar-item${isActive ? ' active' : ''}`}
>
<span className="material-symbols-outlined app-tabbar-icon">{tab.icon}</span>
<span className="app-tabbar-label">{tab.label}</span>
</NavLink>
))}
</nav>
</>
);
}
+27
View File
@@ -0,0 +1,27 @@
export const apiBase = '/api/v1';
export async function request<T>(clientApp: string, path: string, options: RequestInit = {}): Promise<T> {
const token = localStorage.getItem('accessToken');
const headers: Record<string, string> = {
'Content-Type': 'application/json',
'X-Client-App': clientApp,
...(options.headers as Record<string, string>),
};
if (token) headers.Authorization = `Bearer ${token}`;
const res = await fetch(`${apiBase}${path}`, { ...options, headers });
const json = await res.json();
if (json.code !== 0) throw new Error(json.message || '请求失败');
return json.data as T;
}
export function saveAuth(data: { accessToken: string }) {
localStorage.setItem('accessToken', data.accessToken);
}
export function clearAuth() {
localStorage.removeItem('accessToken');
}
export function isLoggedIn() {
return !!localStorage.getItem('accessToken');
}
+9
View File
@@ -0,0 +1,9 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import App from './App';
import './styles.css';
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode><BrowserRouter><App /></BrowserRouter></React.StrictMode>,
);
+134
View File
@@ -0,0 +1,134 @@
import { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import PageHeader from '@dukang/shared-ui/PageHeader';
import { isLoggedIn, request } from '../lib/api';
export default function BillsPage() {
const navigate = useNavigate();
const [bills, setBills] = useState<Array<Record<string, unknown>>>([]);
const [confirmed, setConfirmed] = useState(false);
const [submitting, setSubmitting] = useState(false);
useEffect(() => {
if (!isLoggedIn()) { navigate('/login'); return; }
request<Array<Record<string, unknown>>>('PARTNER_H5', '/partner/settlement/bills').then(setBills);
}, [navigate]);
const bill = bills[0];
function confirmBill() {
if (!confirmed) return;
setSubmitting(true);
setTimeout(() => {
setSubmitting(false);
alert('申请已提交,请耐心等待打款审核。(preV1 演示)');
}, 1500);
}
return (
<div className="partner-bills-page">
<PageHeader title="待确认账单" onBack={() => navigate('/center')} />
<div className="partner-bill-stepper">
<div className="partner-stepper-inner">
<div className="partner-stepper-line" aria-hidden>
<div className="partner-stepper-line-fill" style={{ width: '50%' }} />
</div>
<div className="partner-step">
<div className="partner-step-circle partner-step-circle--sm partner-step-circle--done">
<span className="material-symbols-outlined" style={{ fontSize: 14, fontVariationSettings: "'FILL' 1" }}>check</span>
</div>
<span className="partner-step-label partner-step-label--active"></span>
</div>
<div className="partner-step">
<div className="partner-step-circle partner-step-circle--sm partner-step-circle--active">2</div>
<span className="partner-step-label partner-step-label--active"></span>
</div>
<div className="partner-step">
<div className="partner-step-circle partner-step-circle--sm">3</div>
<span className="partner-step-label"></span>
</div>
</div>
</div>
{!bill && <div className="empty"></div>}
{bill && (
<section className="partner-bill-card">
<div className="partner-bill-card-bar" />
<div style={{ padding: 16 }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: 24 }}>
<div>
<p className="label-md text-muted" style={{ textTransform: 'uppercase', letterSpacing: '0.1em', marginBottom: 4 }}>SETTLEMENT PERIOD</p>
<h2 className="headline-lg" style={{ fontSize: 20 }}>{String(bill.billNo || '月度结算账单')}</h2>
</div>
<span className="partner-status-pill partner-status-pill--paused">{String(bill.status || '待确认')}</span>
</div>
<div className="partner-bill-amount">
<p className="text-muted body-md" style={{ marginBottom: 8 }}></p>
<div style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'center', gap: 4 }}>
<span className="text-primary" style={{ fontWeight: 700, fontSize: 20 }}>¥</span>
<span className="amount-xl">{Number(bill.totalAmount || 0).toLocaleString('zh-CN', { minimumFractionDigits: 2 })}</span>
</div>
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
<div className="partner-info-row">
<span className="text-variant body-md"></span>
<span className="body-md" style={{ fontWeight: 700 }}>¥ {(Number(bill.totalAmount || 0) * 0.82).toLocaleString('zh-CN', { minimumFractionDigits: 2 })}</span>
</div>
<div className="partner-info-row">
<span className="text-variant body-md"></span>
<span className="body-md" style={{ fontWeight: 700 }}>¥ {(Number(bill.totalAmount || 0) * 0.18).toLocaleString('zh-CN', { minimumFractionDigits: 2 })}</span>
</div>
</div>
</div>
</section>
)}
{bills.length > 1 && (
<div style={{ padding: '0 20px' }}>
<h3 className="headline-md" style={{ marginBottom: 12 }}></h3>
{bills.slice(1).map((b) => (
<div key={String(b.id)} className="partner-store-card" style={{ margin: '0 0 12px' }}>
<div className="partner-store-card-header" style={{ marginBottom: 0 }}>
<div>
<p className="body-md">{String(b.billNo)}</p>
<p className="label-md text-muted">{String(b.status)}</p>
</div>
<span className="amount-lg" style={{ fontSize: 18 }}>¥{Number(b.totalAmount).toFixed(2)}</span>
</div>
</div>
))}
</div>
)}
<div className="partner-info-banner" style={{ marginTop: 16 }}>
<span className="material-symbols-outlined text-primary" style={{ fontVariationSettings: "'FILL' 1" }}>info</span>
<p className="body-md text-variant" style={{ lineHeight: 1.5 }}>
</p>
</div>
{bill && (
<footer className="partner-bill-footer">
<label className="partner-checkbox-row" style={{ marginBottom: 16 }}>
<input type="checkbox" checked={confirmed} onChange={(e) => setConfirmed(e.target.checked)} />
<span></span>
</label>
<button
type="button"
className="partner-btn-primary"
disabled={!confirmed || submitting}
style={{ opacity: confirmed ? 1 : 0.5 }}
onClick={confirmBill}
>
{submitting ? '正在提交...' : '确认并申请打款'}
{!submitting && <span className="material-symbols-outlined">payments</span>}
</button>
</footer>
)}
</div>
);
}
+126
View File
@@ -0,0 +1,126 @@
import { useEffect, useState } from 'react';
import { Link, useNavigate } from 'react-router-dom';
import { clearAuth, isLoggedIn, request } from '../lib/api';
export default function CenterPage() {
const navigate = useNavigate();
const [me, setMe] = useState<Record<string, unknown> | null>(null);
const [bills, setBills] = useState<Array<Record<string, unknown>>>([]);
useEffect(() => {
if (!isLoggedIn()) { navigate('/login'); return; }
request('PARTNER_H5', '/partner/me').then(setMe);
request<Array<Record<string, unknown>>>('PARTNER_H5', '/partner/settlement/bills').then(setBills);
}, [navigate]);
const pendingBills = bills.filter((b) => String(b.status).includes('PENDING') || String(b.status).includes('CONFIRM'));
return (
<div className="page partner-center-page">
<header className="header app-page-header">
<h1 className="app-page-title"></h1>
</header>
<section className="partner-profile-card">
<div className="partner-profile-avatar">
<span className="material-symbols-outlined">person</span>
</div>
<div style={{ flex: 1 }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<h2 className="headline-lg" style={{ fontSize: 20 }}>{String(me?.name || '合伙人')}</h2>
<span className="partner-role-badge"></span>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 4, marginTop: 4, color: 'var(--color-subtle-gray)' }}>
<span className="material-symbols-outlined" style={{ fontSize: 16 }}>location_on</span>
<span className="body-md">{String(me?.companyName || '郑州')}</span>
</div>
<p className="label-md text-muted" style={{ marginTop: 4 }}>{String(me?.phone || '')}</p>
</div>
</section>
<section>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '0 20px 8px' }}>
<h3 className="headline-md"></h3>
<Link to="/center/bills" className="label-md text-primary" style={{ display: 'flex', alignItems: 'center' }}>
<span className="material-symbols-outlined" style={{ fontSize: 16 }}>chevron_right</span>
</Link>
</div>
<Link to="/center/bills" className="partner-bills-banner">
<div className="partner-bills-banner-left">
<div className="partner-bills-icon">
<span className="material-symbols-outlined">pending_actions</span>
</div>
<div>
<p className="body-md" style={{ fontWeight: 500 }}></p>
<p className="label-md text-primary"> {pendingBills.length || bills.length} </p>
</div>
</div>
<span className="material-symbols-outlined text-primary">chevron_right</span>
</Link>
<div className="partner-finance-grid">
<div className="partner-finance-card partner-finance-card--hero">
<p className="label-md" style={{ opacity: 0.8, marginBottom: 4 }}> ()</p>
<span className="amount-xl" style={{ color: '#fff', fontSize: 32 }}>
{bills.length > 0 ? Number(bills[0].totalAmount || 0).toLocaleString('zh-CN', { minimumFractionDigits: 2 }) : '0.00'}
</span>
</div>
<div className="partner-finance-card">
<p className="label-md text-muted" style={{ marginBottom: 4 }}></p>
<p className="headline-md">
<span className="text-primary">¥</span>
{pendingBills.reduce((s, b) => s + Number(b.totalAmount || 0), 0).toLocaleString('zh-CN', { minimumFractionDigits: 2 })}
</p>
</div>
<div className="partner-finance-card">
<p className="label-md text-muted" style={{ marginBottom: 4 }}></p>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<span className="headline-md">{bills.length} </span>
<span className="material-symbols-outlined text-muted">history</span>
</div>
</div>
</div>
</section>
<section className="partner-menu-section">
<h3 className="headline-md" style={{ marginBottom: 8 }}></h3>
<div className="partner-menu-card">
<Link to="/stores" className="partner-menu-item">
<div className="partner-menu-item-left">
<div className="partner-menu-icon"><span className="material-symbols-outlined">store</span></div>
<span className="body-md" style={{ fontSize: 16 }}></span>
</div>
<span className="material-symbols-outlined text-muted">chevron_right</span>
</Link>
<Link to="/orders" className="partner-menu-item">
<div className="partner-menu-item-left">
<div className="partner-menu-icon"><span className="material-symbols-outlined">receipt_long</span></div>
<span className="body-md" style={{ fontSize: 16 }}></span>
</div>
<span className="material-symbols-outlined text-muted">chevron_right</span>
</Link>
<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>
</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>
</div>
</section>
<button type="button" className="partner-logout-btn" onClick={() => { clearAuth(); navigate('/login'); }}>
<span className="material-symbols-outlined">logout</span>
退
</button>
<div style={{ textAlign: 'center', opacity: 0.3, padding: '32px 0' }}>
<p className="label-md text-muted"> · </p>
</div>
</div>
);
}
+160
View File
@@ -0,0 +1,160 @@
import { useEffect, useState } from 'react';
import { Link, useNavigate } from 'react-router-dom';
import { isLoggedIn, request } from '../lib/api';
function fmtMoney(n: number) {
return n.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
}
export default function HomePage() {
const navigate = useNavigate();
const [dash, setDash] = useState<Record<string, unknown> | null>(null);
useEffect(() => {
if (!isLoggedIn()) { navigate('/login'); return; }
request('PARTNER_H5', '/partner/dashboard').then(setDash);
}, [navigate]);
const storeCount = Number(dash?.storeCount || 0);
const orderCount = Number(dash?.orderCount || 0);
const activeStores = Math.max(0, storeCount - Math.ceil(storeCount * 0.05));
const abnormalStores = storeCount - activeStores;
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);
return (
<div className="page partner-home">
<header className="partner-home-header">
<h1 className="app-page-title"></h1>
<div className="partner-home-header-actions">
<button type="button" className="partner-notif-btn" aria-label="通知">
<span className="material-symbols-outlined">notifications</span>
<span className="partner-notif-dot" />
</button>
<div className="partner-profile-avatar" style={{ width: 40, height: 40 }}>
<span className="material-symbols-outlined" style={{ fontSize: 20 }}>person</span>
</div>
</div>
</header>
<main className="partner-home-body">
<section className="partner-revenue-card">
<p className="partner-revenue-label">
(CNY)
<span className="material-symbols-outlined" style={{ fontSize: 14 }}>info</span>
</p>
<div className="partner-revenue-amount">{fmtMoney(revenue)}</div>
<div className="partner-revenue-grid">
<div>
<p className="partner-revenue-label"></p>
<p className="headline-md" style={{ color: '#fff', marginTop: 4 }}>¥ {fmtMoney(profit)}</p>
</div>
</div>
</section>
<div className="partner-bento">
<section className="partner-bento-card">
<div className="partner-bento-header">
<h2 className="headline-md"></h2>
<span className="amount-lg" style={{ fontSize: 24 }}>{storeCount}</span>
</div>
<div className="partner-store-stats">
<div className="partner-store-stat">
<span className="partner-dot partner-dot--green" />
<div>
<p className="label-md text-muted"></p>
<p className="headline-md">{activeStores}</p>
</div>
</div>
<div className="partner-store-stat">
<span className="partner-dot partner-dot--red" />
<div>
<p className="label-md text-muted">/</p>
<p className="headline-md">{abnormalStores}</p>
</div>
</div>
</div>
</section>
<section className="partner-bento-card">
<div className="partner-quick-actions">
<Link to="/stores/new" className="partner-quick-action">
<div className="partner-quick-action-icon partner-quick-action-icon--red">
<span className="material-symbols-outlined">add_business</span>
</div>
<span className="partner-quick-action-label"></span>
</Link>
<Link to="/orders" 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>}
</div>
<span className="partner-quick-action-label"></span>
</Link>
<Link to="/center/bills" className="partner-quick-action">
<div className="partner-quick-action-icon partner-quick-action-icon--green">
<span className="material-symbols-outlined">account_balance</span>
</div>
<span className="partner-quick-action-label"></span>
</Link>
<Link to="/orders" className="partner-quick-action">
<div className="partner-quick-action-icon partner-quick-action-icon--blue">
<span className="material-symbols-outlined">monitoring</span>
</div>
<span className="partner-quick-action-label"></span>
</Link>
</div>
</section>
</div>
<section>
<div className="partner-order-summary-header">
<h2 className="headline-md"> {orderCount}</h2>
<Link to="/orders" className="label-md text-primary" style={{ display: 'flex', alignItems: 'center', gap: 2 }}>
<span className="material-symbols-outlined" style={{ fontSize: 14 }}>chevron_right</span>
</Link>
</div>
<div className="partner-order-stats">
<div className="partner-order-stat">
<p className="label-md text-muted" style={{ marginBottom: 4 }}></p>
<p className="headline-lg" style={{ fontSize: 24, fontWeight: 600 }}>{pendingShip}</p>
</div>
<div className="partner-order-stat partner-order-stat--blue">
<p className="label-md text-muted" style={{ marginBottom: 4 }}></p>
<p className="headline-lg" style={{ fontSize: 24, fontWeight: 600 }}>{shipping}</p>
</div>
<div className="partner-order-stat partner-order-stat--green">
<p className="label-md text-muted" style={{ marginBottom: 4 }}></p>
<p className="headline-lg" style={{ fontSize: 24, fontWeight: 600 }}>{completed}</p>
</div>
</div>
</section>
<section className="partner-expansion-card">
<h2 className="headline-md" style={{ marginBottom: 16 }}></h2>
<div className="partner-expansion-split">
<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>
</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>
</div>
</div>
<p className="label-md text-muted" style={{ paddingTop: 16, borderTop: '1px solid rgba(226,190,188,0.1)' }}>
{String(dash?.companyName || '郑州合伙人')} ·
</p>
</section>
</main>
</div>
);
}
+148
View File
@@ -0,0 +1,148 @@
import { useState } from 'react';
import { Link, useNavigate, useSearchParams } from 'react-router-dom';
import { request, saveAuth } from '../lib/api';
export default function LoginPage() {
const navigate = useNavigate();
const [params] = useSearchParams();
const quick = params.get('quick') === '1';
const [phone, setPhone] = useState('13700000001');
const [code, setCode] = useState('123456');
const [loading, setLoading] = useState(false);
const [codeCooldown, setCodeCooldown] = useState(0);
async function login() {
setLoading(true);
try {
await request('PARTNER_H5', '/partner/auth/sms/send', {
method: 'POST',
body: JSON.stringify({ phone, scene: 'PARTNER_LOGIN' }),
});
const data = await request<{ accessToken: string }>('PARTNER_H5', '/partner/auth/login/sms', {
method: 'POST',
body: JSON.stringify({ phone, code }),
});
saveAuth(data);
navigate('/');
} finally {
setLoading(false);
}
}
function sendCode() {
if (codeCooldown > 0) return;
request('PARTNER_H5', '/partner/auth/sms/send', {
method: 'POST',
body: JSON.stringify({ phone, scene: 'PARTNER_LOGIN' }),
}).then(() => {
setCodeCooldown(60);
const timer = setInterval(() => {
setCodeCooldown((s) => {
if (s <= 1) { clearInterval(timer); return 0; }
return s - 1;
});
}, 1000);
});
}
if (quick) {
return (
<div className="partner-auth-page partner-auth-page--quick">
<header className="partner-auth-brand">
<div className="partner-quick-avatar" style={{ width: 120, height: 120, margin: '0 auto 16px' }}>
<span className="material-symbols-outlined" style={{ fontSize: 48 }}>wine_bar</span>
</div>
<h1 className="partner-auth-title" style={{ fontSize: 20 }}></h1>
<p className="partner-auth-subtitle" style={{ fontSize: 12, letterSpacing: '0.2em', textTransform: 'uppercase' }}></p>
</header>
<section className="partner-glass-card">
<div className="partner-quick-badge"></div>
<div className="partner-quick-avatar">
<span className="material-symbols-outlined">person</span>
</div>
<div style={{ textAlign: 'center' }}>
<h2 className="headline-md">
<span className="text-muted body-md" style={{ fontWeight: 400 }}>()</span>
</h2>
<p className="text-muted body-md" style={{ letterSpacing: '0.1em', marginTop: 4 }}>137 **** 0001</p>
</div>
</section>
<nav style={{ width: '100%', maxWidth: 384, marginTop: 16 }}>
<button type="button" className="partner-btn-primary" onClick={login} disabled={loading}>
<span>{loading ? '登录中...' : '一键登录'}</span>
{!loading && <span className="material-symbols-outlined">arrow_forward</span>}
</button>
<Link to="/login" className="partner-btn-ghost" style={{ display: 'block', marginTop: 12 }}></Link>
</nav>
<footer className="partner-auth-footer">
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span className="material-symbols-outlined">verified_user</span>
<span className="label-md" style={{ fontSize: 10, letterSpacing: '0.1em', textTransform: 'uppercase' }}>Secured by Dukang Heritage</span>
</div>
</footer>
</div>
);
}
return (
<div className="partner-auth-page">
<div className="partner-auth-brand">
<div className="partner-auth-logo-circle">
<span className="material-symbols-outlined">wine_bar</span>
</div>
<h1 className="partner-auth-title"></h1>
<p className="partner-auth-subtitle"></p>
</div>
<main className="partner-auth-card">
<h2 className="partner-auth-card-title"></h2>
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
<div className="partner-input-wrap">
<span className="material-symbols-outlined partner-input-icon">smartphone</span>
<input className="partner-input" type="tel" placeholder="请输入手机号" value={phone} onChange={(e) => setPhone(e.target.value)} />
</div>
<div className="partner-input-row">
<div className="partner-input-wrap">
<span className="material-symbols-outlined partner-input-icon">shield</span>
<input className="partner-input" type="text" placeholder="验证码" value={code} onChange={(e) => setCode(e.target.value)} />
</div>
<button type="button" className="partner-code-btn" onClick={sendCode} disabled={codeCooldown > 0}>
{codeCooldown > 0 ? `${codeCooldown}s 后重发` : '获取验证码'}
</button>
</div>
<label className="partner-checkbox-row">
<input type="checkbox" defaultChecked />
<span></span>
</label>
<button type="button" className="partner-btn-primary" onClick={login} disabled={loading}>
<span>{loading ? '登录中...' : '登录'}</span>
{!loading && <span className="material-symbols-outlined">arrow_forward</span>}
</button>
<div className="partner-auth-divider"><span></span></div>
<button type="button" className="partner-btn-outline" style={{ width: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 8 }}>
<svg width="24" height="24" viewBox="0 0 24 24" fill="#07C160"><path d="M8.25 4.5C4.52 4.5 1.5 7.04 1.5 10.17c0 1.78.98 3.37 2.5 4.48l-.63 1.88 2.19-1.09c.84.24 1.74.38 2.69.38.25 0 .5 0 .75-.03-.16-.53-.25-1.09-.25-1.66 0-3.13 3.02-5.67 6.75-5.67.57 0 1.13.06 1.66.17C15.17 6.13 12 4.5 8.25 4.5zm10.5 6.33c-3.11 0-5.62 2.12-5.62 4.73 0 2.61 2.51 4.73 5.62 4.73.79 0 1.54-.14 2.24-.38l1.83.91-.53-1.57c1.27-.92 2.08-2.25 2.08-3.73 0-2.61-2.51-4.73-5.62-4.73z" /></svg>
</button>
<label className="partner-checkbox-row">
<input type="checkbox" />
<span>
<span className="text-primary" style={{ fontWeight: 600 }}></span> <span className="text-primary" style={{ fontWeight: 600 }}></span>
</span>
</label>
</div>
</main>
<Link to="/login?quick=1" className="partner-link"></Link>
<footer className="partner-auth-footer">
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span className="material-symbols-outlined">verified_user</span>
<span className="label-md" style={{ fontSize: 10, letterSpacing: '0.15em', textTransform: 'uppercase' }}>SECURED BY DUKANG HERITAGE</span>
</div>
</footer>
</div>
);
}
@@ -0,0 +1,179 @@
import { useEffect, useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import PageHeader from '@dukang/shared-ui/PageHeader';
import { request } from '../lib/api';
const TIMELINE = [
{ key: 'confirm', label: '待确认' },
{ key: 'accepted', label: '已接单' },
{ key: 'delivering', label: '配送中' },
{ key: 'shipping', label: '运输中', desc: '包裹正在送往目的地' },
{ key: 'done', label: '已送达' },
];
function statusIndex(status: string) {
const s = String(status).toUpperCase();
if (s === 'COMPLETED') return 4;
if (s.includes('SHIP') || s === 'OUT_WAREHOUSE') return 3;
if (s === 'PENDING_RECEIVE') return 3;
if (s.includes('DELIVER')) return 2;
return 1;
}
function statusBanner(status: string) {
const s = String(status).toUpperCase();
if (s.includes('SHIP') || s === 'OUT_WAREHOUSE') return { title: '运输中', desc: '预计今日 18:00 前送达', icon: 'local_shipping' };
if (s === 'COMPLETED') return { title: '已完成', desc: '订单已送达', icon: 'check_circle' };
if (s.includes('PENDING')) return { title: '待发货', desc: '商家正在备货', icon: 'inventory_2' };
return { title: status, desc: '', icon: 'receipt_long' };
}
export default function OrderDetailPage() {
const { id } = useParams();
const navigate = useNavigate();
const [order, setOrder] = useState<Record<string, unknown> | null>(null);
useEffect(() => {
if (id) request('PARTNER_H5', `/partner/orders/${id}`).then(setOrder);
}, [id]);
async function advance(status: string) {
await request('PARTNER_H5', `/partner/orders/${id}/mock-advance-delivery`, {
method: 'POST',
body: JSON.stringify({ targetStatus: status }),
});
if (id) request('PARTNER_H5', `/partner/orders/${id}`).then(setOrder);
}
if (!order) return <div className="empty">...</div>;
const banner = statusBanner(String(order.status));
const currentIdx = statusIndex(String(order.status));
const payAmount = Number(order.payAmount || 0);
return (
<div className="partner-order-detail">
<PageHeader title="订单详情" onBack={() => navigate('/orders')} />
<section className="partner-status-banner">
<div style={{ position: 'relative', zIndex: 1 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 4 }}>
<span className="material-symbols-outlined" style={{ fontVariationSettings: "'FILL' 1" }}>{banner.icon}</span>
<h2 className="headline-md" style={{ color: '#fff' }}>{banner.title}</h2>
</div>
{banner.desc && <p className="body-md" style={{ color: 'rgba(255,255,255,0.9)' }}>{banner.desc}</p>}
</div>
</section>
<section className="partner-timeline">
<div className="partner-timeline-line" />
{TIMELINE.map((item, i) => {
const done = i < currentIdx;
const current = i === currentIdx;
const pending = i > currentIdx;
return (
<div key={item.key} className="partner-timeline-item">
<div className={`partner-timeline-dot${done ? ' partner-timeline-dot--done' : ''}${current ? ' partner-timeline-dot--current' : ''}${pending ? ' partner-timeline-dot--pending' : ''}`}>
{done && <span className="material-symbols-outlined">check</span>}
</div>
<div>
<p className={current ? 'headline-md text-primary' : 'label-md'} style={{ fontWeight: current ? 700 : 500 }}>{item.label}</p>
{item.desc && current && <p className="text-muted body-md" style={{ marginTop: 4 }}>{item.desc}</p>}
</div>
</div>
);
})}
</section>
<section className="partner-detail-section partner-detail-section--accent">
<div style={{ display: 'flex', gap: 16 }}>
<div className="partner-order-product-img" style={{ width: 96, height: 96, position: 'relative' }}>
<div style={{ width: '100%', height: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center', background: 'var(--color-surface-container)' }}>
<span className="material-symbols-outlined text-muted" style={{ fontSize: 32 }}>liquor</span>
</div>
<div style={{ position: 'absolute', bottom: 0, left: 0, right: 0, background: 'rgba(166,29,36,0.8)', textAlign: 'center', padding: '2px 0' }}>
<span className="label-md" style={{ color: '#fff', fontSize: 10 }}></span>
</div>
</div>
<div style={{ flex: 1 }}>
<h3 className="headline-md">{String(order.productName || '杜康好酒')}</h3>
<p className="text-muted body-md" style={{ marginTop: 4 }}>x{Number(order.quantity || 1)}</p>
<div style={{ marginTop: 8, display: 'flex', alignItems: 'baseline', gap: 2 }}>
<span className="label-md text-primary">¥</span>
<span className="amount-lg">{payAmount.toFixed(2)}</span>
</div>
</div>
</div>
</section>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 16, margin: '0 20px' }}>
<div className="partner-detail-section" style={{ margin: 0, borderTop: '2px solid var(--color-heritage-red)' }}>
<p className="label-md text-muted" style={{ marginBottom: 8 }}></p>
<div className="partner-info-row">
<span className="label-md text-muted"></span>
<span className="text-primary" style={{ fontWeight: 700, fontSize: 12 }}>¥{(payAmount * 0.04).toFixed(2)}</span>
</div>
<div className="partner-info-row">
<span className="label-md text-muted"></span>
<span className="text-primary" style={{ fontWeight: 700, fontSize: 12 }}>¥{(payAmount * 0.04).toFixed(2)}</span>
</div>
</div>
<div className="partner-detail-section" style={{ margin: 0, borderTop: '2px solid var(--color-aged-amber)' }}>
<p className="label-md text-muted" style={{ marginBottom: 4 }}></p>
<span className="amount-lg" style={{ color: 'var(--color-aged-amber)' }}>¥{Math.round(payAmount * 0.2)}</span>
</div>
</div>
<section className="partner-detail-section">
<h4 className="headline-md" style={{ marginBottom: 16, display: 'flex', alignItems: 'center', gap: 8 }}>
<span style={{ width: 4, height: 16, background: 'var(--color-heritage-red)', borderRadius: 2 }} />
</h4>
<div className="partner-info-row">
<span className="text-muted body-md"></span>
<span className="body-md">{String(order.orderNo)}</span>
</div>
<div className="partner-info-row">
<span className="text-muted body-md"></span>
<span className="body-md">{String(order.status)}</span>
</div>
</section>
<section className="partner-detail-section">
<h4 className="headline-md" style={{ marginBottom: 16, display: 'flex', alignItems: 'center', gap: 8 }}>
<span style={{ width: 4, height: 16, background: 'var(--color-heritage-red)', borderRadius: 2 }} />
</h4>
<div style={{ display: 'flex', gap: 12 }}>
<span className="material-symbols-outlined text-muted">location_on</span>
<div>
<p className="headline-md" style={{ fontSize: 16 }}>
{String(order.receiverName)} <span className="body-md text-muted" style={{ fontWeight: 400 }}>{String(order.receiverPhone)}</span>
</p>
<p className="text-muted body-md" style={{ marginTop: 4, lineHeight: 1.5 }}>{String(order.receiverAddress)}</p>
</div>
</div>
</section>
<footer className="partner-order-footer">
<div>
<span className="label-md text-muted"></span>
<p className="headline-md text-primary" style={{ fontWeight: 700 }}>¥{(payAmount * 0.08).toFixed(2)}</p>
</div>
<button type="button" className="btn btn-outline" style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '8px 24px' }}>
<span className="material-symbols-outlined" style={{ fontSize: 18 }}>support_agent</span>
</button>
</footer>
{import.meta.env.DEV && (
<details className="partner-dev-tools">
<summary>Dev: Mock </summary>
{['OUT_WAREHOUSE', 'SHIPPING', 'PENDING_RECEIVE', 'COMPLETED'].map((s) => (
<button key={s} type="button" onClick={() => advance(s)}>{s}</button>
))}
</details>
)}
</div>
);
}
+139
View File
@@ -0,0 +1,139 @@
import { useEffect, useMemo, useState } from 'react';
import { Link, useNavigate } from 'react-router-dom';
import PageHeader from '@dukang/shared-ui/PageHeader';
import { isLoggedIn, request } from '../lib/api';
type DateFilter = 'today' | '7d' | '30d';
type StatusFilter = 'ALL' | 'PENDING_SHIP' | 'SHIPPING' | 'COMPLETED' | 'ABNORMAL';
function orderStatusLabel(status: string) {
const s = String(status).toUpperCase();
if (s.includes('SHIP') || s === 'OUT_WAREHOUSE') return { label: '运输中', color: 'var(--color-status-blue)' };
if (s === 'COMPLETED') return { label: '已完成', color: 'var(--color-success-green)' };
if (s.includes('PENDING') || s === 'PAID') return { label: '待发货', color: 'var(--color-secondary)' };
if (s.includes('ERROR') || s.includes('ABNORMAL')) return { label: '异常', color: 'var(--color-error)' };
return { label: status, color: 'var(--color-subtle-gray)' };
}
const DATE_FILTERS: { key: DateFilter; label: string }[] = [
{ key: 'today', label: '今日' },
{ key: '7d', label: '近7天' },
{ key: '30d', label: '近30天' },
];
const STATUS_FILTERS: { key: StatusFilter; label: string }[] = [
{ key: 'ALL', label: '全部' },
{ key: 'PENDING_SHIP', label: '待发货' },
{ key: 'SHIPPING', label: '运输中' },
{ key: 'COMPLETED', label: '已完成' },
{ key: 'ABNORMAL', label: '异常' },
];
export default function OrderListPage() {
const navigate = useNavigate();
const [data, setData] = useState<{ list: Array<Record<string, unknown>> }>({ list: [] });
const [tab, setTab] = useState<'orders' | 'coupons'>('orders');
const [dateFilter, setDateFilter] = useState<DateFilter>('today');
const [statusFilter, setStatusFilter] = useState<StatusFilter>('ALL');
useEffect(() => {
if (!isLoggedIn()) { navigate('/login'); return; }
request('PARTNER_H5', '/partner/orders').then(setData);
}, [navigate]);
const filtered = useMemo(() => data.list.filter((o) => {
if (statusFilter === 'ALL') return true;
const s = String(o.status).toUpperCase();
if (statusFilter === 'PENDING_SHIP') return s.includes('PENDING') || s === 'PAID';
if (statusFilter === 'SHIPPING') return s.includes('SHIP') || s === 'OUT_WAREHOUSE';
if (statusFilter === 'COMPLETED') return s === 'COMPLETED';
if (statusFilter === 'ABNORMAL') return s.includes('ERROR') || s.includes('ABNORMAL');
return true;
}), [data.list, statusFilter]);
return (
<div className="page-no-tab partner-orders-page">
<PageHeader title="订单中心" onBack={() => navigate('/')} />
<div className="partner-segment">
<button type="button" className={tab === 'orders' ? 'active' : ''} onClick={() => setTab('orders')}></button>
<button type="button" className={tab === 'coupons' ? 'active' : ''} onClick={() => setTab('coupons')}></button>
</div>
{tab === 'orders' && (
<>
<div className="partner-filter-row" style={{ marginBottom: 8 }}>
{DATE_FILTERS.map((f) => (
<button key={f.key} type="button" className={`partner-chip${dateFilter === f.key ? ' active' : ''}`} style={{ border: dateFilter === f.key ? 'none' : '1px solid var(--color-outline-variant)', background: dateFilter === f.key ? undefined : '#fff' }} onClick={() => setDateFilter(f.key)}>
{f.label}
</button>
))}
</div>
<div className="partner-filter-row" style={{ borderBottom: '1px solid var(--color-surface-container-high)', paddingBottom: 8, marginBottom: 16 }}>
{STATUS_FILTERS.map((f) => (
<button key={f.key} type="button" className={`partner-filter-tab${statusFilter === f.key ? ' active' : ''}`} onClick={() => setStatusFilter(f.key)}>
{f.label}
</button>
))}
</div>
{filtered.length === 0 && <div className="empty"></div>}
{filtered.map((o) => {
const st = orderStatusLabel(String(o.status));
const payAmount = Number(o.payAmount || 0);
return (
<Link key={String(o.id)} to={`/orders/${o.id}`} className="partner-order-card">
<div className="partner-order-card-top">
<span className="label-md text-muted">NO. {String(o.orderNo)}</span>
<span className="label-md" style={{ color: st.color, fontWeight: 600 }}>{st.label}</span>
</div>
<div className="partner-order-product">
<div className="partner-order-product-img" style={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<span className="material-symbols-outlined text-muted">liquor</span>
</div>
<div style={{ flex: 1, minWidth: 0 }}>
<h3 className="headline-md line-2-clamp">{String(o.productName || '杜康好酒')}</h3>
<p className="text-variant body-md" style={{ marginTop: 4 }}>¥{payAmount.toFixed(2)}</p>
<span className="partner-benefit-tag">
<span className="material-symbols-outlined" style={{ fontSize: 14 }}>confirmation_number</span>
¥{Math.round(payAmount * 0.2)}
</span>
</div>
</div>
<div className="partner-order-address">
<span className="material-symbols-outlined" style={{ fontSize: 18 }}>location_on</span>
<p className="line-2-clamp">{String(o.receiverAddress || '收货地址')}</p>
</div>
<div className="partner-order-commission">
<div>
<p className="label-md text-muted"></p>
<p className="headline-md text-primary" style={{ marginTop: 4 }}>¥{(payAmount * 0.04).toFixed(2)}</p>
</div>
<div>
<p className="label-md text-muted"></p>
<p className="headline-md text-primary" style={{ marginTop: 4 }}>¥0.00</p>
</div>
<div>
<p className="label-md text-muted"></p>
<p className="headline-md" style={{ marginTop: 4, color: 'var(--color-aged-amber)' }}>¥{Math.round(payAmount * 0.2)}</p>
</div>
</div>
</Link>
);
})}
</>
)}
{tab === 'coupons' && (
<div style={{ padding: '0 20px' }}>
<div className="partner-revenue-card" style={{ marginBottom: 16 }}>
<p className="partner-revenue-label"></p>
<div className="partner-revenue-amount" style={{ fontSize: 28 }}>42,800.00</div>
</div>
<p className="text-muted body-md text-center"> preV1 </p>
</div>
)}
</div>
);
}
@@ -0,0 +1,230 @@
import { useState } from 'react';
import { useNavigate, useSearchParams } from 'react-router-dom';
import PageHeader from '@dukang/shared-ui/PageHeader';
import { request } from '../lib/api';
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: '',
});
function goStep(n: number) {
setParams({ step: String(n) });
}
async function submit() {
await request('PARTNER_H5', '/partner/stores', {
method: 'POST',
body: JSON.stringify(form),
});
navigate('/stores');
}
const progress = step === 1 ? 0 : step === 2 ? 50 : 100;
return (
<div className="partner-page-sticky">
<PageHeader title="录入新门店" onBack={() => navigate('/stores')} />
<nav className="partner-stepper">
<div className="partner-stepper-inner">
<div className="partner-stepper-line" aria-hidden>
<div className="partner-stepper-line-fill" style={{ width: `${progress}%` }} />
</div>
{STEPS.map((label, i) => {
const n = i + 1;
const done = step > n;
const active = step === n;
return (
<div key={label} className="partner-step">
<div className={`partner-step-circle${done ? ' partner-step-circle--done' : ''}${active ? ' partner-step-circle--active' : ''}`}>
{done ? <span className="material-symbols-outlined" style={{ fontSize: 16, fontVariationSettings: "'FILL' 1" }}>check</span> : n}
</div>
<span className={`partner-step-label${active || done ? ' partner-step-label--active' : ''}`}>{label}</span>
</div>
);
})}
</div>
</nav>
{step === 1 && (
<>
<section className="partner-form-card">
<div className="partner-section-title">
<div className="partner-section-bar" />
<h2 className="headline-md"></h2>
</div>
<div className="partner-field">
<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 })} />
</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 })} />
</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 })} />
</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 })} />
</div>
<div className="partner-field">
<label></label>
<textarea rows={4} placeholder="请输入门店简介 (10-500字)" value={form.intro} onChange={(e) => setForm({ ...form, intro: e.target.value })} />
<div style={{ textAlign: 'right', marginTop: 4 }}>
<span className="label-md text-muted">{form.intro.length} / 500</span>
</div>
</div>
</section>
<div className="partner-info-banner">
<div className="partner-bills-icon">
<span className="material-symbols-outlined">verified_user</span>
</div>
<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>
</>
)}
{step === 2 && (
<>
<div className="partner-section-title" style={{ padding: '0 20px', marginBottom: 16 }}>
<div className="partner-bills-icon" style={{ background: '#ffdad7', borderRadius: 8, width: 40, height: 40 }}>
<span className="material-symbols-outlined text-primary">photo_library</span>
</div>
<h2 className="headline-md"></h2>
</div>
<section className="partner-form-card">
<h3 className="headline-md"> *</h3>
<p className="label-md text-muted" style={{ margin: '4px 0 12px' }}>1</p>
<button type="button" className="partner-upload-dashed partner-upload-dashed--wide">
<span className="material-symbols-outlined text-primary" style={{ fontSize: 36 }}>add_a_photo</span>
<span className="text-primary" style={{ fontWeight: 500 }}></span>
</button>
</section>
<section className="partner-form-card">
<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>
</div>
</section>
<section className="partner-form-card">
<h3 className="headline-md"> *</h3>
<p className="label-md text-muted" style={{ margin: '4px 0 12px' }}></p>
<button type="button" className="partner-upload-dashed" style={{ flexDirection: 'row', height: 96 }}>
<div className="partner-bills-icon" style={{ background: '#ffb3ae' }}>
<span className="material-symbols-outlined text-primary">description</span>
</div>
<div style={{ textAlign: 'left' }}>
<span className="text-primary" style={{ fontWeight: 700, display: 'block' }}></span>
<span className="label-md text-muted"> JPG, PNG, PDF</span>
</div>
</button>
</section>
<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
</p>
</div>
</>
)}
{step === 3 && (
<>
<div className="partner-section-title" style={{ padding: '0 20px', marginBottom: 16 }}>
<span className="material-symbols-outlined text-primary">account_balance</span>
<h2 className="headline-md"></h2>
</div>
<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 })} />
</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 })} />
</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 })} />
</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>
</>
)}
<footer className="partner-sticky-footer">
{step > 1 && (
<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)}>
<span></span>
<span className="material-symbols-outlined" style={{ fontSize: 20 }}>navigate_next</span>
</button>
) : (
<button type="button" className="partner-btn-primary" onClick={submit}></button>
)}
</footer>
</div>
);
}
@@ -0,0 +1,133 @@
import { useEffect, useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import PageHeader from '@dukang/shared-ui/PageHeader';
import AppImage from '@dukang/shared-ui/AppImage';
import { request } from '../lib/api';
export default function StoreDetailPage() {
const { id } = useParams();
const navigate = useNavigate();
const [store, setStore] = useState<Record<string, unknown> | null>(null);
const [form, setForm] = useState({ name: '', phone: '', address: '', intro: '' });
const [status, setStatus] = useState('OPEN');
const [toast, setToast] = useState('');
useEffect(() => {
if (id) {
request('PARTNER_H5', `/stores/${id}`).then((data) => {
setStore(data);
setForm({
name: String(data.name || ''),
phone: String(data.phone || ''),
address: String(data.address || ''),
intro: String(data.intro || ''),
});
setStatus(String(data.status || 'OPEN').toUpperCase());
});
}
}, [id]);
function save() {
setToast('preV1:保存成功(演示)');
setTimeout(() => setToast(''), 2000);
}
if (!store) return <div className="empty">...</div>;
const statusLabel = status === 'OPEN' ? '营业中' : status === 'PAUSED' ? '暂停服务' : '已下线';
return (
<div className="partner-detail-page">
<PageHeader title="门店详情" onBack={() => navigate('/stores')} />
<main style={{ padding: '16px 20px' }}>
<section className="partner-form-card" style={{ margin: '0 0 16px' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 }}>
<h3 className="label-md text-muted" style={{ textTransform: 'uppercase', letterSpacing: '0.1em' }}></h3>
<span className="partner-status-pill partner-status-pill--open">
<span className="partner-dot partner-dot--green" style={{ width: 8, height: 8, display: 'inline-block', marginRight: 4 }} />
{statusLabel}
</span>
</div>
<div className="partner-status-toggle">
{(['OPEN', 'PAUSED', 'CLOSED'] as const).map((s) => (
<button key={s} type="button" className={status === s ? 'active' : ''} onClick={() => setStatus(s)}>
{s === 'OPEN' ? '营业中' : s === 'PAUSED' ? '暂停服务' : '已下线'}
</button>
))}
</div>
</section>
<section className="partner-form-card" style={{ margin: '0 0 16px' }}>
<h3 className="headline-md" style={{ borderLeft: '4px solid var(--color-heritage-red)', paddingLeft: 12, marginBottom: 16 }}></h3>
<div className="partner-cover">
<AppImage
src={store.coverUrl ? String(store.coverUrl) : null}
alt={form.name}
wrapperClassName="app-image--fill"
/>
</div>
<div className="partner-field">
<label></label>
<input value={form.name} onChange={(e) => setForm({ ...form, name: e.target.value })} style={{ width: '100%', padding: '12px 16px', border: '1px solid rgba(226,190,188,0.5)', borderRadius: 8, fontSize: 16, fontWeight: 500 }} />
</div>
<div className="partner-field">
<label></label>
<div className="partner-field-input">
<span className="material-symbols-outlined">call</span>
<input type="tel" value={form.phone} onChange={(e) => setForm({ ...form, phone: e.target.value })} />
</div>
</div>
<div className="partner-field">
<label></label>
<textarea rows={2} value={form.address} onChange={(e) => setForm({ ...form, 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 })} />
<div style={{ textAlign: 'right', marginTop: 4 }}>
<span className="label-md text-muted">{form.intro.length} / 500</span>
</div>
</div>
</section>
<section className="partner-form-card" style={{ margin: '0 0 16px' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-end', marginBottom: 16 }}>
<h3 className="headline-md" style={{ borderLeft: '4px solid var(--color-heritage-red)', paddingLeft: 12 }}></h3>
<span className="label-md text-muted">12</span>
</div>
<div className="partner-photo-grid">
<button type="button" className="partner-photo-add">
<span className="material-symbols-outlined">add</span>
<span className="label-md"></span>
</button>
</div>
</section>
<section className="partner-form-card" style={{ margin: 0, background: 'var(--color-surface-container)' }}>
<h3 className="headline-md" style={{ borderLeft: '4px solid var(--color-subtle-gray)', paddingLeft: 12, marginBottom: 16 }}></h3>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 16 }}>
<div>
<label className="label-md text-muted"></label>
<p className="body-md" style={{ fontWeight: 500 }}>{String(store.status)}</p>
</div>
<div>
<label className="label-md text-muted">ID</label>
<p className="body-md" style={{ fontWeight: 500, fontFamily: 'monospace' }}>{String(store.id)}</p>
</div>
</div>
</section>
</main>
<footer className="partner-save-footer">
<button type="button" className="partner-save-cancel" onClick={() => navigate('/stores')}></button>
<button type="button" className="partner-save-submit" onClick={save}>
<span className="material-symbols-outlined">save</span>
</button>
</footer>
{toast && <div className="partner-toast">{toast}</div>}
</div>
);
}
@@ -0,0 +1,98 @@
import { useEffect, useMemo, useState } from 'react';
import { Link, useNavigate } from 'react-router-dom';
import { isLoggedIn, request } from '../lib/api';
type StatusFilter = 'ALL' | 'OPEN' | 'PAUSED' | 'CLOSED';
function statusPill(status: string) {
const s = String(status).toUpperCase();
if (s === 'OPEN') return { cls: 'partner-status-pill--open', label: '营业中' };
if (s === 'PAUSED') return { cls: 'partner-status-pill--paused', label: '暂时闭店' };
return { cls: 'partner-status-pill--closed', label: '已关闭' };
}
const FILTERS: { key: StatusFilter; label: string }[] = [
{ key: 'ALL', label: '全部' },
{ key: 'OPEN', label: '营业中' },
{ key: 'PAUSED', label: '暂时闭店' },
{ key: 'CLOSED', label: '已关闭' },
];
export default function StoreListPage() {
const navigate = useNavigate();
const [stores, setStores] = useState<Array<Record<string, unknown>>>([]);
const [q, setQ] = useState('');
const [filter, setFilter] = useState<StatusFilter>('ALL');
useEffect(() => {
if (!isLoggedIn()) { navigate('/login'); return; }
request<Array<Record<string, unknown>>>('PARTNER_H5', '/partner/stores').then(setStores);
}, [navigate]);
const filtered = useMemo(() => stores.filter((s) => {
const matchQ = !q || String(s.name).includes(q) || String(s.address).includes(q);
const matchStatus = filter === 'ALL' || String(s.status).toUpperCase() === filter;
return matchQ && matchStatus;
}), [stores, q, filter]);
return (
<div className="page partner-store-page">
<header className="header app-page-header">
<h1 className="app-page-title"></h1>
</header>
<div className="partner-page-title-block">
<h2></h2>
<p className="text-muted body-md"></p>
</div>
<div className="partner-sticky-filter">
<div className="partner-search">
<span className="material-symbols-outlined">search</span>
<input placeholder="搜索门店名称/地址" value={q} onChange={(e) => setQ(e.target.value)} />
</div>
<div className="partner-chips">
{FILTERS.map((f) => (
<button key={f.key} type="button" className={`partner-chip${filter === f.key ? ' active' : ''}`} onClick={() => setFilter(f.key)}>
{f.label}
</button>
))}
</div>
</div>
<Link to="/stores/new" className="partner-fab-link">
<button type="button" className="partner-btn-primary">
<span className="material-symbols-outlined">add_business</span>
</button>
</Link>
{filtered.length === 0 && <div className="empty"></div>}
{filtered.map((s) => {
const pill = statusPill(String(s.status));
const dim = String(s.status).toUpperCase() === 'CLOSED';
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={{ marginTop: 4 }}>{String(s.address || s.district || '')}</p>
</div>
<span className={`partner-status-pill ${pill.cls}`}>{pill.label}</span>
</div>
</Link>
<div className="partner-store-card-actions">
<button type="button" className="btn btn-outline" style={{ fontSize: 12, padding: '8px 12px' }}></button>
<button type="button" className="btn btn-outline" style={{ fontSize: 12, padding: '8px 12px', borderColor: 'var(--color-subtle-gray)', color: 'var(--color-subtle-gray)' }}></button>
<Link to={`/stores/${s.id}`} className="partner-menu-icon" style={{ width: 40, height: 40, borderRadius: 8, textDecoration: 'none' }}>
<span className="material-symbols-outlined" style={{ fontSize: 20 }}>edit</span>
</Link>
</div>
</div>
);
})}
</div>
);
}
File diff suppressed because it is too large Load Diff