feat(h5): add pull-to-refresh on user partner and shop pages
CI / verify (pull_request) Has been cancelled

Share PullToRefresh for partner/shop H5; enable Taro pull-down on mini-user lists.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-22 11:19:42 +08:00
parent 270cebc79a
commit ae7c63c08d
29 changed files with 443 additions and 121 deletions
+3 -2
View File
@@ -1,6 +1,7 @@
import { useEffect, useMemo, useState } from 'react';
import { useNavigate, useSearchParams } from 'react-router-dom';
import PageHeader from '@dukang/shared-ui/PageHeader';
import PullToRefresh from '@dukang/shared-ui/PullToRefresh';
import type { PartnerBillDto } from '@dukang/shared-types';
import { isLoggedIn, request } from '../lib/api';
import { toastError, toastSuccess } from '../lib/toast';
@@ -81,7 +82,7 @@ export default function BillsPage() {
}
return (
<div className="partner-bills-page">
<PullToRefresh onRefresh={loadBills} className="partner-bills-page">
<PageHeader title="账单确认" onBack={() => navigate('/center/settlement')} />
<div className="partner-bill-stepper">
@@ -232,6 +233,6 @@ export default function BillsPage() {
</footer>
</>
)}
</div>
</PullToRefresh>
);
}
+21 -11
View File
@@ -1,7 +1,8 @@
import { useEffect, useMemo, useState } from 'react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { Link } from 'react-router-dom';
import type { PartnerBillDto } from '@dukang/shared-types';
import { isWxAuthorizeEnabled } from '@dukang/shared-types';
import PullToRefresh from '@dukang/shared-ui/PullToRefresh';
import { request } from '../lib/api';
import { usePartnerSession } from '../contexts/PartnerSessionContext';
import { contactSupport } from '../lib/contact';
@@ -36,15 +37,24 @@ export default function CenterPage({ variant = 'primary', roleLabel }: CenterPag
document.title = isPrimary ? '合伙人中心' : '个人中心';
}, [isPrimary]);
const loadCenter = useCallback(() => {
const tasks: Promise<unknown>[] = [Promise.resolve(refresh())];
if (isPrimary) {
tasks.push(
request<PartnerBillDto[]>('PARTNER_H5', '/partner/settlement/bills')
.then(setBills)
.catch(() => setBills([])),
listPartnerStaff()
.then((list) => setStaffCount(list.length))
.catch(() => setStaffCount(0)),
);
}
return Promise.all(tasks).then(() => undefined);
}, [isPrimary, refresh]);
useEffect(() => {
if (!isPrimary) return;
void request<PartnerBillDto[]>('PARTNER_H5', '/partner/settlement/bills')
.then(setBills)
.catch(() => setBills([]));
void listPartnerStaff()
.then((list) => setStaffCount(list.length))
.catch(() => setStaffCount(0));
}, [isPrimary]);
void loadCenter();
}, [loadCenter]);
const finance = useMemo(() => {
const pending = bills.filter((b) => b.status === 'DRAFT' || b.status === 'REJECTED');
@@ -109,7 +119,7 @@ export default function CenterPage({ variant = 'primary', roleLabel }: CenterPag
const badgeLabel = roleLabel || (isPrimary ? '城市合伙人' : '拓店员');
return (
<div className="page partner-center-page partner-home--flush-top">
<PullToRefresh onRefresh={loadCenter} className="page partner-center-page partner-home--flush-top">
<section className="partner-profile-card">
<button
type="button"
@@ -286,6 +296,6 @@ export default function CenterPage({ variant = 'primary', roleLabel }: CenterPag
<div className="partner-center-footer">
<p className="label-md text-muted"> · </p>
</div>
</div>
</PullToRefresh>
);
}
+42 -24
View File
@@ -1,6 +1,7 @@
import { useEffect, useMemo, useState } from 'react';
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 { isLoggedIn, request } from '../lib/api';
import { fetchPartnerLeaderboard } from '../lib/leaderboard';
import {
@@ -155,45 +156,62 @@ export default function HomePage() {
document.title = '工作台';
}, []);
useEffect(() => {
if (!isLoggedIn()) { navigate('/login'); return; }
// 等 session 带上账号后再按权限发请求,避免无权限接口弹错
if (!account) return;
const loadHome = useCallback(() => {
if (!isLoggedIn()) {
navigate('/login');
return Promise.resolve();
}
if (!account) return Promise.resolve();
const tasks: Promise<unknown>[] = [];
if (canOrders) {
request<{ list: Array<Record<string, unknown>> }>('PARTNER_H5', '/partner/orders', { silent: true })
.then((data) => setOrders(Array.isArray(data.list) ? data.list : []))
.catch(() => setOrders([]));
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) {
request<Record<string, unknown>>('PARTNER_H5', '/partner/dashboard', { silent: true })
.then(setDash)
.catch(() => setDash(null));
tasks.push(
request<Record<string, unknown>>('PARTNER_H5', '/partner/dashboard', { silent: true })
.then(setDash)
.catch(() => setDash(null)),
);
} else {
setDash(null);
}
if (canStores) {
request<Array<Record<string, unknown>>>('PARTNER_H5', '/partner/stores', { silent: true })
.then((data) => setStores(Array.isArray(data) ? data : []))
.catch(() => setStores([]));
tasks.push(
request<Array<Record<string, unknown>>>('PARTNER_H5', '/partner/stores', { silent: true })
.then((data) => setStores(Array.isArray(data) ? data : []))
.catch(() => setStores([])),
);
} else {
setStores([]);
}
// 主账号与全部子账号均可查看同团队贡献榜(后端不校验业务权限点)
fetchPartnerLeaderboard('month')
.then((data) => {
setLeaderboardEntries(data.list.slice(0, LEADERBOARD_PREVIEW_LIMIT));
})
.catch(() => {
setLeaderboardEntries([]);
});
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);
@@ -219,7 +237,7 @@ export default function HomePage() {
const profit = revenue * 0.25;
return (
<div className="page partner-home partner-home--flush-top">
<PullToRefresh onRefresh={loadHome} className="page partner-home partner-home--flush-top">
<main className="partner-home-body">
{isPrimary && (
<>
@@ -346,6 +364,6 @@ export default function HomePage() {
<LeaderboardPreview entries={leaderboardEntries} />
</main>
</div>
</PullToRefresh>
);
}
+10 -5
View File
@@ -1,7 +1,8 @@
import { useEffect, useState } from 'react';
import { useCallback, useEffect, useState } from 'react';
import { useNavigate, Link } from 'react-router-dom';
import type { PartnerLeaderboardPeriod, PartnerLeaderboardResponse } from '@dukang/shared-types';
import PageHeader from '@dukang/shared-ui/PageHeader';
import PullToRefresh from '@dukang/shared-ui/PullToRefresh';
import { fetchPartnerLeaderboard } from '../lib/leaderboard';
import { usePartnerSession } from '../contexts/PartnerSessionContext';
import { isPrimaryAccount } from '../lib/partnerAccess';
@@ -33,10 +34,10 @@ export default function LeaderboardPage() {
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
useEffect(() => {
const loadLeaderboard = useCallback(() => {
setLoading(true);
setError('');
fetchPartnerLeaderboard(period)
return fetchPartnerLeaderboard(period)
.then(setData)
.catch((e) => {
setData(null);
@@ -45,8 +46,12 @@ export default function LeaderboardPage() {
.finally(() => setLoading(false));
}, [period]);
useEffect(() => {
void loadLeaderboard();
}, [loadLeaderboard]);
return (
<div className="page-no-tab">
<PullToRefresh onRefresh={loadLeaderboard} className="page-no-tab">
<PageHeader title="团队贡献榜" onBack={() => navigate('/')} />
<div className="partner-leaderboard-tabs">
@@ -124,6 +129,6 @@ export default function LeaderboardPage() {
<span className="material-symbols-outlined">add</span>
</Link>
)}
</div>
</PullToRefresh>
);
}
+15 -7
View File
@@ -1,6 +1,7 @@
import { useEffect, useMemo, useState } from 'react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { Link, useNavigate } from 'react-router-dom';
import PageHeader from '@dukang/shared-ui/PageHeader';
import PullToRefresh from '@dukang/shared-ui/PullToRefresh';
import { isLoggedIn, request } from '../lib/api';
import { usePartnerSession } from '../contexts/PartnerSessionContext';
import { hasWarehouseAccess } from '../lib/partnerAccess';
@@ -59,14 +60,17 @@ export default function OrderListPage({ tabRoot = false }: OrderListPageProps) {
document.title = '订单管理';
}, []);
useEffect(() => {
if (!isLoggedIn()) { navigate('/login'); return; }
const loadOrders = useCallback(() => {
if (!isLoggedIn()) {
navigate('/login');
return Promise.resolve();
}
if (!warehouseOk) {
setData({ list: [], hasWarehouseAccess: false, message: '未配置仓库管理权限' });
setLoaded(true);
return;
return Promise.resolve();
}
request<OrdersResponse>('PARTNER_H5', '/partner/orders')
return request<OrdersResponse>('PARTNER_H5', '/partner/orders')
.then((res) => {
setData({
list: Array.isArray(res.list) ? res.list : [],
@@ -78,6 +82,10 @@ export default function OrderListPage({ tabRoot = false }: OrderListPageProps) {
.finally(() => setLoaded(true));
}, [navigate, warehouseOk]);
useEffect(() => {
void loadOrders();
}, [loadOrders]);
const filtered = useMemo(() => data.list.filter((o) => {
if (statusFilter === 'ALL') return true;
const s = String(o.status).toUpperCase();
@@ -142,7 +150,7 @@ export default function OrderListPage({ tabRoot = false }: OrderListPageProps) {
}
return (
<div className={`page partner-orders-page${tabRoot ? '' : ' page-no-tab'}`}>
<PullToRefresh onRefresh={loadOrders} className={`page partner-orders-page${tabRoot ? '' : ' page-no-tab'}`}>
{!tabRoot && <PageHeader title="订单中心" onBack={() => navigate('/')} />}
<div className="partner-segment">
@@ -265,6 +273,6 @@ export default function OrderListPage({ tabRoot = false }: OrderListPageProps) {
</div>
</div>
)}
</div>
</PullToRefresh>
);
}
+10 -5
View File
@@ -1,6 +1,7 @@
import { useEffect, useMemo, useState } from 'react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import PageHeader from '@dukang/shared-ui/PageHeader';
import PullToRefresh from '@dukang/shared-ui/PullToRefresh';
import type { PartnerBillDto } from '@dukang/shared-types';
import { isLoggedIn, request } from '../lib/api';
@@ -47,10 +48,14 @@ export default function SettlementPage() {
const [month, setMonth] = useState({ year: now.getFullYear(), month: now.getMonth() + 1 });
const [statusFilter, setStatusFilter] = useState<StatusFilter>('all');
const loadBills = useCallback(() => {
return request<PartnerBillDto[]>('PARTNER_H5', '/partner/settlement/bills').then(setBills);
}, []);
useEffect(() => {
if (!isLoggedIn()) { navigate('/login'); return; }
request<PartnerBillDto[]>('PARTNER_H5', '/partner/settlement/bills').then(setBills);
}, [navigate]);
void loadBills();
}, [navigate, loadBills]);
const summary = useMemo(() => {
const pending = bills.filter((b) => b.status === 'DRAFT');
@@ -102,7 +107,7 @@ export default function SettlementPage() {
];
return (
<div className="page-no-tab partner-settlement-page">
<PullToRefresh onRefresh={loadBills} className="page-no-tab partner-settlement-page">
<PageHeader title="财务对账中心" onBack={() => navigate('/')} />
<main className="partner-settlement-body">
@@ -211,6 +216,6 @@ export default function SettlementPage() {
)}
</section>
</main>
</div>
</PullToRefresh>
);
}
+3 -2
View File
@@ -1,6 +1,7 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import { Link, useNavigate } from 'react-router-dom';
import PageHeader from '@dukang/shared-ui/PageHeader';
import PullToRefresh from '@dukang/shared-ui/PullToRefresh';
import {
AccountStatus,
PARTNER_STAFF_ROLE_LABELS,
@@ -86,7 +87,7 @@ export default function StaffListPage() {
}
return (
<div className="page-no-tab partner-staff-page">
<PullToRefresh onRefresh={loadStaff} className="page-no-tab partner-staff-page">
<PageHeader title="子账号管理" onBack={() => navigate('/center')} />
<div style={{ padding: '0 20px 16px' }}>
@@ -151,6 +152,6 @@ export default function StaffListPage() {
</Link>
</div>
</div>
</PullToRefresh>
);
}
+3 -2
View File
@@ -1,5 +1,6 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import { Link, useNavigate, useSearchParams } from 'react-router-dom';
import PullToRefresh from '@dukang/shared-ui/PullToRefresh';
import { isLoggedIn, request } from '../lib/api';
import { usePartnerSession } from '../contexts/PartnerSessionContext';
import { canCreatePartnerStore, canManagePartnerStore } from '../lib/partnerAccess';
@@ -85,7 +86,7 @@ export default function StoreListPage() {
}
return (
<div className="page partner-store-page partner-home--flush-top">
<PullToRefresh onRefresh={loadStores} className="page partner-store-page partner-home--flush-top">
{error && <p className="partner-form-error" role="alert" style={{ margin: '0 20px 12px' }}>{error}</p>}
<div className="partner-sticky-filter">
@@ -188,6 +189,6 @@ export default function StoreListPage() {
</div>
);
})}
</div>
</PullToRefresh>
);
}
+10 -5
View File
@@ -1,7 +1,8 @@
import { useEffect, useMemo, useState } from 'react';
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) {
@@ -20,10 +21,10 @@ export default function WeeklyReportPage() {
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
useEffect(() => {
const loadWeeklyReport = useCallback(() => {
setLoading(true);
setError('');
fetchPartnerWeeklyReport(selectedStart)
return fetchPartnerWeeklyReport(selectedStart)
.then(setData)
.catch((e) => {
setData(null);
@@ -32,6 +33,10 @@ export default function WeeklyReportPage() {
.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));
@@ -42,7 +47,7 @@ export default function WeeklyReportPage() {
const growthPositive = (summary?.gmvGrowthPercent ?? 0) >= 0;
return (
<div className="page-no-tab">
<PullToRefresh onRefresh={loadWeeklyReport} className="page-no-tab">
<PageHeader title="数据周报" onBack={() => navigate('/')} />
<main className="partner-weekly-page">
@@ -211,6 +216,6 @@ export default function WeeklyReportPage() {
</>
)}
</main>
</div>
</PullToRefresh>
);
}