diff --git a/apps/h5-partner/src/pages/BillsPage.tsx b/apps/h5-partner/src/pages/BillsPage.tsx
index 9f6ad20..61c4d27 100644
--- a/apps/h5-partner/src/pages/BillsPage.tsx
+++ b/apps/h5-partner/src/pages/BillsPage.tsx
@@ -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 (
-
+
navigate('/center/settlement')} />
@@ -232,6 +233,6 @@ export default function BillsPage() {
>
)}
-
+
);
}
diff --git a/apps/h5-partner/src/pages/CenterPage.tsx b/apps/h5-partner/src/pages/CenterPage.tsx
index ba2db17..b7c06a5 100644
--- a/apps/h5-partner/src/pages/CenterPage.tsx
+++ b/apps/h5-partner/src/pages/CenterPage.tsx
@@ -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
[] = [Promise.resolve(refresh())];
+ if (isPrimary) {
+ tasks.push(
+ request('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('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 (
-
-
+
);
}
diff --git a/apps/h5-partner/src/pages/HomePage.tsx b/apps/h5-partner/src/pages/HomePage.tsx
index c7be2fb..6ca4998 100644
--- a/apps/h5-partner/src/pages/HomePage.tsx
+++ b/apps/h5-partner/src/pages/HomePage.tsx
@@ -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[] = [];
if (canOrders) {
- request<{ list: Array> }>('PARTNER_H5', '/partner/orders', { silent: true })
- .then((data) => setOrders(Array.isArray(data.list) ? data.list : []))
- .catch(() => setOrders([]));
+ tasks.push(
+ request<{ list: Array> }>('PARTNER_H5', '/partner/orders', { silent: true })
+ .then((data) => setOrders(Array.isArray(data.list) ? data.list : []))
+ .catch(() => setOrders([])),
+ );
} else {
setOrders([]);
}
if (canDashboard) {
- request>('PARTNER_H5', '/partner/dashboard', { silent: true })
- .then(setDash)
- .catch(() => setDash(null));
+ tasks.push(
+ request>('PARTNER_H5', '/partner/dashboard', { silent: true })
+ .then(setDash)
+ .catch(() => setDash(null)),
+ );
} else {
setDash(null);
}
if (canStores) {
- request>>('PARTNER_H5', '/partner/stores', { silent: true })
- .then((data) => setStores(Array.isArray(data) ? data : []))
- .catch(() => setStores([]));
+ tasks.push(
+ request>>('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 (
-
+
{isPrimary && (
<>
@@ -346,6 +364,6 @@ export default function HomePage() {
-
+
);
}
diff --git a/apps/h5-partner/src/pages/LeaderboardPage.tsx b/apps/h5-partner/src/pages/LeaderboardPage.tsx
index 9d649d2..9feaa68 100644
--- a/apps/h5-partner/src/pages/LeaderboardPage.tsx
+++ b/apps/h5-partner/src/pages/LeaderboardPage.tsx
@@ -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 (
-
+
navigate('/')} />
@@ -124,6 +129,6 @@ export default function LeaderboardPage() {
add
)}
-
+
);
}
diff --git a/apps/h5-partner/src/pages/OrderListPage.tsx b/apps/h5-partner/src/pages/OrderListPage.tsx
index bd7ce97..2712a3f 100644
--- a/apps/h5-partner/src/pages/OrderListPage.tsx
+++ b/apps/h5-partner/src/pages/OrderListPage.tsx
@@ -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
('PARTNER_H5', '/partner/orders')
+ return request('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 (
-
+
{!tabRoot && navigate('/')} />}
@@ -265,6 +273,6 @@ export default function OrderListPage({ tabRoot = false }: OrderListPageProps) {
)}
-
+
);
}
diff --git a/apps/h5-partner/src/pages/SettlementPage.tsx b/apps/h5-partner/src/pages/SettlementPage.tsx
index f843c66..0f3f5af 100644
--- a/apps/h5-partner/src/pages/SettlementPage.tsx
+++ b/apps/h5-partner/src/pages/SettlementPage.tsx
@@ -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('all');
+ const loadBills = useCallback(() => {
+ return request('PARTNER_H5', '/partner/settlement/bills').then(setBills);
+ }, []);
+
useEffect(() => {
if (!isLoggedIn()) { navigate('/login'); return; }
- request('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 (
-
+
navigate('/')} />
@@ -211,6 +216,6 @@ export default function SettlementPage() {
)}
-
+
);
}
diff --git a/apps/h5-partner/src/pages/StaffListPage.tsx b/apps/h5-partner/src/pages/StaffListPage.tsx
index fe54507..fbb706f 100644
--- a/apps/h5-partner/src/pages/StaffListPage.tsx
+++ b/apps/h5-partner/src/pages/StaffListPage.tsx
@@ -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 (
-
+
navigate('/center')} />
@@ -151,6 +152,6 @@ export default function StaffListPage() {
添加子账号
-
+
);
}
diff --git a/apps/h5-partner/src/pages/StoreListPage.tsx b/apps/h5-partner/src/pages/StoreListPage.tsx
index 0f3c9ce..e5bbcda 100644
--- a/apps/h5-partner/src/pages/StoreListPage.tsx
+++ b/apps/h5-partner/src/pages/StoreListPage.tsx
@@ -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 (
-
+
{error && {error}
}
@@ -188,6 +189,6 @@ export default function StoreListPage() {
);
})}
-
+
);
}
diff --git a/apps/h5-partner/src/pages/WeeklyReportPage.tsx b/apps/h5-partner/src/pages/WeeklyReportPage.tsx
index 543b16f..fcec099 100644
--- a/apps/h5-partner/src/pages/WeeklyReportPage.tsx
+++ b/apps/h5-partner/src/pages/WeeklyReportPage.tsx
@@ -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 (
-
+
navigate('/')} />
@@ -211,6 +216,6 @@ export default function WeeklyReportPage() {
>
)}
-
+
);
}
diff --git a/apps/h5-shop/src/pages/HomePage.tsx b/apps/h5-shop/src/pages/HomePage.tsx
index 9214a6b..8ba46f4 100644
--- a/apps/h5-shop/src/pages/HomePage.tsx
+++ b/apps/h5-shop/src/pages/HomePage.tsx
@@ -1,5 +1,6 @@
import { useCallback, useEffect, useState } from 'react';
import { Link, useNavigate } from 'react-router-dom';
+import PullToRefresh from '@dukang/shared-ui/PullToRefresh';
import { request } from '../lib/api';
function formatMoney(n: number) {
@@ -47,7 +48,7 @@ export default function HomePage() {
const closeTime = String(store?.closeTime || '22:00');
return (
-
+
@@ -127,6 +128,6 @@ export default function HomePage() {
-
+
);
}
diff --git a/apps/h5-shop/src/pages/MinePage.tsx b/apps/h5-shop/src/pages/MinePage.tsx
index e8b659e..924e348 100644
--- a/apps/h5-shop/src/pages/MinePage.tsx
+++ b/apps/h5-shop/src/pages/MinePage.tsx
@@ -1,6 +1,7 @@
-import { useEffect, useState } from 'react';
+import { useCallback, useEffect, useState } from 'react';
import { useNavigate, useSearchParams } from 'react-router-dom';
import { isWxAuthorizeEnabled } from '@dukang/shared-types';
+import PullToRefresh from '@dukang/shared-ui/PullToRefresh';
import { stripOAuthParamsFromLocation } from '@dukang/weixin-sdk';
import { useStoreSession } from '../contexts/StoreSessionContext';
import { getStoreProfile, request } from '../lib/api';
@@ -24,21 +25,21 @@ export default function MinePage() {
const [binding, setBinding] = useState(false);
const [bindMsg, setBindMsg] = useState('');
- useEffect(() => {
- request('SHOP_H5', '/shop/store').then(setStore).catch(() => setStore(null));
+ const loadMine = useCallback(() => {
+ return Promise.all([
+ request('SHOP_H5', '/shop/store').then(setStore).catch(() => setStore(null)),
+ fetchClientConfig()
+ .then((config) => setWxAuthorize(isWxAuthorizeEnabled(config)))
+ .catch(() => setWxAuthorize(false)),
+ fetchShopAccount()
+ .then((me) => setHasWechat(!!(me.hasWechat || me.wxOpenId)))
+ .catch(() => setHasWechat(null)),
+ ]);
}, []);
useEffect(() => {
- fetchClientConfig()
- .then((config) => setWxAuthorize(isWxAuthorizeEnabled(config)))
- .catch(() => setWxAuthorize(false));
- }, []);
-
- useEffect(() => {
- fetchShopAccount()
- .then((me) => setHasWechat(!!(me.hasWechat || me.wxOpenId)))
- .catch(() => setHasWechat(null));
- }, []);
+ void loadMine();
+ }, [loadMine]);
useEffect(() => {
if (!isWechatEnv() || !wxAuthorize || !searchParams.get('code')) return;
@@ -92,7 +93,7 @@ export default function MinePage() {
}
return (
-
+
@@ -174,6 +175,6 @@ export default function MinePage() {
退出登录
-
+
);
}
diff --git a/apps/h5-shop/src/pages/RecordsPage.tsx b/apps/h5-shop/src/pages/RecordsPage.tsx
index 138c8f3..93ed980 100644
--- a/apps/h5-shop/src/pages/RecordsPage.tsx
+++ b/apps/h5-shop/src/pages/RecordsPage.tsx
@@ -1,4 +1,5 @@
-import { useEffect, useMemo, useState } from 'react';
+import { useCallback, useEffect, useMemo, useState } from 'react';
+import PullToRefresh from '@dukang/shared-ui/PullToRefresh';
import { request } from '../lib/api';
type RangeKey = 'today' | '7d' | '30d';
@@ -28,15 +29,21 @@ export default function RecordsPage() {
const [statusFilter, setStatusFilter] = useState('all');
const [storeName, setStoreName] = useState('');
- useEffect(() => {
- request<{ list: Array> }>('SHOP_H5', '/shop/redeem/records').then((d) => {
- setRecords(d.list || []);
- });
- request>('SHOP_H5', '/shop/store')
- .then((s) => setStoreName(String(s.name || '')))
- .catch(() => {});
+ const loadRecords = useCallback(() => {
+ return Promise.all([
+ request<{ list: Array> }>('SHOP_H5', '/shop/redeem/records').then((d) => {
+ setRecords(d.list || []);
+ }),
+ request>('SHOP_H5', '/shop/store')
+ .then((s) => setStoreName(String(s.name || '')))
+ .catch(() => {}),
+ ]);
}, []);
+ useEffect(() => {
+ void loadRecords();
+ }, [loadRecords]);
+
const filtered = useMemo(() => {
return records.filter((r) => {
if (!inRange(String(r.createdAt), range)) return false;
@@ -55,7 +62,7 @@ export default function RecordsPage() {
}, [filtered]);
return (
-
+
@@ -176,6 +183,6 @@ export default function RecordsPage() {
)}
-
+
);
}
diff --git a/apps/h5-shop/src/pages/SelectStorePage.tsx b/apps/h5-shop/src/pages/SelectStorePage.tsx
index 5326801..d64c43a 100644
--- a/apps/h5-shop/src/pages/SelectStorePage.tsx
+++ b/apps/h5-shop/src/pages/SelectStorePage.tsx
@@ -1,5 +1,6 @@
-import { useEffect, useState } from 'react';
+import { useCallback, useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
+import PullToRefresh from '@dukang/shared-ui/PullToRefresh';
import { useStoreSession } from '../contexts/StoreSessionContext';
import {
needsStoreSelection,
@@ -19,15 +20,19 @@ export default function SelectStorePage() {
const currentStoreId = store?.storeId || '';
const canGoBack = Boolean(currentStoreId);
+ const loadStores = useCallback(() => {
+ return request('SHOP_H5', '/shop/auth/stores')
+ .then((list) => setStores(list))
+ .catch((e) => setMsg(e instanceof Error ? e.message : '加载门店失败'));
+ }, []);
+
useEffect(() => {
if (!authenticated) {
navigate('/login', { replace: true });
return;
}
- void request('SHOP_H5', '/shop/auth/stores')
- .then((list) => setStores(list))
- .catch((e) => setMsg(e instanceof Error ? e.message : '加载门店失败'));
- }, [authenticated, navigate]);
+ void loadStores();
+ }, [authenticated, navigate, loadStores]);
async function onSelect(storeId: string) {
if (loadingId) return;
@@ -63,7 +68,7 @@ export default function SelectStorePage() {
}
return (
-
-
+
);
}
diff --git a/apps/h5-shop/src/pages/StaffPage.tsx b/apps/h5-shop/src/pages/StaffPage.tsx
index 50c755e..6105aab 100644
--- a/apps/h5-shop/src/pages/StaffPage.tsx
+++ b/apps/h5-shop/src/pages/StaffPage.tsx
@@ -1,6 +1,7 @@
import { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { STORE_STAFF_ROLE_LABELS, type StoreStaffRole } from '@dukang/shared-types';
+import PullToRefresh from '@dukang/shared-ui/PullToRefresh';
import { useStoreSession } from '../contexts/StoreSessionContext';
import { getStoreProfile, request } from '../lib/api';
@@ -116,7 +117,7 @@ export default function StaffPage() {
form.storeIds.length === 0 ? ownedStores.length : form.storeIds.length;
return (
-
-
+
);
}
diff --git a/apps/mini-user/src/pages/addresses/index.config.ts b/apps/mini-user/src/pages/addresses/index.config.ts
index 20e9799..7b477d7 100644
--- a/apps/mini-user/src/pages/addresses/index.config.ts
+++ b/apps/mini-user/src/pages/addresses/index.config.ts
@@ -1,4 +1,6 @@
export default definePageConfig({
navigationStyle: 'custom',
navigationBarTitleText: '地址管理',
+ enablePullDownRefresh: true,
+ backgroundTextStyle: 'dark',
});
diff --git a/apps/mini-user/src/pages/addresses/index.tsx b/apps/mini-user/src/pages/addresses/index.tsx
index 87b5969..21c2807 100644
--- a/apps/mini-user/src/pages/addresses/index.tsx
+++ b/apps/mini-user/src/pages/addresses/index.tsx
@@ -1,6 +1,6 @@
import { useCallback, useState } from 'react';
import { View, Text } from '@tarojs/components';
-import Taro, { useDidShow, useRouter } from '@tarojs/taro';
+import Taro, { useDidShow, usePullDownRefresh, useRouter } from '@tarojs/taro';
import PageShell from '../../components/PageShell';
import SubPageHeader from '../../components/SubPageHeader';
import {
@@ -44,6 +44,10 @@ export default function AddressesPage() {
loadList();
});
+ usePullDownRefresh(() => {
+ void Promise.resolve(loadList()).finally(() => Taro.stopPullDownRefresh());
+ });
+
function selectAddress(addr: Address) {
if (!selectMode) return;
Taro.redirectTo({
diff --git a/apps/mini-user/src/pages/benefit/index.config.ts b/apps/mini-user/src/pages/benefit/index.config.ts
index a945419..204c17e 100644
--- a/apps/mini-user/src/pages/benefit/index.config.ts
+++ b/apps/mini-user/src/pages/benefit/index.config.ts
@@ -1,4 +1,6 @@
export default definePageConfig({
navigationStyle: 'custom',
navigationBarTitleText: '好客权益',
+ enablePullDownRefresh: true,
+ backgroundTextStyle: 'dark',
});
diff --git a/apps/mini-user/src/pages/benefit/index.tsx b/apps/mini-user/src/pages/benefit/index.tsx
index 993f9d4..7ffada2 100644
--- a/apps/mini-user/src/pages/benefit/index.tsx
+++ b/apps/mini-user/src/pages/benefit/index.tsx
@@ -1,6 +1,6 @@
-import { useEffect, useState } from 'react';
+import { useCallback, useEffect, useState } from 'react';
import { View, Text } from '@tarojs/components';
-import Taro, { useDidShow } from '@tarojs/taro';
+import Taro, { useDidShow, usePullDownRefresh } from '@tarojs/taro';
import PageShell from '../../components/PageShell';
import UserTabBar, { shouldRenderPageTabBar, syncTabBarSelected } from '../../components/UserTabBar';
import { goLogin } from '../../lib/auth-nav';
@@ -44,9 +44,9 @@ export default function BenefitPage() {
syncTabBarSelected(2);
});
- useEffect(() => {
- if (!loggedIn) return;
- Promise.all([
+ const loadBenefit = useCallback(() => {
+ if (!loggedIn) return Promise.resolve();
+ return Promise.all([
request('/benefit/summary'),
request('/benefit/coupons'),
])
@@ -57,6 +57,14 @@ export default function BenefitPage() {
.catch((e) => toast(e instanceof Error ? e.message : '加载失败'));
}, [loggedIn]);
+ useEffect(() => {
+ void loadBenefit();
+ }, [loadBenefit]);
+
+ usePullDownRefresh(() => {
+ void loadBenefit().finally(() => Taro.stopPullDownRefresh());
+ });
+
const available = coupons.filter((c) => c.status === 'ACTIVE' && c.balance > 0);
const history = coupons.filter((c) => c.status === 'USED_UP' || c.status === 'VOID');
const visible = tab === 'available' ? available : history;
diff --git a/apps/mini-user/src/pages/home/index.config.ts b/apps/mini-user/src/pages/home/index.config.ts
index 0d044a3..f25a67f 100644
--- a/apps/mini-user/src/pages/home/index.config.ts
+++ b/apps/mini-user/src/pages/home/index.config.ts
@@ -1,3 +1,5 @@
export default definePageConfig({
navigationBarTitleText: '杜康好客',
+ enablePullDownRefresh: true,
+ backgroundTextStyle: 'dark',
});
diff --git a/apps/mini-user/src/pages/home/index.tsx b/apps/mini-user/src/pages/home/index.tsx
index 3ec53cf..6f72c9f 100644
--- a/apps/mini-user/src/pages/home/index.tsx
+++ b/apps/mini-user/src/pages/home/index.tsx
@@ -1,6 +1,6 @@
-import { useEffect, useMemo, useState } from 'react';
+import { useCallback, useEffect, useMemo, useState } from 'react';
import { View, Text } from '@tarojs/components';
-import Taro, { useDidShow } from '@tarojs/taro';
+import Taro, { useDidShow, usePullDownRefresh } from '@tarojs/taro';
import PageShell from '../../components/PageShell';
import TabMainHeader from '../../components/TabMainHeader';
import CouponBadge from '../../components/CouponBadge';
@@ -41,14 +41,39 @@ export default function HomePage() {
});
});
- useEffect(() => {
+ const loadProducts = useCallback(() => {
setLoading(true);
- request(`/catalog/products?cityCode=${encodeURIComponent(cityCode)}`)
+ return request(`/catalog/products?cityCode=${encodeURIComponent(cityCode)}`)
.then((list) => setProducts(Array.isArray(list) ? list : []))
.catch((e) => toast(e instanceof Error ? e.message : '加载失败'))
.finally(() => setLoading(false));
}, [cityCode]);
+ useEffect(() => {
+ void loadProducts();
+ }, [loadProducts]);
+
+ usePullDownRefresh(() => {
+ void (async () => {
+ try {
+ const resolved = await resolveUserCity();
+ setDisplayCity(resolved.displayCity);
+ const nextCode = getCityCodeForCatalog(resolved);
+ setCityCode(nextCode);
+ setLoading(true);
+ const list = await request(
+ `/catalog/products?cityCode=${encodeURIComponent(nextCode)}`,
+ );
+ setProducts(Array.isArray(list) ? list : []);
+ } catch (e) {
+ toast(e instanceof Error ? e.message : '加载失败');
+ } finally {
+ setLoading(false);
+ Taro.stopPullDownRefresh();
+ }
+ })();
+ });
+
const availableAromas = useMemo(
() =>
AROMA_TABS.filter((item) =>
diff --git a/apps/mini-user/src/pages/mine/index.config.ts b/apps/mini-user/src/pages/mine/index.config.ts
index 3844ede..08e8af0 100644
--- a/apps/mini-user/src/pages/mine/index.config.ts
+++ b/apps/mini-user/src/pages/mine/index.config.ts
@@ -1,3 +1,5 @@
export default definePageConfig({
navigationBarTitleText: '我的',
+ enablePullDownRefresh: true,
+ backgroundTextStyle: 'dark',
});
diff --git a/apps/mini-user/src/pages/mine/index.tsx b/apps/mini-user/src/pages/mine/index.tsx
index 7f17058..3a0246a 100644
--- a/apps/mini-user/src/pages/mine/index.tsx
+++ b/apps/mini-user/src/pages/mine/index.tsx
@@ -1,6 +1,6 @@
import { useEffect, useState } from 'react';
import { View, Text, Image, Button, Input } from '@tarojs/components';
-import Taro, { useDidShow } from '@tarojs/taro';
+import Taro, { useDidShow, usePullDownRefresh } from '@tarojs/taro';
import { isWxAuthorizeEnabled, type ClientRuntimeConfig } from '@dukang/shared-types';
import PageShell from '../../components/PageShell';
import TabMainHeader from '../../components/TabMainHeader';
@@ -64,9 +64,9 @@ export default function MinePage() {
}
function loadProfile() {
- if (!isLoggedIn()) return;
+ if (!isLoggedIn()) return Promise.resolve();
setProfileLoadError('');
- Promise.all([
+ return Promise.all([
request('/auth/me'),
request>>('/benefit/coupons').catch(() => []),
...ORDER_SHORTCUTS.map((s) =>
@@ -109,6 +109,17 @@ export default function MinePage() {
}
});
+ usePullDownRefresh(() => {
+ const loggedInNow = isLoggedIn();
+ setAuthed(loggedInNow);
+ if (!loggedInNow) {
+ resetGuestState();
+ Taro.stopPullDownRefresh();
+ return;
+ }
+ void loadProfile().finally(() => Taro.stopPullDownRefresh());
+ });
+
useEffect(() => {
request('/common/client-config')
.then((config) => setWxAuthorize(isWxAuthorizeEnabled(config)))
diff --git a/apps/mini-user/src/pages/orders/index.config.ts b/apps/mini-user/src/pages/orders/index.config.ts
index a28bb6e..fb118cf 100644
--- a/apps/mini-user/src/pages/orders/index.config.ts
+++ b/apps/mini-user/src/pages/orders/index.config.ts
@@ -1,4 +1,6 @@
export default definePageConfig({
navigationStyle: 'custom',
navigationBarTitleText: '我的订单',
+ enablePullDownRefresh: true,
+ backgroundTextStyle: 'dark',
});
diff --git a/apps/mini-user/src/pages/orders/index.tsx b/apps/mini-user/src/pages/orders/index.tsx
index 60d46e6..bbd0cc3 100644
--- a/apps/mini-user/src/pages/orders/index.tsx
+++ b/apps/mini-user/src/pages/orders/index.tsx
@@ -1,6 +1,6 @@
-import { useEffect, useState } from 'react';
+import { useCallback, useEffect, useState } from 'react';
import { View, Text } from '@tarojs/components';
-import Taro, { useRouter } from '@tarojs/taro';
+import Taro, { usePullDownRefresh, useRouter } from '@tarojs/taro';
import PageShell from '../../components/PageShell';
import SubPageHeader from '../../components/SubPageHeader';
import { request, toast } from '../../lib/api';
@@ -47,9 +47,9 @@ export default function OrdersPage() {
const [orders, setOrders] = useState([]);
const [loading, setLoading] = useState(true);
- useEffect(() => {
+ const loadOrders = useCallback(() => {
setLoading(true);
- request<{ list?: OrderRow[]; items?: OrderRow[]; total?: number } | OrderRow[]>(
+ return request<{ list?: OrderRow[]; items?: OrderRow[]; total?: number } | OrderRow[]>(
`/trade/orders?tab=${encodeURIComponent(tab)}&pageSize=20`,
)
.then((data) => {
@@ -63,6 +63,14 @@ export default function OrdersPage() {
.finally(() => setLoading(false));
}, [tab]);
+ useEffect(() => {
+ void loadOrders();
+ }, [loadOrders]);
+
+ usePullDownRefresh(() => {
+ void loadOrders().finally(() => Taro.stopPullDownRefresh());
+ });
+
return (
diff --git a/apps/mini-user/src/pages/stores/index.config.ts b/apps/mini-user/src/pages/stores/index.config.ts
index 860f46f..50e4a98 100644
--- a/apps/mini-user/src/pages/stores/index.config.ts
+++ b/apps/mini-user/src/pages/stores/index.config.ts
@@ -1,3 +1,5 @@
export default definePageConfig({
navigationBarTitleText: '门店',
+ enablePullDownRefresh: true,
+ backgroundTextStyle: 'dark',
});
diff --git a/apps/mini-user/src/pages/stores/index.tsx b/apps/mini-user/src/pages/stores/index.tsx
index d77a9f4..e545c22 100644
--- a/apps/mini-user/src/pages/stores/index.tsx
+++ b/apps/mini-user/src/pages/stores/index.tsx
@@ -1,6 +1,6 @@
-import { useEffect, useState } from 'react';
+import { useCallback, useEffect, useState } from 'react';
import { View, Text, Image, Input } from '@tarojs/components';
-import Taro, { useDidShow } from '@tarojs/taro';
+import Taro, { useDidShow, usePullDownRefresh } from '@tarojs/taro';
import PageShell from '../../components/PageShell';
import TabMainHeader from '../../components/TabMainHeader';
import RegionPicker from '../../components/RegionPicker';
@@ -49,15 +49,39 @@ export default function StoresPage() {
});
});
- useEffect(() => {
+ const loadStores = useCallback(() => {
setLoading(true);
const path = cityCode ? `/stores?cityCode=${encodeURIComponent(cityCode)}` : '/stores';
- request(path)
+ return request(path)
.then((list) => setStores(Array.isArray(list) ? list : []))
.catch((e) => toast(e instanceof Error ? e.message : '加载失败'))
.finally(() => setLoading(false));
}, [cityCode]);
+ useEffect(() => {
+ void loadStores();
+ }, [loadStores]);
+
+ usePullDownRefresh(() => {
+ void (async () => {
+ try {
+ const resolved = await resolveUserCity();
+ setRegion(resolved.region);
+ const nextCode = getCityCodeForCatalog(resolved);
+ setCityCode(nextCode);
+ setLoading(true);
+ const path = nextCode ? `/stores?cityCode=${encodeURIComponent(nextCode)}` : '/stores';
+ const list = await request(path);
+ setStores(Array.isArray(list) ? list : []);
+ } catch (e) {
+ toast(e instanceof Error ? e.message : '加载失败');
+ } finally {
+ setLoading(false);
+ Taro.stopPullDownRefresh();
+ }
+ })();
+ });
+
const filtered = stores.filter((s) => {
if (!matchesRegionFilter(s, region)) return false;
if (!keyword.trim()) return true;
diff --git a/packages/shared-ui/package.json b/packages/shared-ui/package.json
index ee6bbe4..698dc7b 100644
--- a/packages/shared-ui/package.json
+++ b/packages/shared-ui/package.json
@@ -10,7 +10,8 @@
"./PageHeader": "./src/PageHeader.tsx",
"./CouponBadge": "./src/CouponBadge.tsx",
"./OrderStatusTabs": "./src/OrderStatusTabs.tsx",
- "./AppImage": "./src/AppImage.tsx"
+ "./AppImage": "./src/AppImage.tsx",
+ "./PullToRefresh": "./src/PullToRefresh.tsx"
},
"peerDependencies": {
"react": "^18.3.1"
diff --git a/packages/shared-ui/src/PullToRefresh.tsx b/packages/shared-ui/src/PullToRefresh.tsx
new file mode 100644
index 0000000..8ad262a
--- /dev/null
+++ b/packages/shared-ui/src/PullToRefresh.tsx
@@ -0,0 +1,134 @@
+import { useCallback, useEffect, useRef, useState, type CSSProperties, type ReactNode } from 'react';
+import './pull-to-refresh.css';
+
+const PULL_THRESHOLD = 64;
+const MAX_PULL = 96;
+
+type PullToRefreshProps = {
+ onRefresh: () => void | Promise;
+ children: ReactNode;
+ className?: string;
+ style?: CSSProperties;
+ disabled?: boolean;
+};
+
+function getScrollTop() {
+ return window.scrollY || document.documentElement.scrollTop || document.body.scrollTop || 0;
+}
+
+export default function PullToRefresh({
+ onRefresh,
+ children,
+ className,
+ style,
+ disabled = false,
+}: PullToRefreshProps) {
+ const rootRef = useRef(null);
+ const [pull, setPull] = useState(0);
+ const [refreshing, setRefreshing] = useState(false);
+ const startY = useRef(0);
+ const pulling = useRef(false);
+ const busy = useRef(false);
+ const pullRef = useRef(0);
+ const disabledRef = useRef(disabled);
+ const onRefreshRef = useRef(onRefresh);
+
+ disabledRef.current = disabled;
+ onRefreshRef.current = onRefresh;
+ pullRef.current = pull;
+
+ const finishRefresh = useCallback(async () => {
+ if (busy.current) return;
+ busy.current = true;
+ setRefreshing(true);
+ setPull(40);
+ pullRef.current = 40;
+ try {
+ await onRefreshRef.current();
+ } finally {
+ setRefreshing(false);
+ setPull(0);
+ pullRef.current = 0;
+ busy.current = false;
+ pulling.current = false;
+ }
+ }, []);
+
+ useEffect(() => {
+ const el = rootRef.current;
+ if (!el) return;
+
+ const onTouchStart = (e: TouchEvent) => {
+ if (disabledRef.current || busy.current) return;
+ if (getScrollTop() > 2) return;
+ startY.current = e.touches[0]?.clientY ?? 0;
+ pulling.current = true;
+ };
+
+ const onTouchMove = (e: TouchEvent) => {
+ if (!pulling.current || disabledRef.current || busy.current) return;
+ if (getScrollTop() > 2) {
+ pulling.current = false;
+ setPull(0);
+ pullRef.current = 0;
+ return;
+ }
+ const y = e.touches[0]?.clientY ?? 0;
+ const delta = y - startY.current;
+ if (delta <= 0) {
+ setPull(0);
+ pullRef.current = 0;
+ return;
+ }
+ const next = Math.min(MAX_PULL, delta * 0.45);
+ setPull(next);
+ pullRef.current = next;
+ if (next > 8 && e.cancelable) e.preventDefault();
+ };
+
+ const onTouchEnd = () => {
+ if (!pulling.current || disabledRef.current) return;
+ pulling.current = false;
+ if (pullRef.current >= PULL_THRESHOLD) {
+ void finishRefresh();
+ return;
+ }
+ setPull(0);
+ pullRef.current = 0;
+ };
+
+ el.addEventListener('touchstart', onTouchStart, { passive: true });
+ el.addEventListener('touchmove', onTouchMove, { passive: false });
+ el.addEventListener('touchend', onTouchEnd);
+ el.addEventListener('touchcancel', onTouchEnd);
+ return () => {
+ el.removeEventListener('touchstart', onTouchStart);
+ el.removeEventListener('touchmove', onTouchMove);
+ el.removeEventListener('touchend', onTouchEnd);
+ el.removeEventListener('touchcancel', onTouchEnd);
+ };
+ }, [finishRefresh]);
+
+ const showHint = pull > 8 || refreshing;
+ const ready = pull >= PULL_THRESHOLD;
+ const pad = showHint ? Math.max(pull, refreshing ? 40 : 0) : 0;
+
+ return (
+
+
+
+ {refreshing ? '刷新中…' : ready ? '松开刷新' : '下拉刷新'}
+
+
+ {children}
+
+ );
+}
diff --git a/packages/shared-ui/src/pull-to-refresh.css b/packages/shared-ui/src/pull-to-refresh.css
new file mode 100644
index 0000000..d9c566b
--- /dev/null
+++ b/packages/shared-ui/src/pull-to-refresh.css
@@ -0,0 +1,26 @@
+.dukang-ptr {
+ position: relative;
+ min-height: 100%;
+ box-sizing: border-box;
+}
+
+.dukang-ptr-indicator {
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ display: flex;
+ align-items: flex-end;
+ justify-content: center;
+ overflow: hidden;
+ pointer-events: none;
+ z-index: 2;
+}
+
+.dukang-ptr-text {
+ padding-bottom: 10px;
+ font-size: 12px;
+ line-height: 1;
+ color: #8a8580;
+ letter-spacing: 0.02em;
+}