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 ( -
+
{canGoBack ? (
- + ); } 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; +}