feat(mp): 小程序统一组件库 Phase 1 — Token 扩展 + 10 组件 + useListPage Hook
三层架构组件库: - 第 1 层原子组件:PageShell/ContentCard/StatusTag/SectionTitle/LoadingCard - 第 2 层组合模式:PageHeader/SearchSection/CardList/PaginationBar - 第 3 层 Hook:useListPage(列表页通用逻辑抽象) Token 扩展:新增 --tk-card-*/--tk-gap-*/--tk-page-* 等结构化 CSS 变量, 关怀模式通过变量覆写自动生效,新组件零额外代码即获关怀支持。 设计规格:docs/superpowers/specs/2026-05-16-miniprogram-unified-components-design.md
This commit is contained in:
163
apps/miniprogram/src/hooks/useListPage.ts
Normal file
163
apps/miniprogram/src/hooks/useListPage.ts
Normal file
@@ -0,0 +1,163 @@
|
||||
import { useState, useCallback, useEffect, useRef, useMemo } from 'react';
|
||||
|
||||
interface UseListPageConfig<T> {
|
||||
fetchFn: (params: {
|
||||
page: number;
|
||||
pageSize: number;
|
||||
keyword: string;
|
||||
filter?: string;
|
||||
}) => Promise<{ items: T[]; total: number }>;
|
||||
pageSize?: number;
|
||||
autoLoad?: boolean;
|
||||
}
|
||||
|
||||
interface UseListPageReturn<T> {
|
||||
items: T[];
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
isEmpty: boolean;
|
||||
page: number;
|
||||
total: number;
|
||||
pageSize: number;
|
||||
keyword: string;
|
||||
setKeyword: (v: string) => void;
|
||||
filter: string | undefined;
|
||||
setFilter: (v: string | undefined) => void;
|
||||
refresh: () => void;
|
||||
listPageProps: {
|
||||
items: T[];
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
isEmpty: boolean;
|
||||
searchProps: {
|
||||
value: string;
|
||||
onChange: (v: string) => void;
|
||||
onSearch: () => void;
|
||||
};
|
||||
paginationProps: {
|
||||
current: number;
|
||||
total: number;
|
||||
pageSize: number;
|
||||
onChange: (page: number) => void;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export function useListPage<T>(
|
||||
config: UseListPageConfig<T>,
|
||||
): UseListPageReturn<T> {
|
||||
const { fetchFn, pageSize = 10, autoLoad = true } = config;
|
||||
|
||||
const [items, setItems] = useState<T[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [page, setPage] = useState(1);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [keyword, setKeywordState] = useState('');
|
||||
const [filter, setFilterState] = useState<string | undefined>(undefined);
|
||||
|
||||
const debounceRef = useRef<ReturnType<typeof setTimeout>>();
|
||||
const mountedRef = useRef(false);
|
||||
|
||||
const isEmpty = !loading && items.length === 0;
|
||||
|
||||
const fetchData = useCallback(
|
||||
async (targetPage: number, kw: string, ft?: string) => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const result = await fetchFn({
|
||||
page: targetPage,
|
||||
pageSize,
|
||||
keyword: kw,
|
||||
filter: ft,
|
||||
});
|
||||
setItems(result.items);
|
||||
setTotal(result.total);
|
||||
setPage(targetPage);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : '加载失败');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
},
|
||||
[fetchFn, pageSize],
|
||||
);
|
||||
|
||||
const refresh = useCallback(() => {
|
||||
fetchData(page, keyword, filter);
|
||||
}, [fetchData, page, keyword, filter]);
|
||||
|
||||
const handleKeywordChange = useCallback(
|
||||
(v: string) => {
|
||||
setKeywordState(v);
|
||||
if (debounceRef.current) clearTimeout(debounceRef.current);
|
||||
debounceRef.current = setTimeout(() => {
|
||||
fetchData(1, v, filter);
|
||||
}, 300);
|
||||
},
|
||||
[fetchData, filter],
|
||||
);
|
||||
|
||||
const handleFilterChange = useCallback(
|
||||
(v: string | undefined) => {
|
||||
setFilterState(v);
|
||||
fetchData(1, keyword, v);
|
||||
},
|
||||
[fetchData, keyword],
|
||||
);
|
||||
|
||||
const handlePageChange = useCallback(
|
||||
(p: number) => {
|
||||
fetchData(p, keyword, filter);
|
||||
},
|
||||
[fetchData, keyword, filter],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (autoLoad && !mountedRef.current) {
|
||||
mountedRef.current = true;
|
||||
fetchData(1, '', undefined);
|
||||
}
|
||||
return () => {
|
||||
if (debounceRef.current) clearTimeout(debounceRef.current);
|
||||
};
|
||||
}, [autoLoad, fetchData]);
|
||||
|
||||
const listPageProps = useMemo(
|
||||
() => ({
|
||||
items,
|
||||
loading,
|
||||
error,
|
||||
isEmpty,
|
||||
searchProps: {
|
||||
value: keyword,
|
||||
onChange: handleKeywordChange,
|
||||
onSearch: refresh,
|
||||
},
|
||||
paginationProps: {
|
||||
current: page,
|
||||
total,
|
||||
pageSize,
|
||||
onChange: handlePageChange,
|
||||
},
|
||||
}),
|
||||
[items, loading, error, isEmpty, keyword, handleKeywordChange, refresh, page, total, pageSize, handlePageChange],
|
||||
);
|
||||
|
||||
return {
|
||||
items,
|
||||
loading,
|
||||
error,
|
||||
isEmpty,
|
||||
page,
|
||||
total,
|
||||
pageSize,
|
||||
keyword,
|
||||
setKeyword: handleKeywordChange,
|
||||
filter,
|
||||
setFilter: handleFilterChange,
|
||||
refresh,
|
||||
listPageProps,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user