feat: 技术支持工单/企微权限/开发版本管理/消息推送等迭代
This commit is contained in:
@@ -1,371 +0,0 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import type { PartnerLeaderboardEntry } from '@dukang/shared-types';
|
||||
import PullToRefresh from '@dukang/shared-ui/PullToRefresh';
|
||||
import { usePartnerPageView } from '../lib/usePageView';
|
||||
import { isLoggedIn, request } from '../lib/api';
|
||||
import { fetchPartnerLeaderboard } from '../lib/leaderboard';
|
||||
import {
|
||||
canAccessPartnerDashboard,
|
||||
canAccessPartnerOrders,
|
||||
canAccessPartnerStores,
|
||||
getPartnerNavKind,
|
||||
hasWarehouseAccess,
|
||||
} from '../lib/partnerAccess';
|
||||
import { usePartnerSession } from '../contexts/PartnerSessionContext';
|
||||
|
||||
function fmtMoney(n: number) {
|
||||
return n.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
||||
}
|
||||
|
||||
const LEADERBOARD_PREVIEW_LIMIT = 3;
|
||||
|
||||
function OrderSummarySection({
|
||||
title,
|
||||
todayCount,
|
||||
pendingShip,
|
||||
shipping,
|
||||
completed,
|
||||
ordersLink = '/orders',
|
||||
}: {
|
||||
title?: string;
|
||||
todayCount: number;
|
||||
pendingShip: number;
|
||||
shipping: number;
|
||||
completed: number;
|
||||
ordersLink?: string;
|
||||
}) {
|
||||
return (
|
||||
<section>
|
||||
<div className="partner-order-summary-header">
|
||||
<h2 className="headline-md">{title || `今日订单 ${todayCount}`}</h2>
|
||||
<Link to={ordersLink} 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>
|
||||
);
|
||||
}
|
||||
|
||||
function isPendingShip(status: string) {
|
||||
const s = status.toUpperCase();
|
||||
return s.includes('PENDING') || s === 'PAID' || s === 'PENDING_SHIP';
|
||||
}
|
||||
|
||||
function isShipping(status: string) {
|
||||
const s = status.toUpperCase();
|
||||
return s.includes('SHIP') || s === 'OUT_WAREHOUSE' || s === 'PENDING_RECEIVE';
|
||||
}
|
||||
|
||||
function isCompleted(status: string) {
|
||||
return status.toUpperCase() === 'COMPLETED';
|
||||
}
|
||||
|
||||
function isToday(dateStr: string) {
|
||||
const d = new Date(dateStr);
|
||||
const now = new Date();
|
||||
return d.getFullYear() === now.getFullYear()
|
||||
&& d.getMonth() === now.getMonth()
|
||||
&& d.getDate() === now.getDate();
|
||||
}
|
||||
|
||||
function summarizeOrders(orders: Array<Record<string, unknown>>) {
|
||||
let pendingShip = 0;
|
||||
let shipping = 0;
|
||||
let completed = 0;
|
||||
let todayCount = 0;
|
||||
for (const o of orders) {
|
||||
const status = String(o.status || '');
|
||||
if (isToday(String(o.createdAt || ''))) todayCount += 1;
|
||||
if (isPendingShip(status)) pendingShip += 1;
|
||||
else if (isShipping(status)) shipping += 1;
|
||||
else if (isCompleted(status)) completed += 1;
|
||||
}
|
||||
return { pendingShip, shipping, completed, todayCount };
|
||||
}
|
||||
|
||||
function LeaderboardPreview({ entries }: { entries: PartnerLeaderboardEntry[] }) {
|
||||
return (
|
||||
<section className="partner-bento-card partner-leaderboard-preview">
|
||||
<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: 20 }}>emoji_events</span>
|
||||
团队贡献榜
|
||||
</h3>
|
||||
<Link to="/leaderboard" 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>
|
||||
{entries.length === 0 ? (
|
||||
<p className="label-md text-muted">暂无排行数据</p>
|
||||
) : (
|
||||
entries.map((entry) => (
|
||||
<div key={entry.accountId} className="partner-leaderboard-row partner-leaderboard-row--compact">
|
||||
<div className={`partner-leaderboard-avatar partner-leaderboard-avatar--rank-${Math.min(entry.rank, 3)}`}>
|
||||
<span className="material-symbols-outlined">person</span>
|
||||
<span className="partner-leaderboard-avatar-badge">{entry.rank}</span>
|
||||
</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.roleLabel})</span>
|
||||
</p>
|
||||
<p className="label-md text-muted">累计拓店: {entry.totalStores} 间</p>
|
||||
</div>
|
||||
<div className="partner-leaderboard-stat">
|
||||
<p className="headline-md text-primary">{entry.periodStores} 间</p>
|
||||
<p className="label-md text-muted">本月新增</p>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
export default function HomePage() {
|
||||
usePartnerPageView('partner_home_view');
|
||||
const navigate = useNavigate();
|
||||
const { account } = usePartnerSession();
|
||||
const navKind = getPartnerNavKind(account);
|
||||
const isPrimary = navKind === 'primary';
|
||||
const isWarehouse = navKind === 'warehouse_staff';
|
||||
const warehouseOk = hasWarehouseAccess(account);
|
||||
const canOrders = canAccessPartnerOrders(account) && warehouseOk && (isPrimary || isWarehouse);
|
||||
const canDashboard = canAccessPartnerDashboard(account) && !isWarehouse;
|
||||
const canStores = canAccessPartnerStores(account) && !isWarehouse;
|
||||
|
||||
const [dash, setDash] = useState<Record<string, unknown> | null>(null);
|
||||
const [stores, setStores] = useState<Array<Record<string, unknown>>>([]);
|
||||
const [orders, setOrders] = useState<Array<Record<string, unknown>>>([]);
|
||||
const [leaderboardEntries, setLeaderboardEntries] = useState<PartnerLeaderboardEntry[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
document.title = '工作台';
|
||||
}, []);
|
||||
|
||||
const loadHome = useCallback(() => {
|
||||
if (!isLoggedIn()) {
|
||||
navigate('/login');
|
||||
return Promise.resolve();
|
||||
}
|
||||
if (!account) return Promise.resolve();
|
||||
|
||||
const tasks: Promise<unknown>[] = [];
|
||||
|
||||
if (canOrders) {
|
||||
tasks.push(
|
||||
request<{ list: Array<Record<string, unknown>> }>('PARTNER_H5', '/partner/orders', { silent: true })
|
||||
.then((data) => setOrders(Array.isArray(data.list) ? data.list : []))
|
||||
.catch(() => setOrders([])),
|
||||
);
|
||||
} else {
|
||||
setOrders([]);
|
||||
}
|
||||
|
||||
if (canDashboard) {
|
||||
tasks.push(
|
||||
request<Record<string, unknown>>('PARTNER_H5', '/partner/dashboard', { silent: true })
|
||||
.then(setDash)
|
||||
.catch(() => setDash(null)),
|
||||
);
|
||||
} else {
|
||||
setDash(null);
|
||||
}
|
||||
|
||||
if (canStores) {
|
||||
tasks.push(
|
||||
request<Array<Record<string, unknown>>>('PARTNER_H5', '/partner/stores', { silent: true })
|
||||
.then((data) => setStores(Array.isArray(data) ? data : []))
|
||||
.catch(() => setStores([])),
|
||||
);
|
||||
} else {
|
||||
setStores([]);
|
||||
}
|
||||
|
||||
tasks.push(
|
||||
fetchPartnerLeaderboard('month')
|
||||
.then((data) => {
|
||||
setLeaderboardEntries(data.list.slice(0, LEADERBOARD_PREVIEW_LIMIT));
|
||||
})
|
||||
.catch(() => {
|
||||
setLeaderboardEntries([]);
|
||||
}),
|
||||
);
|
||||
|
||||
return Promise.all(tasks).then(() => undefined);
|
||||
}, [navigate, account, canOrders, canDashboard, canStores]);
|
||||
|
||||
useEffect(() => {
|
||||
void loadHome();
|
||||
}, [loadHome]);
|
||||
|
||||
const storeCount = Number(dash?.storeCount || stores.length || 0);
|
||||
const orderStats = useMemo(() => summarizeOrders(orders), [orders]);
|
||||
const pendingAuditCount = Number(dash?.pendingAuditCount || 0);
|
||||
const activeStores = stores.filter((s) => String(s.status).toUpperCase() === 'OPEN').length;
|
||||
const abnormalStores = Math.max(0, storeCount - activeStores);
|
||||
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 lastMonthNew = stores.filter((s) => {
|
||||
const created = new Date(String(s.createdAt || ''));
|
||||
const now = new Date();
|
||||
const last = new Date(now.getFullYear(), now.getMonth() - 1, 1);
|
||||
return created.getMonth() === last.getMonth() && created.getFullYear() === last.getFullYear();
|
||||
}).length;
|
||||
const monthGrowthPct = lastMonthNew > 0
|
||||
? Math.round(((monthNew - lastMonthNew) / lastMonthNew) * 100)
|
||||
: (monthNew > 0 ? 100 : 0);
|
||||
|
||||
const orderCount = Number(dash?.orderCount || orderStats.todayCount || 0);
|
||||
const revenue = orderCount * 128.45;
|
||||
const profit = revenue * 0.25;
|
||||
|
||||
return (
|
||||
<PullToRefresh onRefresh={loadHome} className="page partner-home partner-home--flush-top">
|
||||
<main className="partner-home-body">
|
||||
{isPrimary && (
|
||||
<>
|
||||
<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>
|
||||
<section className="partner-bento-card" style={{ marginBottom: 16 }}>
|
||||
<div className="partner-quick-actions">
|
||||
<Link to="/proxy-order" className="partner-quick-action">
|
||||
<div className="partner-quick-action-icon partner-quick-action-icon--red">
|
||||
<span className="material-symbols-outlined">shopping_cart_checkout</span>
|
||||
</div>
|
||||
<span className="partner-quick-action-label">代下单</span>
|
||||
</Link>
|
||||
<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="/center/settlement" 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="/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>
|
||||
<span className="partner-quick-action-label">数据周报</span>
|
||||
</Link>
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
)}
|
||||
|
||||
{isWarehouse && (
|
||||
<p className="headline-md" style={{ marginBottom: 8 }}>订单管理</p>
|
||||
)}
|
||||
|
||||
{canOrders && (
|
||||
<OrderSummarySection
|
||||
title={isWarehouse ? '今日订单' : undefined}
|
||||
todayCount={orderStats.todayCount}
|
||||
pendingShip={orderStats.pendingShip}
|
||||
shipping={orderStats.shipping}
|
||||
completed={orderStats.completed}
|
||||
/>
|
||||
)}
|
||||
|
||||
{(isPrimary || isWarehouse) && !warehouseOk && (
|
||||
<section className="partner-bento-card" style={{ marginBottom: 16 }}>
|
||||
<div className="partner-warehouse-denied">
|
||||
<span className="material-symbols-outlined text-primary">warehouse</span>
|
||||
<p className="body-md" style={{ fontWeight: 600, marginTop: 8 }}>未配置仓库管理权限</p>
|
||||
<p className="label-md text-muted" style={{ marginTop: 4, lineHeight: 1.5 }}>
|
||||
购酒订单由总部履约,不会推送到本账号。如需管仓发货,请联系总部配置仓库。
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{canStores && (
|
||||
<>
|
||||
<section className="partner-bento-card" style={{ marginBottom: 16 }}>
|
||||
<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-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 className="partner-expansion-value-row">
|
||||
<span className="headline-lg" style={{ fontSize: 24 }}>{monthNew}</span>
|
||||
{monthGrowthPct !== 0 && (
|
||||
<span className={`partner-expansion-trend${monthGrowthPct > 0 ? ' partner-expansion-trend--up' : ' partner-expansion-trend--down'}`}>
|
||||
<span className="material-symbols-outlined" style={{ fontSize: 14 }}>
|
||||
{monthGrowthPct > 0 ? 'trending_up' : 'trending_down'}
|
||||
</span>
|
||||
{Math.abs(monthGrowthPct)}%
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p className="label-md text-muted" style={{ marginBottom: 4 }}>待审核门店</p>
|
||||
<span className="headline-lg" style={{ fontSize: 24 }}>{pendingAuditCount}</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
)}
|
||||
|
||||
<LeaderboardPreview entries={leaderboardEntries} />
|
||||
</main>
|
||||
</PullToRefresh>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user