对 47 个前端文件统一执行 npm run format,消除存量格式差异。 随后在 CI 与 Makefile check 中启用 format:check,确保后续提交强制遵守 prettier 风格。
134 lines
4.2 KiB
TypeScript
134 lines
4.2 KiB
TypeScript
import {
|
||
useState,
|
||
useCallback,
|
||
useRef,
|
||
useEffect,
|
||
type Dispatch,
|
||
type SetStateAction,
|
||
type MutableRefObject,
|
||
} from 'react';
|
||
import type { Topic, TopicList, TopicRenamed, TopicSummary, Command } from '../../types/protocol';
|
||
|
||
export interface UseTopicsReturn {
|
||
topics: Topic[];
|
||
setTopics: Dispatch<SetStateAction<Topic[]>>;
|
||
selectedTopic: string | null;
|
||
setSelectedTopic: Dispatch<SetStateAction<string | null>>;
|
||
topicRefreshTrigger: number;
|
||
bumpTopicRefreshTrigger: () => void;
|
||
topicsRef: MutableRefObject<Topic[]>;
|
||
selectedTopicRef: MutableRefObject<string | null>;
|
||
pendingNewTopicRef: MutableRefObject<boolean>;
|
||
/** 处理 topic_list 消息:映射格式并按 pendingNewTopic 自动聚焦,返回是否自动聚焦了新话题 */
|
||
handleTopicList: (msg: TopicList) => boolean;
|
||
/** 处理 topic_renamed 消息:用刷新后的列表替换本地状态(不改 selectedTopic) */
|
||
handleTopicRenamed: (msg: TopicRenamed) => void;
|
||
createTopic: (title?: string) => Command;
|
||
switchTopic: (topicId: string) => Command;
|
||
deleteTopic: (topicId: string) => Command;
|
||
renameTopic: (topicId: string, title: string) => Command;
|
||
requestTopicList: (sessionId: string | null) => Command | null;
|
||
}
|
||
|
||
/** 将后端 TopicSummary[] 映射为前端 Topic[] */
|
||
function mapTopicSummaries(summaries: TopicSummary[]): Topic[] {
|
||
return summaries.map((t) => ({
|
||
id: t.topic_id,
|
||
session_id: t.session_id,
|
||
title: t.title,
|
||
description: t.description || undefined,
|
||
message_count: Number(t.message_count),
|
||
created_at: t.created_at,
|
||
updated_at: t.last_active_at,
|
||
}));
|
||
}
|
||
|
||
export function useTopics(): UseTopicsReturn {
|
||
const [topics, setTopics] = useState<Topic[]>([]);
|
||
const [selectedTopic, setSelectedTopic] = useState<string | null>(null);
|
||
const [topicRefreshTrigger, setTopicRefreshTrigger] = useState(0);
|
||
|
||
const topicsRef = useRef<Topic[]>([]);
|
||
const selectedTopicRef = useRef<string | null>(null);
|
||
const pendingNewTopicRef = useRef(false);
|
||
|
||
// ref 同步:确保回调中读到最新值
|
||
useEffect(() => {
|
||
topicsRef.current = topics;
|
||
}, [topics]);
|
||
|
||
useEffect(() => {
|
||
selectedTopicRef.current = selectedTopic;
|
||
}, [selectedTopic]);
|
||
|
||
const bumpTopicRefreshTrigger = useCallback(() => {
|
||
setTopicRefreshTrigger((n) => n + 1);
|
||
}, []);
|
||
|
||
const handleTopicList = useCallback((msg: TopicList): boolean => {
|
||
const newTopics = mapTopicSummaries(msg.topics);
|
||
setTopics(newTopics);
|
||
|
||
// 新建话题后自动聚焦到新话题(列表按 last_active_at DESC 排序,第一个即最新)
|
||
if (pendingNewTopicRef.current) {
|
||
pendingNewTopicRef.current = false;
|
||
if (newTopics.length > 0) {
|
||
setSelectedTopic(newTopics[0].id);
|
||
return true;
|
||
}
|
||
}
|
||
return false;
|
||
}, []);
|
||
|
||
const handleTopicRenamed = useCallback((msg: TopicRenamed): void => {
|
||
// 后端返回刷新后的完整列表,直接替换;selectedTopic 基于 id 不变,无需调整
|
||
setTopics(mapTopicSummaries(msg.topics));
|
||
}, []);
|
||
|
||
const createTopic = useCallback((title?: string): Command => {
|
||
pendingNewTopicRef.current = true;
|
||
return {
|
||
type: 'create_session',
|
||
title:
|
||
title ||
|
||
`话题 ${new Date().toLocaleString('zh-CN', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' })}`,
|
||
};
|
||
}, []);
|
||
|
||
const switchTopic = useCallback((topicId: string): Command => {
|
||
return { type: 'switch_topic', topic_id: topicId };
|
||
}, []);
|
||
|
||
const deleteTopic = useCallback((topicId: string): Command => {
|
||
return { type: 'delete_topic', topic_id: topicId };
|
||
}, []);
|
||
|
||
const renameTopic = useCallback((topicId: string, title: string): Command => {
|
||
return { type: 'rename_topic', topic_id: topicId, title };
|
||
}, []);
|
||
|
||
const requestTopicList = useCallback((sessionId: string | null): Command | null => {
|
||
if (!sessionId) return null;
|
||
return { type: 'list_topics', session_id: sessionId };
|
||
}, []);
|
||
|
||
return {
|
||
topics,
|
||
setTopics,
|
||
selectedTopic,
|
||
setSelectedTopic,
|
||
topicRefreshTrigger,
|
||
bumpTopicRefreshTrigger,
|
||
topicsRef,
|
||
selectedTopicRef,
|
||
pendingNewTopicRef,
|
||
handleTopicList,
|
||
handleTopicRenamed,
|
||
createTopic,
|
||
switchTopic,
|
||
deleteTopic,
|
||
renameTopic,
|
||
requestTopicList,
|
||
};
|
||
}
|