feat: 重设计私信页并优化开源展柜体验

私信改为飞书风布局,支持缩略图表情/图片、按会话草稿与用户搜索发起会话;主页发私信直达消息页。开源展柜修复空列表粘滞并改版目录式 UI。
This commit is contained in:
2026-09-02 02:45:29 +08:00
parent 1c0f7ede55
commit 500e452c2f
11 changed files with 1176 additions and 258 deletions

View File

@@ -13,15 +13,22 @@ type Fetcher<T> = () => Promise<T>;
* 会话快照数据源:有缓存则跳过请求;无缓存时保留上一份画面直到新数据返回。
* 手机下拉软刷新 commit / PAGE_FORCE_REFRESH_EVENT 会应用新快照。
*/
function isEmptyArray(value: unknown): boolean {
return Array.isArray(value) && value.length === 0;
}
export function useSessionResource<T>(
key: string | null,
fetcher: Fetcher<T>,
opts?: {
enabled?: boolean;
onError?: (e: unknown) => void;
/** 缓存为空数组时仍后台再拉,避免启动竞态假空粘死 */
revalidateEmpty?: boolean;
},
) {
const enabled = opts?.enabled !== false;
const revalidateEmpty = opts?.revalidateEmpty === true;
const fetcherRef = useRef(fetcher);
fetcherRef.current = fetcher;
const onErrorRef = useRef(opts?.onError);
@@ -49,7 +56,7 @@ export function useSessionResource<T>(
}
const hit = getSessionSnapshot<T>(key);
if (hit !== undefined) {
if (hit !== undefined && !(revalidateEmpty && isEmptyArray(hit))) {
setData(hit);
setLoading(false);
setPending(false);
@@ -57,9 +64,16 @@ export function useSessionResource<T>(
}
const seq = ++seqRef.current;
const keep = dataRef.current !== undefined;
if (keep) setPending(true);
else setLoading(true);
const keep = dataRef.current !== undefined || (hit !== undefined && isEmptyArray(hit));
if (hit !== undefined && isEmptyArray(hit)) {
setData(hit);
setPending(true);
setLoading(false);
} else if (keep && dataRef.current !== undefined) {
setPending(true);
} else {
setLoading(true);
}
fetcherRef.current()
.then((next) => {
@@ -70,6 +84,7 @@ export function useSessionResource<T>(
.catch((e: unknown) => {
if (seq !== seqRef.current) return;
onErrorRef.current?.(e);
// 失败不写空快照;已有画面则保留
if (!keep) setData(undefined);
})
.finally(() => {
@@ -81,7 +96,7 @@ export function useSessionResource<T>(
return () => {
seqRef.current += 1;
};
}, [key, enabled]);
}, [key, enabled, revalidateEmpty]);
const replace = useCallback((next: T | ((prev: T | undefined) => T)) => {
setData((prev) => {
@@ -99,18 +114,20 @@ export function useSessionResource<T>(
const reload = (opts: { allowLoading: boolean }) => {
if (!key || !enabled) return;
const warm = getSessionSnapshot<T>(key);
if (warm !== undefined) {
if (warm !== undefined && !(revalidateEmpty && isEmptyArray(warm))) {
setData(warm);
setLoading(false);
setPending(false);
return;
}
const seq = ++seqRef.current;
const keep = dataRef.current !== undefined;
const keep = dataRef.current !== undefined || (warm !== undefined && isEmptyArray(warm));
if (opts.allowLoading) {
deleteSessionSnapshot(key);
if (keep) setPending(true);
else setLoading(true);
} else if (warm !== undefined && isEmptyArray(warm)) {
setPending(true);
}
fetcherRef.current()
.then((next) => {
@@ -138,7 +155,7 @@ export function useSessionResource<T>(
window.removeEventListener(PAGE_FORCE_REFRESH_EVENT, onForce);
window.removeEventListener(PAGE_SOFT_REFRESH_COMMIT_EVENT, onCommit);
};
}, [key, enabled]);
}, [key, enabled, revalidateEmpty]);
return { data, loading, pending, replace, invalidate };
}