Files
dukang/apps/h5-partner/src/pages/WeeklyReportPage.tsx
T
jacy ae7c63c08d
CI / verify (pull_request) Has been cancelled
feat(h5): add pull-to-refresh on user partner and shop pages
Share PullToRefresh for partner/shop H5; enable Taro pull-down on mini-user lists.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-22 11:19:42 +08:00

222 lines
9.0 KiB
TypeScript

import { useCallback, useEffect, useMemo, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import type { PartnerWeeklyReportResponse } from '@dukang/shared-types';
import PageHeader from '@dukang/shared-ui/PageHeader';
import PullToRefresh from '@dukang/shared-ui/PullToRefresh';
import { fetchPartnerWeeklyReport } from '../lib/weeklyReport';
function fmtMoney(n: number) {
return n.toLocaleString('zh-CN', { minimumFractionDigits: 0, maximumFractionDigits: 0 });
}
function fmtGrowth(n: number) {
const sign = n > 0 ? '+' : '';
return `${sign}${n}%`;
}
export default function WeeklyReportPage() {
const navigate = useNavigate();
const [selectedStart, setSelectedStart] = useState<string | undefined>();
const [data, setData] = useState<PartnerWeeklyReportResponse | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
const loadWeeklyReport = useCallback(() => {
setLoading(true);
setError('');
return fetchPartnerWeeklyReport(selectedStart)
.then(setData)
.catch((e) => {
setData(null);
setError(e instanceof Error ? e.message : '加载失败');
})
.finally(() => setLoading(false));
}, [selectedStart]);
useEffect(() => {
void loadWeeklyReport();
}, [loadWeeklyReport]);
const maxDailyGmv = useMemo(() => {
if (!data?.dailyGmv.length) return 1;
return Math.max(1, ...data.dailyGmv.map((item) => item.amount));
}, [data]);
const topStores = data?.storeRanking.slice(0, 3) ?? [];
const summary = data?.summary;
const growthPositive = (summary?.gmvGrowthPercent ?? 0) >= 0;
return (
<PullToRefresh onRefresh={loadWeeklyReport} className="page-no-tab">
<PageHeader title="数据周报" onBack={() => navigate('/')} />
<main className="partner-weekly-page">
<section className="partner-weekly-title-section">
<h2>经营数据周报</h2>
{data && (
<div className="partner-weekly-tabs">
{data.availablePeriods.map((period) => (
<button
key={period.startDate}
type="button"
className={`partner-weekly-tab${
period.startDate === data.period.startDate ? ' active' : ''
}`}
onClick={() => setSelectedStart(period.startDate)}
>
{period.label}
</button>
))}
</div>
)}
</section>
{loading && <p className="label-md text-muted partner-weekly-state">加载中…</p>}
{!loading && error && (
<p className="label-md text-error partner-weekly-state">{error}</p>
)}
{!loading && !error && data && summary && (
<>
<section className="partner-weekly-hero">
<div className="partner-weekly-hero-top">
<div>
<p className="partner-weekly-hero-label">累计成交额 (GMV)</p>
<p className="partner-weekly-hero-gmv">¥{fmtMoney(summary.gmv)}</p>
</div>
<div
className={`partner-weekly-growth-badge${
growthPositive ? ' partner-weekly-growth-badge--up' : ' partner-weekly-growth-badge--down'
}`}
>
<span className="material-symbols-outlined">
{growthPositive ? 'trending_up' : 'trending_down'}
</span>
<span>{fmtGrowth(summary.gmvGrowthPercent)}</span>
</div>
</div>
<div className="partner-weekly-hero-grid">
<div>
<p className="label-md text-muted">活跃门店</p>
<p className="partner-weekly-stat-value">
{summary.activeStoreCount}
<span className="partner-weekly-stat-sub">/ {summary.totalStoreCount}</span>
</p>
</div>
<div>
<p className="label-md text-muted">订单总量</p>
<p className="partner-weekly-stat-value">{summary.orderCount}</p>
</div>
</div>
</section>
<section className="partner-weekly-card">
<div className="partner-weekly-card-header">
<div className="partner-weekly-card-title">
<span className="material-symbols-outlined partner-weekly-icon-amber">storefront</span>
<h3 className="headline-md">本周新签约门店</h3>
</div>
<p className="partner-weekly-target">
{summary.newStoreCount}
<span className="partner-weekly-target-sub">/ {summary.newStoreTarget}</span>
</p>
</div>
<div className="partner-weekly-progress-track">
<div
className="partner-weekly-progress-bar"
style={{ width: `${summary.newStoreProgressPercent}%` }}
/>
</div>
<p className="label-md text-muted partner-weekly-progress-hint">
已完成本周目标的 {summary.newStoreProgressPercent}%
{summary.newStoreCount < summary.newStoreTarget
? `还差 ${summary.newStoreTarget - summary.newStoreCount} 家!`
: '目标已达成!'}
</p>
</section>
<section className="partner-weekly-card">
<div className="partner-weekly-card-header">
<h3 className="headline-md">每日成交趋势</h3>
<span className="label-md text-muted">过去 7 </span>
</div>
<div className="partner-weekly-chart">
<div className="partner-weekly-chart-bars">
{data.dailyGmv.map((item) => {
const height = Math.max(8, Math.round((item.amount / maxDailyGmv) * 100));
const isMax = item.amount === maxDailyGmv && item.amount > 0;
return (
<div
key={item.date}
className={`partner-weekly-chart-bar${isMax ? ' partner-weekly-chart-bar--peak' : ''}`}
style={{ height: `${height}%` }}
title={`${item.weekdayLabel} ¥${fmtMoney(item.amount)}`}
/>
);
})}
</div>
<div className="partner-weekly-chart-labels">
{data.dailyGmv.map((item) => (
<span key={item.date}>{item.weekdayLabel}</span>
))}
</div>
</div>
</section>
<section className="partner-weekly-rank-section">
<div className="partner-weekly-card-header">
<h3 className="headline-md">门店业绩排行</h3>
<button
type="button"
className="partner-weekly-link"
onClick={() => navigate('/stores')}
>
查看全部
<span className="material-symbols-outlined">arrow_forward</span>
</button>
</div>
{topStores.length === 0 ? (
<p className="label-md text-muted partner-weekly-empty">本周暂无核销排行数据</p>
) : (
<div className="partner-weekly-rank-list">
{topStores.map((store) => (
<div
key={store.storeId}
className={`partner-weekly-rank-row${
store.rank === 1 ? ' partner-weekly-rank-row--first' : ''
}`}
>
<div className="partner-weekly-rank-badge">
{store.rank === 1 ? (
<span className="material-symbols-outlined">workspace_premium</span>
) : (
<span>{store.rank}</span>
)}
</div>
<div className="partner-weekly-rank-info">
<p className="partner-weekly-rank-name">{store.name}</p>
{store.subtitle && (
<p className="label-md text-muted">{store.subtitle}</p>
)}
</div>
<p className="partner-weekly-rank-amount">¥{fmtMoney(store.redeemAmount)}</p>
</div>
))}
</div>
)}
</section>
<section className="partner-weekly-insight">
<span className="material-symbols-outlined">tips_and_updates</span>
<div>
<p className="headline-md">合伙人经营策略</p>
<p className="body-md text-muted">{data.insight}</p>
</div>
</section>
</>
)}
</main>
</PullToRefresh>
);
}