2
0

TreeText.tsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401
  1. import React, { useState, useCallback, useEffect } from "react";
  2. import { Tree, Segmented, Spin } from "antd";
  3. import type { TreeDataNode } from "antd";
  4. import {
  5. BookOutlined,
  6. FileTextOutlined,
  7. FolderOutlined,
  8. FontSizeOutlined,
  9. TranslationOutlined,
  10. } from "@ant-design/icons";
  11. import { get } from "../../request";
  12. import type {
  13. IPaliListResponse,
  14. IPaliParagraphResponse,
  15. ISentenceListResponse,
  16. } from "../../api/Corpus";
  17. // 定义节点类型
  18. type NodeType =
  19. | "book"
  20. | "chapter"
  21. | "paragraph"
  22. | "sentence"
  23. | "text"
  24. | "resources"
  25. | "translations"
  26. | "similar"
  27. | "preview";
  28. // 定义模式类型
  29. type ParagraphMode = "preview" | "edit";
  30. // 定义基础节点数据接口
  31. interface BaseNodeData {
  32. id: string;
  33. title: string;
  34. type: NodeType;
  35. isLeaf?: boolean;
  36. preview?: string;
  37. content?: React.ReactNode;
  38. children?: BaseNodeData[];
  39. }
  40. // 定义树节点接口
  41. interface TreeNode extends TreeDataNode {
  42. key: string;
  43. type: NodeType;
  44. isLeaf?: boolean;
  45. children?: TreeNode[];
  46. }
  47. // 定义API响应接口
  48. type ApiResponse = BaseNodeData;
  49. // 定义组件状态接口
  50. interface IWidget {
  51. type?: NodeType;
  52. rootId?: string;
  53. channelsId?: string[];
  54. }
  55. const TreeTextComponent = ({ type, rootId, channelsId }: IWidget) => {
  56. const [treeData, setTreeData] = useState<BaseNodeData[]>([]);
  57. const [loadingKeys, setLoadingKeys] = useState<string[]>([]);
  58. const [paragraphModes, setParagraphModes] = useState<
  59. Record<string, ParagraphMode>
  60. >({});
  61. useEffect(() => {
  62. if (type === "chapter") {
  63. const url = `/v2/palitext/${rootId}`;
  64. get<IPaliParagraphResponse>(url).then((json) => {
  65. if (json.ok) {
  66. setTreeData([
  67. {
  68. id: json.data.uid,
  69. title: json.data.text,
  70. type: "chapter",
  71. },
  72. ]);
  73. }
  74. });
  75. }
  76. }, [rootId, type]);
  77. // 模拟API调用
  78. const mockApiCall = useCallback(
  79. async (type: NodeType, key: string): Promise<ApiResponse[]> => {
  80. console.log("Calling API:", type, key);
  81. //await new Promise((resolve) => setTimeout(resolve, 1000)); // 模拟网络延迟
  82. // 模拟不同类型节点的响应数据
  83. if (type === "book") {
  84. return [
  85. { id: "chapter_1", title: "第一章", type: "chapter" },
  86. { id: "chapter_2", title: "第二章", type: "chapter" },
  87. { id: "chapter_3", title: "第三章", type: "chapter" },
  88. ];
  89. } else if (type === "chapter") {
  90. const url = `/v2/palitext?view=children&id=${key}`;
  91. const paragraphs = await get<IPaliListResponse>(url);
  92. return paragraphs.data.rows.map((item) => {
  93. if (item.level < 8) {
  94. return {
  95. id: item.uid,
  96. title: item.toc,
  97. type: "chapter",
  98. };
  99. } else {
  100. return {
  101. id: `${item.book}-${item.paragraph}`,
  102. title: item.paragraph.toString(),
  103. type: "paragraph",
  104. preview: item.text,
  105. };
  106. }
  107. });
  108. } else if (type === "paragraph") {
  109. const [book, paragraph] = key.split("-");
  110. const url = `/v2/sentence?view=paragraph&book=${book}&para=${paragraph}&channels=${channelsId?.join()}`;
  111. const res = await get<ISentenceListResponse>(url);
  112. return res.data.rows.map((item) => {
  113. return {
  114. id: item.id ?? "123",
  115. title: `${item.book}-${item.paragraph}-${item.word_start}-${item.word_end}`,
  116. type: "sentence",
  117. children: [
  118. {
  119. id: "text_node",
  120. title: item.content,
  121. type: "text",
  122. isLeaf: true,
  123. },
  124. {
  125. id: "resources",
  126. title: "资源",
  127. type: "resources",
  128. children: [
  129. {
  130. id: "translations",
  131. title: "参考译文",
  132. type: "translations",
  133. },
  134. {
  135. id: "similar",
  136. title: "相似句",
  137. type: "similar",
  138. },
  139. ],
  140. },
  141. ],
  142. };
  143. });
  144. } else if (type === "similar") {
  145. return [
  146. {
  147. id: "text_node",
  148. title: "句子文本:This is the original sentence text.",
  149. type: "text",
  150. isLeaf: true,
  151. },
  152. {
  153. id: "resources",
  154. title: "资源",
  155. type: "resources",
  156. children: [
  157. {
  158. id: "translations",
  159. title: "参考译文",
  160. type: "translations",
  161. },
  162. {
  163. id: "similar",
  164. title: "相似句",
  165. type: "similar",
  166. },
  167. ],
  168. },
  169. ];
  170. }
  171. return [];
  172. },
  173. [channelsId]
  174. );
  175. // 获取节点图标
  176. const getNodeIcon = (type: NodeType): React.ReactNode => {
  177. switch (type) {
  178. case "book":
  179. return <BookOutlined />;
  180. case "chapter":
  181. return <FolderOutlined />;
  182. case "paragraph":
  183. return <FileTextOutlined />;
  184. case "sentence":
  185. return <FontSizeOutlined />;
  186. case "translations":
  187. return <TranslationOutlined />;
  188. case "similar":
  189. return <FileTextOutlined />;
  190. default:
  191. return null;
  192. }
  193. };
  194. // 构建树节点
  195. const buildTreeNode = (
  196. node: BaseNodeData,
  197. parentKey: string = ""
  198. ): TreeNode => {
  199. const key = parentKey
  200. ? `${parentKey}_${node.id}`
  201. : `${node.type}_${node.id}`;
  202. const isLoading = loadingKeys.includes(key);
  203. let children: TreeNode[] = [];
  204. let hasChildren = false;
  205. if (node.type === "paragraph") {
  206. const mode = paragraphModes[key] || "preview";
  207. if (mode === "preview" && node.preview) {
  208. // 预览模式:只显示预览文字节点
  209. children = [
  210. {
  211. title: `预览:${node.preview}`,
  212. key: `${key}_preview`,
  213. type: "preview" as NodeType,
  214. isLeaf: true,
  215. icon: <FontSizeOutlined style={{ color: "#1890ff" }} />,
  216. },
  217. ];
  218. } else if (mode === "edit" && node.children) {
  219. // 编辑模式:显示子sentence节点
  220. children = node.children.map((child) => buildTreeNode(child, key));
  221. }
  222. hasChildren = Boolean(node.children && node.children.length > 0);
  223. } else if (node.children) {
  224. children = node.children.map((child) => buildTreeNode(child, key));
  225. hasChildren = true;
  226. } else if (
  227. !node.isLeaf &&
  228. ["book", "chapter", "paragraph", "sentence"].includes(node.type)
  229. ) {
  230. hasChildren = true;
  231. }
  232. const handleModeChange = (value: string | number): void => {
  233. setParagraphModes((prev) => ({
  234. ...prev,
  235. [key]: value as ParagraphMode,
  236. }));
  237. };
  238. const handleSegmentedClick = (e: React.MouseEvent): void => {
  239. e.stopPropagation();
  240. };
  241. const treeNode: TreeNode = {
  242. title: (
  243. <div style={{ display: "inline-block" }}>
  244. <div
  245. style={{
  246. display: "flex",
  247. alignItems: "center",
  248. justifyContent: "space-between",
  249. width: "100%",
  250. }}
  251. >
  252. {node.content ?? <span>{node.title}</span>}
  253. {node.type === "paragraph" && (
  254. <Segmented
  255. size="small"
  256. value={paragraphModes[key] || "preview"}
  257. onChange={handleModeChange}
  258. options={[
  259. { label: "预览", value: "preview" },
  260. { label: "编辑", value: "edit" },
  261. ]}
  262. style={{ marginLeft: 8 }}
  263. onClick={handleSegmentedClick}
  264. />
  265. )}
  266. </div>
  267. </div>
  268. ),
  269. key,
  270. type: node.type,
  271. icon: isLoading ? <Spin size="small" /> : getNodeIcon(node.type),
  272. isLeaf: node.isLeaf || (!hasChildren && node.type === "text"),
  273. children: children.length > 0 ? children : undefined,
  274. };
  275. return treeNode;
  276. };
  277. // 懒加载处理
  278. const onLoadData = async (node: TreeNode): Promise<void> => {
  279. const { key, type } = node;
  280. if (loadingKeys.includes(key)) return;
  281. setLoadingKeys((prev) => [...prev, key]);
  282. try {
  283. const id = key.split("_").pop() || "";
  284. const data = await mockApiCall(type, id);
  285. // 更新树数据
  286. const updateTreeData = (
  287. nodes: BaseNodeData[],
  288. parentKey: string = ""
  289. ): BaseNodeData[] => {
  290. return nodes.map((node) => {
  291. const currentKey = parentKey
  292. ? `${parentKey}_${node.id}`
  293. : `${node.type}_${node.id}`;
  294. if (currentKey === key) {
  295. return {
  296. ...node,
  297. children: data.map((child) => ({
  298. ...child,
  299. children: child.children || [],
  300. })),
  301. };
  302. } else if (node.children) {
  303. return {
  304. ...node,
  305. children: updateTreeData(node.children, currentKey),
  306. };
  307. }
  308. return node;
  309. });
  310. };
  311. setTreeData((prev) => updateTreeData(prev));
  312. } catch (error) {
  313. console.error("加载数据失败:", error);
  314. } finally {
  315. setLoadingKeys((prev) => prev.filter((k) => k !== key));
  316. }
  317. };
  318. return (
  319. <div
  320. style={{ padding: 20, backgroundColor: "#f5f5f5", minHeight: "100vh" }}
  321. >
  322. <div
  323. style={{
  324. backgroundColor: "white",
  325. padding: 20,
  326. borderRadius: 8,
  327. boxShadow: "0 2px 8px rgba(0,0,0,0.1)",
  328. }}
  329. >
  330. <h2 style={{ marginBottom: 20, color: "#1890ff" }}>
  331. 树状文本展示组件 (TypeScript)
  332. </h2>
  333. <Tree
  334. showIcon
  335. showLine={true}
  336. loadData={onLoadData}
  337. treeData={treeData.map((node) => buildTreeNode(node))}
  338. style={{ fontSize: 14 }}
  339. blockNode
  340. />
  341. </div>
  342. {/* 使用说明 */}
  343. <div
  344. style={{
  345. marginTop: 20,
  346. backgroundColor: "white",
  347. padding: 20,
  348. borderRadius: 8,
  349. boxShadow: "0 2px 8px rgba(0,0,0,0.1)",
  350. }}
  351. >
  352. <h3>功能说明:</h3>
  353. <ul style={{ lineHeight: 1.8 }}>
  354. <li>📚 点击book节点懒加载chapter数据</li>
  355. <li>📄 点击chapter节点懒加载paragraph数据</li>
  356. <li>📝 paragraph节点右侧可切换预览/编辑模式</li>
  357. <li>👁️ 预览模式:只显示预览文字</li>
  358. <li>✏️ 编辑模式:显示子sentence节点</li>
  359. <li>📖 sentence节点包含句子文本和资源(参考译文、相似句)</li>
  360. </ul>
  361. <h4 style={{ marginTop: 20 }}>TypeScript 特性:</h4>
  362. <ul style={{ lineHeight: 1.8 }}>
  363. <li>🔒 完整的类型定义和类型安全</li>
  364. <li>📝 接口定义清晰,便于维护</li>
  365. <li>⚡ 更好的IDE支持和代码提示</li>
  366. <li>🛡️ 编译时错误检查</li>
  367. </ul>
  368. </div>
  369. </div>
  370. );
  371. };
  372. export default TreeTextComponent;