feat(h5): add pull-to-refresh on user partner and shop pages
CI / verify (pull_request) Has been cancelled
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:
@@ -1,6 +1,7 @@
|
|||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||||
import PageHeader from '@dukang/shared-ui/PageHeader';
|
import PageHeader from '@dukang/shared-ui/PageHeader';
|
||||||
|
import PullToRefresh from '@dukang/shared-ui/PullToRefresh';
|
||||||
import type { PartnerBillDto } from '@dukang/shared-types';
|
import type { PartnerBillDto } from '@dukang/shared-types';
|
||||||
import { isLoggedIn, request } from '../lib/api';
|
import { isLoggedIn, request } from '../lib/api';
|
||||||
import { toastError, toastSuccess } from '../lib/toast';
|
import { toastError, toastSuccess } from '../lib/toast';
|
||||||
@@ -81,7 +82,7 @@ export default function BillsPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="partner-bills-page">
|
<PullToRefresh onRefresh={loadBills} className="partner-bills-page">
|
||||||
<PageHeader title="账单确认" onBack={() => navigate('/center/settlement')} />
|
<PageHeader title="账单确认" onBack={() => navigate('/center/settlement')} />
|
||||||
|
|
||||||
<div className="partner-bill-stepper">
|
<div className="partner-bill-stepper">
|
||||||
@@ -232,6 +233,6 @@ export default function BillsPage() {
|
|||||||
</footer>
|
</footer>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</PullToRefresh>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import type { PartnerBillDto } from '@dukang/shared-types';
|
import type { PartnerBillDto } from '@dukang/shared-types';
|
||||||
import { isWxAuthorizeEnabled } from '@dukang/shared-types';
|
import { isWxAuthorizeEnabled } from '@dukang/shared-types';
|
||||||
|
import PullToRefresh from '@dukang/shared-ui/PullToRefresh';
|
||||||
import { request } from '../lib/api';
|
import { request } from '../lib/api';
|
||||||
import { usePartnerSession } from '../contexts/PartnerSessionContext';
|
import { usePartnerSession } from '../contexts/PartnerSessionContext';
|
||||||
import { contactSupport } from '../lib/contact';
|
import { contactSupport } from '../lib/contact';
|
||||||
@@ -36,15 +37,24 @@ export default function CenterPage({ variant = 'primary', roleLabel }: CenterPag
|
|||||||
document.title = isPrimary ? '合伙人中心' : '个人中心';
|
document.title = isPrimary ? '合伙人中心' : '个人中心';
|
||||||
}, [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(() => {
|
useEffect(() => {
|
||||||
if (!isPrimary) return;
|
void loadCenter();
|
||||||
void request<PartnerBillDto[]>('PARTNER_H5', '/partner/settlement/bills')
|
}, [loadCenter]);
|
||||||
.then(setBills)
|
|
||||||
.catch(() => setBills([]));
|
|
||||||
void listPartnerStaff()
|
|
||||||
.then((list) => setStaffCount(list.length))
|
|
||||||
.catch(() => setStaffCount(0));
|
|
||||||
}, [isPrimary]);
|
|
||||||
|
|
||||||
const finance = useMemo(() => {
|
const finance = useMemo(() => {
|
||||||
const pending = bills.filter((b) => b.status === 'DRAFT' || b.status === 'REJECTED');
|
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 ? '城市合伙人' : '拓店员');
|
const badgeLabel = roleLabel || (isPrimary ? '城市合伙人' : '拓店员');
|
||||||
|
|
||||||
return (
|
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">
|
<section className="partner-profile-card">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -286,6 +296,6 @@ export default function CenterPage({ variant = 'primary', roleLabel }: CenterPag
|
|||||||
<div className="partner-center-footer">
|
<div className="partner-center-footer">
|
||||||
<p className="label-md text-muted">传承千年 · 杜康好客</p>
|
<p className="label-md text-muted">传承千年 · 杜康好客</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</PullToRefresh>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 { Link, useNavigate } from 'react-router-dom';
|
||||||
import type { PartnerLeaderboardEntry } from '@dukang/shared-types';
|
import type { PartnerLeaderboardEntry } from '@dukang/shared-types';
|
||||||
|
import PullToRefresh from '@dukang/shared-ui/PullToRefresh';
|
||||||
import { isLoggedIn, request } from '../lib/api';
|
import { isLoggedIn, request } from '../lib/api';
|
||||||
import { fetchPartnerLeaderboard } from '../lib/leaderboard';
|
import { fetchPartnerLeaderboard } from '../lib/leaderboard';
|
||||||
import {
|
import {
|
||||||
@@ -155,45 +156,62 @@ export default function HomePage() {
|
|||||||
document.title = '工作台';
|
document.title = '工作台';
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
const loadHome = useCallback(() => {
|
||||||
if (!isLoggedIn()) { navigate('/login'); return; }
|
if (!isLoggedIn()) {
|
||||||
// 等 session 带上账号后再按权限发请求,避免无权限接口弹错
|
navigate('/login');
|
||||||
if (!account) return;
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
if (!account) return Promise.resolve();
|
||||||
|
|
||||||
|
const tasks: Promise<unknown>[] = [];
|
||||||
|
|
||||||
if (canOrders) {
|
if (canOrders) {
|
||||||
request<{ list: Array<Record<string, unknown>> }>('PARTNER_H5', '/partner/orders', { silent: true })
|
tasks.push(
|
||||||
.then((data) => setOrders(Array.isArray(data.list) ? data.list : []))
|
request<{ list: Array<Record<string, unknown>> }>('PARTNER_H5', '/partner/orders', { silent: true })
|
||||||
.catch(() => setOrders([]));
|
.then((data) => setOrders(Array.isArray(data.list) ? data.list : []))
|
||||||
|
.catch(() => setOrders([])),
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
setOrders([]);
|
setOrders([]);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (canDashboard) {
|
if (canDashboard) {
|
||||||
request<Record<string, unknown>>('PARTNER_H5', '/partner/dashboard', { silent: true })
|
tasks.push(
|
||||||
.then(setDash)
|
request<Record<string, unknown>>('PARTNER_H5', '/partner/dashboard', { silent: true })
|
||||||
.catch(() => setDash(null));
|
.then(setDash)
|
||||||
|
.catch(() => setDash(null)),
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
setDash(null);
|
setDash(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (canStores) {
|
if (canStores) {
|
||||||
request<Array<Record<string, unknown>>>('PARTNER_H5', '/partner/stores', { silent: true })
|
tasks.push(
|
||||||
.then((data) => setStores(Array.isArray(data) ? data : []))
|
request<Array<Record<string, unknown>>>('PARTNER_H5', '/partner/stores', { silent: true })
|
||||||
.catch(() => setStores([]));
|
.then((data) => setStores(Array.isArray(data) ? data : []))
|
||||||
|
.catch(() => setStores([])),
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
setStores([]);
|
setStores([]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 主账号与全部子账号均可查看同团队贡献榜(后端不校验业务权限点)
|
tasks.push(
|
||||||
fetchPartnerLeaderboard('month')
|
fetchPartnerLeaderboard('month')
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
setLeaderboardEntries(data.list.slice(0, LEADERBOARD_PREVIEW_LIMIT));
|
setLeaderboardEntries(data.list.slice(0, LEADERBOARD_PREVIEW_LIMIT));
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
setLeaderboardEntries([]);
|
setLeaderboardEntries([]);
|
||||||
});
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
return Promise.all(tasks).then(() => undefined);
|
||||||
}, [navigate, account, canOrders, canDashboard, canStores]);
|
}, [navigate, account, canOrders, canDashboard, canStores]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
void loadHome();
|
||||||
|
}, [loadHome]);
|
||||||
|
|
||||||
const storeCount = Number(dash?.storeCount || stores.length || 0);
|
const storeCount = Number(dash?.storeCount || stores.length || 0);
|
||||||
const orderStats = useMemo(() => summarizeOrders(orders), [orders]);
|
const orderStats = useMemo(() => summarizeOrders(orders), [orders]);
|
||||||
const pendingAuditCount = Number(dash?.pendingAuditCount || 0);
|
const pendingAuditCount = Number(dash?.pendingAuditCount || 0);
|
||||||
@@ -219,7 +237,7 @@ export default function HomePage() {
|
|||||||
const profit = revenue * 0.25;
|
const profit = revenue * 0.25;
|
||||||
|
|
||||||
return (
|
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">
|
<main className="partner-home-body">
|
||||||
{isPrimary && (
|
{isPrimary && (
|
||||||
<>
|
<>
|
||||||
@@ -346,6 +364,6 @@ export default function HomePage() {
|
|||||||
|
|
||||||
<LeaderboardPreview entries={leaderboardEntries} />
|
<LeaderboardPreview entries={leaderboardEntries} />
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</PullToRefresh>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
import { useNavigate, Link } from 'react-router-dom';
|
import { useNavigate, Link } from 'react-router-dom';
|
||||||
import type { PartnerLeaderboardPeriod, PartnerLeaderboardResponse } from '@dukang/shared-types';
|
import type { PartnerLeaderboardPeriod, PartnerLeaderboardResponse } from '@dukang/shared-types';
|
||||||
import PageHeader from '@dukang/shared-ui/PageHeader';
|
import PageHeader from '@dukang/shared-ui/PageHeader';
|
||||||
|
import PullToRefresh from '@dukang/shared-ui/PullToRefresh';
|
||||||
import { fetchPartnerLeaderboard } from '../lib/leaderboard';
|
import { fetchPartnerLeaderboard } from '../lib/leaderboard';
|
||||||
import { usePartnerSession } from '../contexts/PartnerSessionContext';
|
import { usePartnerSession } from '../contexts/PartnerSessionContext';
|
||||||
import { isPrimaryAccount } from '../lib/partnerAccess';
|
import { isPrimaryAccount } from '../lib/partnerAccess';
|
||||||
@@ -33,10 +34,10 @@ export default function LeaderboardPage() {
|
|||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
|
|
||||||
useEffect(() => {
|
const loadLeaderboard = useCallback(() => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError('');
|
setError('');
|
||||||
fetchPartnerLeaderboard(period)
|
return fetchPartnerLeaderboard(period)
|
||||||
.then(setData)
|
.then(setData)
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
setData(null);
|
setData(null);
|
||||||
@@ -45,8 +46,12 @@ export default function LeaderboardPage() {
|
|||||||
.finally(() => setLoading(false));
|
.finally(() => setLoading(false));
|
||||||
}, [period]);
|
}, [period]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
void loadLeaderboard();
|
||||||
|
}, [loadLeaderboard]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="page-no-tab">
|
<PullToRefresh onRefresh={loadLeaderboard} className="page-no-tab">
|
||||||
<PageHeader title="团队贡献榜" onBack={() => navigate('/')} />
|
<PageHeader title="团队贡献榜" onBack={() => navigate('/')} />
|
||||||
|
|
||||||
<div className="partner-leaderboard-tabs">
|
<div className="partner-leaderboard-tabs">
|
||||||
@@ -124,6 +129,6 @@ export default function LeaderboardPage() {
|
|||||||
<span className="material-symbols-outlined">add</span>
|
<span className="material-symbols-outlined">add</span>
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
</div>
|
</PullToRefresh>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 { Link, useNavigate } from 'react-router-dom';
|
||||||
import PageHeader from '@dukang/shared-ui/PageHeader';
|
import PageHeader from '@dukang/shared-ui/PageHeader';
|
||||||
|
import PullToRefresh from '@dukang/shared-ui/PullToRefresh';
|
||||||
import { isLoggedIn, request } from '../lib/api';
|
import { isLoggedIn, request } from '../lib/api';
|
||||||
import { usePartnerSession } from '../contexts/PartnerSessionContext';
|
import { usePartnerSession } from '../contexts/PartnerSessionContext';
|
||||||
import { hasWarehouseAccess } from '../lib/partnerAccess';
|
import { hasWarehouseAccess } from '../lib/partnerAccess';
|
||||||
@@ -59,14 +60,17 @@ export default function OrderListPage({ tabRoot = false }: OrderListPageProps) {
|
|||||||
document.title = '订单管理';
|
document.title = '订单管理';
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
const loadOrders = useCallback(() => {
|
||||||
if (!isLoggedIn()) { navigate('/login'); return; }
|
if (!isLoggedIn()) {
|
||||||
|
navigate('/login');
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
if (!warehouseOk) {
|
if (!warehouseOk) {
|
||||||
setData({ list: [], hasWarehouseAccess: false, message: '未配置仓库管理权限' });
|
setData({ list: [], hasWarehouseAccess: false, message: '未配置仓库管理权限' });
|
||||||
setLoaded(true);
|
setLoaded(true);
|
||||||
return;
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
request<OrdersResponse>('PARTNER_H5', '/partner/orders')
|
return request<OrdersResponse>('PARTNER_H5', '/partner/orders')
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
setData({
|
setData({
|
||||||
list: Array.isArray(res.list) ? res.list : [],
|
list: Array.isArray(res.list) ? res.list : [],
|
||||||
@@ -78,6 +82,10 @@ export default function OrderListPage({ tabRoot = false }: OrderListPageProps) {
|
|||||||
.finally(() => setLoaded(true));
|
.finally(() => setLoaded(true));
|
||||||
}, [navigate, warehouseOk]);
|
}, [navigate, warehouseOk]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
void loadOrders();
|
||||||
|
}, [loadOrders]);
|
||||||
|
|
||||||
const filtered = useMemo(() => data.list.filter((o) => {
|
const filtered = useMemo(() => data.list.filter((o) => {
|
||||||
if (statusFilter === 'ALL') return true;
|
if (statusFilter === 'ALL') return true;
|
||||||
const s = String(o.status).toUpperCase();
|
const s = String(o.status).toUpperCase();
|
||||||
@@ -142,7 +150,7 @@ export default function OrderListPage({ tabRoot = false }: OrderListPageProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
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('/')} />}
|
{!tabRoot && <PageHeader title="订单中心" onBack={() => navigate('/')} />}
|
||||||
|
|
||||||
<div className="partner-segment">
|
<div className="partner-segment">
|
||||||
@@ -265,6 +273,6 @@ export default function OrderListPage({ tabRoot = false }: OrderListPageProps) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</PullToRefresh>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import PageHeader from '@dukang/shared-ui/PageHeader';
|
import PageHeader from '@dukang/shared-ui/PageHeader';
|
||||||
|
import PullToRefresh from '@dukang/shared-ui/PullToRefresh';
|
||||||
import type { PartnerBillDto } from '@dukang/shared-types';
|
import type { PartnerBillDto } from '@dukang/shared-types';
|
||||||
import { isLoggedIn, request } from '../lib/api';
|
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 [month, setMonth] = useState({ year: now.getFullYear(), month: now.getMonth() + 1 });
|
||||||
const [statusFilter, setStatusFilter] = useState<StatusFilter>('all');
|
const [statusFilter, setStatusFilter] = useState<StatusFilter>('all');
|
||||||
|
|
||||||
|
const loadBills = useCallback(() => {
|
||||||
|
return request<PartnerBillDto[]>('PARTNER_H5', '/partner/settlement/bills').then(setBills);
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isLoggedIn()) { navigate('/login'); return; }
|
if (!isLoggedIn()) { navigate('/login'); return; }
|
||||||
request<PartnerBillDto[]>('PARTNER_H5', '/partner/settlement/bills').then(setBills);
|
void loadBills();
|
||||||
}, [navigate]);
|
}, [navigate, loadBills]);
|
||||||
|
|
||||||
const summary = useMemo(() => {
|
const summary = useMemo(() => {
|
||||||
const pending = bills.filter((b) => b.status === 'DRAFT');
|
const pending = bills.filter((b) => b.status === 'DRAFT');
|
||||||
@@ -102,7 +107,7 @@ export default function SettlementPage() {
|
|||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="page-no-tab partner-settlement-page">
|
<PullToRefresh onRefresh={loadBills} className="page-no-tab partner-settlement-page">
|
||||||
<PageHeader title="财务对账中心" onBack={() => navigate('/')} />
|
<PageHeader title="财务对账中心" onBack={() => navigate('/')} />
|
||||||
|
|
||||||
<main className="partner-settlement-body">
|
<main className="partner-settlement-body">
|
||||||
@@ -211,6 +216,6 @@ export default function SettlementPage() {
|
|||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</PullToRefresh>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import { Link, useNavigate } from 'react-router-dom';
|
import { Link, useNavigate } from 'react-router-dom';
|
||||||
import PageHeader from '@dukang/shared-ui/PageHeader';
|
import PageHeader from '@dukang/shared-ui/PageHeader';
|
||||||
|
import PullToRefresh from '@dukang/shared-ui/PullToRefresh';
|
||||||
import {
|
import {
|
||||||
AccountStatus,
|
AccountStatus,
|
||||||
PARTNER_STAFF_ROLE_LABELS,
|
PARTNER_STAFF_ROLE_LABELS,
|
||||||
@@ -86,7 +87,7 @@ export default function StaffListPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="page-no-tab partner-staff-page">
|
<PullToRefresh onRefresh={loadStaff} className="page-no-tab partner-staff-page">
|
||||||
<PageHeader title="子账号管理" onBack={() => navigate('/center')} />
|
<PageHeader title="子账号管理" onBack={() => navigate('/center')} />
|
||||||
|
|
||||||
<div style={{ padding: '0 20px 16px' }}>
|
<div style={{ padding: '0 20px 16px' }}>
|
||||||
@@ -151,6 +152,6 @@ export default function StaffListPage() {
|
|||||||
添加子账号
|
添加子账号
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</PullToRefresh>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import { Link, useNavigate, useSearchParams } from 'react-router-dom';
|
import { Link, useNavigate, useSearchParams } from 'react-router-dom';
|
||||||
|
import PullToRefresh from '@dukang/shared-ui/PullToRefresh';
|
||||||
import { isLoggedIn, request } from '../lib/api';
|
import { isLoggedIn, request } from '../lib/api';
|
||||||
import { usePartnerSession } from '../contexts/PartnerSessionContext';
|
import { usePartnerSession } from '../contexts/PartnerSessionContext';
|
||||||
import { canCreatePartnerStore, canManagePartnerStore } from '../lib/partnerAccess';
|
import { canCreatePartnerStore, canManagePartnerStore } from '../lib/partnerAccess';
|
||||||
@@ -85,7 +86,7 @@ export default function StoreListPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
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>}
|
{error && <p className="partner-form-error" role="alert" style={{ margin: '0 20px 12px' }}>{error}</p>}
|
||||||
|
|
||||||
<div className="partner-sticky-filter">
|
<div className="partner-sticky-filter">
|
||||||
@@ -188,6 +189,6 @@ export default function StoreListPage() {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</PullToRefresh>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import type { PartnerWeeklyReportResponse } from '@dukang/shared-types';
|
import type { PartnerWeeklyReportResponse } from '@dukang/shared-types';
|
||||||
import PageHeader from '@dukang/shared-ui/PageHeader';
|
import PageHeader from '@dukang/shared-ui/PageHeader';
|
||||||
|
import PullToRefresh from '@dukang/shared-ui/PullToRefresh';
|
||||||
import { fetchPartnerWeeklyReport } from '../lib/weeklyReport';
|
import { fetchPartnerWeeklyReport } from '../lib/weeklyReport';
|
||||||
|
|
||||||
function fmtMoney(n: number) {
|
function fmtMoney(n: number) {
|
||||||
@@ -20,10 +21,10 @@ export default function WeeklyReportPage() {
|
|||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
|
|
||||||
useEffect(() => {
|
const loadWeeklyReport = useCallback(() => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError('');
|
setError('');
|
||||||
fetchPartnerWeeklyReport(selectedStart)
|
return fetchPartnerWeeklyReport(selectedStart)
|
||||||
.then(setData)
|
.then(setData)
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
setData(null);
|
setData(null);
|
||||||
@@ -32,6 +33,10 @@ export default function WeeklyReportPage() {
|
|||||||
.finally(() => setLoading(false));
|
.finally(() => setLoading(false));
|
||||||
}, [selectedStart]);
|
}, [selectedStart]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
void loadWeeklyReport();
|
||||||
|
}, [loadWeeklyReport]);
|
||||||
|
|
||||||
const maxDailyGmv = useMemo(() => {
|
const maxDailyGmv = useMemo(() => {
|
||||||
if (!data?.dailyGmv.length) return 1;
|
if (!data?.dailyGmv.length) return 1;
|
||||||
return Math.max(1, ...data.dailyGmv.map((item) => item.amount));
|
return Math.max(1, ...data.dailyGmv.map((item) => item.amount));
|
||||||
@@ -42,7 +47,7 @@ export default function WeeklyReportPage() {
|
|||||||
const growthPositive = (summary?.gmvGrowthPercent ?? 0) >= 0;
|
const growthPositive = (summary?.gmvGrowthPercent ?? 0) >= 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="page-no-tab">
|
<PullToRefresh onRefresh={loadWeeklyReport} className="page-no-tab">
|
||||||
<PageHeader title="数据周报" onBack={() => navigate('/')} />
|
<PageHeader title="数据周报" onBack={() => navigate('/')} />
|
||||||
|
|
||||||
<main className="partner-weekly-page">
|
<main className="partner-weekly-page">
|
||||||
@@ -211,6 +216,6 @@ export default function WeeklyReportPage() {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</PullToRefresh>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
import { Link, useNavigate } from 'react-router-dom';
|
import { Link, useNavigate } from 'react-router-dom';
|
||||||
|
import PullToRefresh from '@dukang/shared-ui/PullToRefresh';
|
||||||
import { request } from '../lib/api';
|
import { request } from '../lib/api';
|
||||||
|
|
||||||
function formatMoney(n: number) {
|
function formatMoney(n: number) {
|
||||||
@@ -47,7 +48,7 @@ export default function HomePage() {
|
|||||||
const closeTime = String(store?.closeTime || '22:00');
|
const closeTime = String(store?.closeTime || '22:00');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="shop-home-page">
|
<PullToRefresh onRefresh={loadDashboard} className="shop-home-page">
|
||||||
<header className="shop-home-header">
|
<header className="shop-home-header">
|
||||||
<h1 className="app-page-title">门店管理中心</h1>
|
<h1 className="app-page-title">门店管理中心</h1>
|
||||||
</header>
|
</header>
|
||||||
@@ -127,6 +128,6 @@ export default function HomePage() {
|
|||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</PullToRefresh>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||||
import { isWxAuthorizeEnabled } from '@dukang/shared-types';
|
import { isWxAuthorizeEnabled } from '@dukang/shared-types';
|
||||||
|
import PullToRefresh from '@dukang/shared-ui/PullToRefresh';
|
||||||
import { stripOAuthParamsFromLocation } from '@dukang/weixin-sdk';
|
import { stripOAuthParamsFromLocation } from '@dukang/weixin-sdk';
|
||||||
import { useStoreSession } from '../contexts/StoreSessionContext';
|
import { useStoreSession } from '../contexts/StoreSessionContext';
|
||||||
import { getStoreProfile, request } from '../lib/api';
|
import { getStoreProfile, request } from '../lib/api';
|
||||||
@@ -24,21 +25,21 @@ export default function MinePage() {
|
|||||||
const [binding, setBinding] = useState(false);
|
const [binding, setBinding] = useState(false);
|
||||||
const [bindMsg, setBindMsg] = useState('');
|
const [bindMsg, setBindMsg] = useState('');
|
||||||
|
|
||||||
useEffect(() => {
|
const loadMine = useCallback(() => {
|
||||||
request('SHOP_H5', '/shop/store').then(setStore).catch(() => setStore(null));
|
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(() => {
|
useEffect(() => {
|
||||||
fetchClientConfig()
|
void loadMine();
|
||||||
.then((config) => setWxAuthorize(isWxAuthorizeEnabled(config)))
|
}, [loadMine]);
|
||||||
.catch(() => setWxAuthorize(false));
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetchShopAccount()
|
|
||||||
.then((me) => setHasWechat(!!(me.hasWechat || me.wxOpenId)))
|
|
||||||
.catch(() => setHasWechat(null));
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isWechatEnv() || !wxAuthorize || !searchParams.get('code')) return;
|
if (!isWechatEnv() || !wxAuthorize || !searchParams.get('code')) return;
|
||||||
@@ -92,7 +93,7 @@ export default function MinePage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="shop-mine-page">
|
<PullToRefresh onRefresh={loadMine} className="shop-mine-page">
|
||||||
<header className="shop-mine-header">
|
<header className="shop-mine-header">
|
||||||
<h1 className="app-page-title">我的</h1>
|
<h1 className="app-page-title">我的</h1>
|
||||||
</header>
|
</header>
|
||||||
@@ -174,6 +175,6 @@ export default function MinePage() {
|
|||||||
退出登录
|
退出登录
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</PullToRefresh>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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';
|
import { request } from '../lib/api';
|
||||||
|
|
||||||
type RangeKey = 'today' | '7d' | '30d';
|
type RangeKey = 'today' | '7d' | '30d';
|
||||||
@@ -28,15 +29,21 @@ export default function RecordsPage() {
|
|||||||
const [statusFilter, setStatusFilter] = useState<StatusFilter>('all');
|
const [statusFilter, setStatusFilter] = useState<StatusFilter>('all');
|
||||||
const [storeName, setStoreName] = useState('');
|
const [storeName, setStoreName] = useState('');
|
||||||
|
|
||||||
useEffect(() => {
|
const loadRecords = useCallback(() => {
|
||||||
request<{ list: Array<Record<string, unknown>> }>('SHOP_H5', '/shop/redeem/records').then((d) => {
|
return Promise.all([
|
||||||
setRecords(d.list || []);
|
request<{ list: Array<Record<string, unknown>> }>('SHOP_H5', '/shop/redeem/records').then((d) => {
|
||||||
});
|
setRecords(d.list || []);
|
||||||
request<Record<string, unknown>>('SHOP_H5', '/shop/store')
|
}),
|
||||||
.then((s) => setStoreName(String(s.name || '')))
|
request<Record<string, unknown>>('SHOP_H5', '/shop/store')
|
||||||
.catch(() => {});
|
.then((s) => setStoreName(String(s.name || '')))
|
||||||
|
.catch(() => {}),
|
||||||
|
]);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
void loadRecords();
|
||||||
|
}, [loadRecords]);
|
||||||
|
|
||||||
const filtered = useMemo(() => {
|
const filtered = useMemo(() => {
|
||||||
return records.filter((r) => {
|
return records.filter((r) => {
|
||||||
if (!inRange(String(r.createdAt), range)) return false;
|
if (!inRange(String(r.createdAt), range)) return false;
|
||||||
@@ -55,7 +62,7 @@ export default function RecordsPage() {
|
|||||||
}, [filtered]);
|
}, [filtered]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="shop-records-page">
|
<PullToRefresh onRefresh={loadRecords} className="shop-records-page">
|
||||||
<header className="shop-records-header">
|
<header className="shop-records-header">
|
||||||
<h1 className="app-page-title">核销记录</h1>
|
<h1 className="app-page-title">核销记录</h1>
|
||||||
</header>
|
</header>
|
||||||
@@ -176,6 +183,6 @@ export default function RecordsPage() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</PullToRefresh>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import PullToRefresh from '@dukang/shared-ui/PullToRefresh';
|
||||||
import { useStoreSession } from '../contexts/StoreSessionContext';
|
import { useStoreSession } from '../contexts/StoreSessionContext';
|
||||||
import {
|
import {
|
||||||
needsStoreSelection,
|
needsStoreSelection,
|
||||||
@@ -19,15 +20,19 @@ export default function SelectStorePage() {
|
|||||||
const currentStoreId = store?.storeId || '';
|
const currentStoreId = store?.storeId || '';
|
||||||
const canGoBack = Boolean(currentStoreId);
|
const canGoBack = Boolean(currentStoreId);
|
||||||
|
|
||||||
|
const loadStores = useCallback(() => {
|
||||||
|
return request<ShopStoreOption[]>('SHOP_H5', '/shop/auth/stores')
|
||||||
|
.then((list) => setStores(list))
|
||||||
|
.catch((e) => setMsg(e instanceof Error ? e.message : '加载门店失败'));
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!authenticated) {
|
if (!authenticated) {
|
||||||
navigate('/login', { replace: true });
|
navigate('/login', { replace: true });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
void request<ShopStoreOption[]>('SHOP_H5', '/shop/auth/stores')
|
void loadStores();
|
||||||
.then((list) => setStores(list))
|
}, [authenticated, navigate, loadStores]);
|
||||||
.catch((e) => setMsg(e instanceof Error ? e.message : '加载门店失败'));
|
|
||||||
}, [authenticated, navigate]);
|
|
||||||
|
|
||||||
async function onSelect(storeId: string) {
|
async function onSelect(storeId: string) {
|
||||||
if (loadingId) return;
|
if (loadingId) return;
|
||||||
@@ -63,7 +68,7 @@ export default function SelectStorePage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="shop-select-store-page">
|
<PullToRefresh onRefresh={loadStores} className="shop-select-store-page">
|
||||||
<header className="shop-subpage-header">
|
<header className="shop-subpage-header">
|
||||||
{canGoBack ? (
|
{canGoBack ? (
|
||||||
<button
|
<button
|
||||||
@@ -151,7 +156,7 @@ export default function SelectStorePage() {
|
|||||||
) : null}
|
) : null}
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</PullToRefresh>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { STORE_STAFF_ROLE_LABELS, type StoreStaffRole } from '@dukang/shared-types';
|
import { STORE_STAFF_ROLE_LABELS, type StoreStaffRole } from '@dukang/shared-types';
|
||||||
|
import PullToRefresh from '@dukang/shared-ui/PullToRefresh';
|
||||||
import { useStoreSession } from '../contexts/StoreSessionContext';
|
import { useStoreSession } from '../contexts/StoreSessionContext';
|
||||||
import { getStoreProfile, request } from '../lib/api';
|
import { getStoreProfile, request } from '../lib/api';
|
||||||
|
|
||||||
@@ -116,7 +117,7 @@ export default function StaffPage() {
|
|||||||
form.storeIds.length === 0 ? ownedStores.length : form.storeIds.length;
|
form.storeIds.length === 0 ? ownedStores.length : form.storeIds.length;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="shop-staff-page">
|
<PullToRefresh onRefresh={reload} className="shop-staff-page">
|
||||||
<header className="shop-subpage-header">
|
<header className="shop-subpage-header">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -322,6 +323,6 @@ export default function StaffPage() {
|
|||||||
</section>
|
</section>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</PullToRefresh>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
export default definePageConfig({
|
export default definePageConfig({
|
||||||
navigationStyle: 'custom',
|
navigationStyle: 'custom',
|
||||||
navigationBarTitleText: '地址管理',
|
navigationBarTitleText: '地址管理',
|
||||||
|
enablePullDownRefresh: true,
|
||||||
|
backgroundTextStyle: 'dark',
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useCallback, useState } from 'react';
|
import { useCallback, useState } from 'react';
|
||||||
import { View, Text } from '@tarojs/components';
|
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 PageShell from '../../components/PageShell';
|
||||||
import SubPageHeader from '../../components/SubPageHeader';
|
import SubPageHeader from '../../components/SubPageHeader';
|
||||||
import {
|
import {
|
||||||
@@ -44,6 +44,10 @@ export default function AddressesPage() {
|
|||||||
loadList();
|
loadList();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
usePullDownRefresh(() => {
|
||||||
|
void Promise.resolve(loadList()).finally(() => Taro.stopPullDownRefresh());
|
||||||
|
});
|
||||||
|
|
||||||
function selectAddress(addr: Address) {
|
function selectAddress(addr: Address) {
|
||||||
if (!selectMode) return;
|
if (!selectMode) return;
|
||||||
Taro.redirectTo({
|
Taro.redirectTo({
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
export default definePageConfig({
|
export default definePageConfig({
|
||||||
navigationStyle: 'custom',
|
navigationStyle: 'custom',
|
||||||
navigationBarTitleText: '好客权益',
|
navigationBarTitleText: '好客权益',
|
||||||
|
enablePullDownRefresh: true,
|
||||||
|
backgroundTextStyle: 'dark',
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
import { View, Text } from '@tarojs/components';
|
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 PageShell from '../../components/PageShell';
|
||||||
import UserTabBar, { shouldRenderPageTabBar, syncTabBarSelected } from '../../components/UserTabBar';
|
import UserTabBar, { shouldRenderPageTabBar, syncTabBarSelected } from '../../components/UserTabBar';
|
||||||
import { goLogin } from '../../lib/auth-nav';
|
import { goLogin } from '../../lib/auth-nav';
|
||||||
@@ -44,9 +44,9 @@ export default function BenefitPage() {
|
|||||||
syncTabBarSelected(2);
|
syncTabBarSelected(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
const loadBenefit = useCallback(() => {
|
||||||
if (!loggedIn) return;
|
if (!loggedIn) return Promise.resolve();
|
||||||
Promise.all([
|
return Promise.all([
|
||||||
request<BenefitSummary>('/benefit/summary'),
|
request<BenefitSummary>('/benefit/summary'),
|
||||||
request<CouponItem[]>('/benefit/coupons'),
|
request<CouponItem[]>('/benefit/coupons'),
|
||||||
])
|
])
|
||||||
@@ -57,6 +57,14 @@ export default function BenefitPage() {
|
|||||||
.catch((e) => toast(e instanceof Error ? e.message : '加载失败'));
|
.catch((e) => toast(e instanceof Error ? e.message : '加载失败'));
|
||||||
}, [loggedIn]);
|
}, [loggedIn]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
void loadBenefit();
|
||||||
|
}, [loadBenefit]);
|
||||||
|
|
||||||
|
usePullDownRefresh(() => {
|
||||||
|
void loadBenefit().finally(() => Taro.stopPullDownRefresh());
|
||||||
|
});
|
||||||
|
|
||||||
const available = coupons.filter((c) => c.status === 'ACTIVE' && c.balance > 0);
|
const available = coupons.filter((c) => c.status === 'ACTIVE' && c.balance > 0);
|
||||||
const history = coupons.filter((c) => c.status === 'USED_UP' || c.status === 'VOID');
|
const history = coupons.filter((c) => c.status === 'USED_UP' || c.status === 'VOID');
|
||||||
const visible = tab === 'available' ? available : history;
|
const visible = tab === 'available' ? available : history;
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
export default definePageConfig({
|
export default definePageConfig({
|
||||||
navigationBarTitleText: '杜康好客',
|
navigationBarTitleText: '杜康好客',
|
||||||
|
enablePullDownRefresh: true,
|
||||||
|
backgroundTextStyle: 'dark',
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import { View, Text } from '@tarojs/components';
|
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 PageShell from '../../components/PageShell';
|
||||||
import TabMainHeader from '../../components/TabMainHeader';
|
import TabMainHeader from '../../components/TabMainHeader';
|
||||||
import CouponBadge from '../../components/CouponBadge';
|
import CouponBadge from '../../components/CouponBadge';
|
||||||
@@ -41,14 +41,39 @@ export default function HomePage() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
const loadProducts = useCallback(() => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
request<Product[]>(`/catalog/products?cityCode=${encodeURIComponent(cityCode)}`)
|
return request<Product[]>(`/catalog/products?cityCode=${encodeURIComponent(cityCode)}`)
|
||||||
.then((list) => setProducts(Array.isArray(list) ? list : []))
|
.then((list) => setProducts(Array.isArray(list) ? list : []))
|
||||||
.catch((e) => toast(e instanceof Error ? e.message : '加载失败'))
|
.catch((e) => toast(e instanceof Error ? e.message : '加载失败'))
|
||||||
.finally(() => setLoading(false));
|
.finally(() => setLoading(false));
|
||||||
}, [cityCode]);
|
}, [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<Product[]>(
|
||||||
|
`/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(
|
const availableAromas = useMemo(
|
||||||
() =>
|
() =>
|
||||||
AROMA_TABS.filter((item) =>
|
AROMA_TABS.filter((item) =>
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
export default definePageConfig({
|
export default definePageConfig({
|
||||||
navigationBarTitleText: '我的',
|
navigationBarTitleText: '我的',
|
||||||
|
enablePullDownRefresh: true,
|
||||||
|
backgroundTextStyle: 'dark',
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { View, Text, Image, Button, Input } from '@tarojs/components';
|
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 { isWxAuthorizeEnabled, type ClientRuntimeConfig } from '@dukang/shared-types';
|
||||||
import PageShell from '../../components/PageShell';
|
import PageShell from '../../components/PageShell';
|
||||||
import TabMainHeader from '../../components/TabMainHeader';
|
import TabMainHeader from '../../components/TabMainHeader';
|
||||||
@@ -64,9 +64,9 @@ export default function MinePage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function loadProfile() {
|
function loadProfile() {
|
||||||
if (!isLoggedIn()) return;
|
if (!isLoggedIn()) return Promise.resolve();
|
||||||
setProfileLoadError('');
|
setProfileLoadError('');
|
||||||
Promise.all([
|
return Promise.all([
|
||||||
request<UserProfile>('/auth/me'),
|
request<UserProfile>('/auth/me'),
|
||||||
request<Array<Record<string, unknown>>>('/benefit/coupons').catch(() => []),
|
request<Array<Record<string, unknown>>>('/benefit/coupons').catch(() => []),
|
||||||
...ORDER_SHORTCUTS.map((s) =>
|
...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(() => {
|
useEffect(() => {
|
||||||
request<ClientRuntimeConfig>('/common/client-config')
|
request<ClientRuntimeConfig>('/common/client-config')
|
||||||
.then((config) => setWxAuthorize(isWxAuthorizeEnabled(config)))
|
.then((config) => setWxAuthorize(isWxAuthorizeEnabled(config)))
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
export default definePageConfig({
|
export default definePageConfig({
|
||||||
navigationStyle: 'custom',
|
navigationStyle: 'custom',
|
||||||
navigationBarTitleText: '我的订单',
|
navigationBarTitleText: '我的订单',
|
||||||
|
enablePullDownRefresh: true,
|
||||||
|
backgroundTextStyle: 'dark',
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
import { View, Text } from '@tarojs/components';
|
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 PageShell from '../../components/PageShell';
|
||||||
import SubPageHeader from '../../components/SubPageHeader';
|
import SubPageHeader from '../../components/SubPageHeader';
|
||||||
import { request, toast } from '../../lib/api';
|
import { request, toast } from '../../lib/api';
|
||||||
@@ -47,9 +47,9 @@ export default function OrdersPage() {
|
|||||||
const [orders, setOrders] = useState<OrderRow[]>([]);
|
const [orders, setOrders] = useState<OrderRow[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
useEffect(() => {
|
const loadOrders = useCallback(() => {
|
||||||
setLoading(true);
|
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`,
|
`/trade/orders?tab=${encodeURIComponent(tab)}&pageSize=20`,
|
||||||
)
|
)
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
@@ -63,6 +63,14 @@ export default function OrdersPage() {
|
|||||||
.finally(() => setLoading(false));
|
.finally(() => setLoading(false));
|
||||||
}, [tab]);
|
}, [tab]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
void loadOrders();
|
||||||
|
}, [loadOrders]);
|
||||||
|
|
||||||
|
usePullDownRefresh(() => {
|
||||||
|
void loadOrders().finally(() => Taro.stopPullDownRefresh());
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageShell variant="sub" className="orders-page">
|
<PageShell variant="sub" className="orders-page">
|
||||||
<SubPageHeader title="我的订单" />
|
<SubPageHeader title="我的订单" />
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
export default definePageConfig({
|
export default definePageConfig({
|
||||||
navigationBarTitleText: '门店',
|
navigationBarTitleText: '门店',
|
||||||
|
enablePullDownRefresh: true,
|
||||||
|
backgroundTextStyle: 'dark',
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
import { View, Text, Image, Input } from '@tarojs/components';
|
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 PageShell from '../../components/PageShell';
|
||||||
import TabMainHeader from '../../components/TabMainHeader';
|
import TabMainHeader from '../../components/TabMainHeader';
|
||||||
import RegionPicker from '../../components/RegionPicker';
|
import RegionPicker from '../../components/RegionPicker';
|
||||||
@@ -49,15 +49,39 @@ export default function StoresPage() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
const loadStores = useCallback(() => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const path = cityCode ? `/stores?cityCode=${encodeURIComponent(cityCode)}` : '/stores';
|
const path = cityCode ? `/stores?cityCode=${encodeURIComponent(cityCode)}` : '/stores';
|
||||||
request<Store[]>(path)
|
return request<Store[]>(path)
|
||||||
.then((list) => setStores(Array.isArray(list) ? list : []))
|
.then((list) => setStores(Array.isArray(list) ? list : []))
|
||||||
.catch((e) => toast(e instanceof Error ? e.message : '加载失败'))
|
.catch((e) => toast(e instanceof Error ? e.message : '加载失败'))
|
||||||
.finally(() => setLoading(false));
|
.finally(() => setLoading(false));
|
||||||
}, [cityCode]);
|
}, [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<Store[]>(path);
|
||||||
|
setStores(Array.isArray(list) ? list : []);
|
||||||
|
} catch (e) {
|
||||||
|
toast(e instanceof Error ? e.message : '加载失败');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
Taro.stopPullDownRefresh();
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
});
|
||||||
|
|
||||||
const filtered = stores.filter((s) => {
|
const filtered = stores.filter((s) => {
|
||||||
if (!matchesRegionFilter(s, region)) return false;
|
if (!matchesRegionFilter(s, region)) return false;
|
||||||
if (!keyword.trim()) return true;
|
if (!keyword.trim()) return true;
|
||||||
|
|||||||
@@ -10,7 +10,8 @@
|
|||||||
"./PageHeader": "./src/PageHeader.tsx",
|
"./PageHeader": "./src/PageHeader.tsx",
|
||||||
"./CouponBadge": "./src/CouponBadge.tsx",
|
"./CouponBadge": "./src/CouponBadge.tsx",
|
||||||
"./OrderStatusTabs": "./src/OrderStatusTabs.tsx",
|
"./OrderStatusTabs": "./src/OrderStatusTabs.tsx",
|
||||||
"./AppImage": "./src/AppImage.tsx"
|
"./AppImage": "./src/AppImage.tsx",
|
||||||
|
"./PullToRefresh": "./src/PullToRefresh.tsx"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"react": "^18.3.1"
|
"react": "^18.3.1"
|
||||||
|
|||||||
@@ -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<void>;
|
||||||
|
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<HTMLDivElement>(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 (
|
||||||
|
<div
|
||||||
|
ref={rootRef}
|
||||||
|
className={['dukang-ptr', className].filter(Boolean).join(' ')}
|
||||||
|
style={{ ...style, paddingTop: pad || undefined }}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={`dukang-ptr-indicator${showHint ? ' dukang-ptr-indicator--visible' : ''}`}
|
||||||
|
style={{ height: pad }}
|
||||||
|
aria-live="polite"
|
||||||
|
>
|
||||||
|
<span className="dukang-ptr-text">
|
||||||
|
{refreshing ? '刷新中…' : ready ? '松开刷新' : '下拉刷新'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user