visuddhinanda 1 неделя назад
Родитель
Сommit
3b73eaa2a1

+ 14 - 0
dashboard-v6/src/api/pali-text.ts

@@ -188,3 +188,17 @@ export const fetchParaNodeChunk = (
   if (channelIds) url += `&channels=${channelIds}`;
   return get<ParagraphNodeListResponse>(url);
 };
+
+export interface IFetchChapterTocParams {
+  book: number;
+  para: number;
+}
+
+export const fetchChapterToc = (
+  params: IFetchChapterTocParams
+): Promise<IChapterTocListResponse> => {
+  const { book, para } = params;
+  return get<IChapterTocListResponse>(
+    `/v2/chapter?view=toc&book=${book}&para=${para}`
+  );
+};

+ 25 - 48
dashboard-v6/src/components/article/ChapterToc.tsx

@@ -1,20 +1,19 @@
-import { useState, useEffect } from "react";
-
-import { get } from "../../request";
-
-import { Skeleton } from "antd";
 import type { Key } from "antd/lib/table/interface";
-import type { IChapterToc, IChapterTocListResponse } from "../../api/pali-text";
+import { Skeleton } from "antd";
+
+import { useChapterToc } from "./hooks/useChapterToc";
+import type { IChapterToc } from "../../api/pali-text";
 import type { ListNodeData } from "./components/EditableTree";
 import TocTree from "./components/TocTree";
 
 interface IWidget {
-  book?: number;
-  para?: number;
+  book: number;
+  para: number;
   maxLevel?: number;
   onSelect?: (selectedKeys: Key[]) => void;
   onData?: (data: IChapterToc[]) => void;
 }
+
 const ChapterTocWidget = ({
   book,
   para,
@@ -22,49 +21,27 @@ const ChapterTocWidget = ({
   onSelect,
   onData,
 }: IWidget) => {
-  const [tocList, setTocList] = useState<ListNodeData[]>([]);
-  const [loading, setLoading] = useState(true);
-  useEffect(() => {
-    const url = `/v2/chapter?view=toc&book=${book}&para=${para}`;
-    setLoading(true);
-    console.info("api request", url);
-    get<IChapterTocListResponse>(url)
-      .then((json) => {
-        console.info("api response", json);
-        const chapters = json.data.rows.filter(
-          (value) => value.level <= maxLevel
-        );
-        onData?.(chapters);
-        const toc = chapters.map((item, id) => {
-          return {
-            key: `${item.book}-${item.paragraph}`,
-            title: item.text,
-            level: item.level,
-          };
-        });
-        setTocList(toc);
-        if (chapters.length > 0) {
-          const path: string[] = [];
-          for (let index = chapters.length - 1; index >= 0; index--) {
-            const element = chapters[index];
-            if (element.book === book && para && element.paragraph <= para) {
-              path.push(`${element.book}-${element.paragraph}`);
-              break;
-            }
-          }
-        }
-      })
-      .finally(() => setLoading(false));
-  }, [book, maxLevel, para]);
+  const { data, loading } = useChapterToc({ book, para });
+
+  const chapters = data.rows.filter((item) => item.level <= maxLevel);
+
+  // onData 回调:chapters 变化时通知父组件
+  // 放在 useMemo/useEffect 取决于父组件是否需要在渲染外消费
+  // 这里跟原逻辑保持一致,在渲染时调用
+  onData?.(chapters);
+
+  const tocList: ListNodeData[] = chapters.map((item) => ({
+    key: `${item.book}-${item.paragraph}`,
+    title: item.text,
+    level: item.level,
+  }));
+
+  if (loading) return <Skeleton active />;
 
-  return loading ? (
-    <Skeleton active />
-  ) : (
+  return (
     <TocTree
       treeData={tocList}
-      onSelect={(selectedKeys: Key[]) => {
-        onSelect?.(selectedKeys);
-      }}
+      onSelect={(selectedKeys: Key[]) => onSelect?.(selectedKeys)}
     />
   );
 };

+ 71 - 0
dashboard-v6/src/components/article/hooks/useChapterToc.ts

@@ -0,0 +1,71 @@
+import { useState, useEffect, useCallback } from "react";
+
+import { fetchChapterToc } from "../../../api/pali-text";
+import type {
+  IChapterToc,
+  IFetchChapterTocParams,
+} from "../../../api/pali-text";
+
+interface IChapterTocData {
+  rows: IChapterToc[];
+  count: number;
+}
+
+interface IUseChapterTocReturn {
+  data: IChapterTocData;
+  loading: boolean;
+  errorCode: number | null;
+  errorMessage: string | null;
+  refresh: () => void;
+}
+
+export const useChapterToc = (
+  params: IFetchChapterTocParams
+): IUseChapterTocReturn => {
+  const [data, setData] = useState<IChapterTocData>({ rows: [], count: 0 });
+  const [loading, setLoading] = useState(true);
+  const [errorCode, setErrorCode] = useState<number | null>(null);
+  const [errorMessage, setErrorMessage] = useState<string | null>(null);
+  const [tick, setTick] = useState(0);
+
+  const refresh = useCallback(() => setTick((t) => t + 1), []);
+
+  const paramsKey = JSON.stringify(params);
+
+  useEffect(() => {
+    let active = true;
+
+    const fetchData = async () => {
+      setLoading(true); // ← 这里仍然会触发 lint,见下方说明
+      setErrorCode(null);
+      setErrorMessage(null);
+
+      try {
+        const res = await fetchChapterToc(JSON.parse(paramsKey));
+        if (!active) return;
+
+        if (!res.ok) {
+          setErrorCode(400);
+          setErrorMessage(res.message);
+          return;
+        }
+
+        setData(res.data);
+      } catch (e) {
+        if (!active) return;
+        setErrorCode(e as number);
+        setErrorMessage("Unknown error");
+      } finally {
+        if (active) setLoading(false);
+      }
+    };
+
+    fetchData();
+
+    return () => {
+      active = false;
+    };
+  }, [paramsKey, tick]);
+
+  return { data, loading, errorCode, errorMessage, refresh };
+};

+ 4 - 3
dashboard-v6/src/components/channel/ChannelInfo.tsx

@@ -1,12 +1,13 @@
 import { Statistic, StatisticCard } from "@ant-design/pro-components";
 import { Modal } from "antd";
 import { useState } from "react";
-import type { IItem } from "./ChannelPickerTable";
+
+import type { IChannelItem } from "../../api/channel";
 
 interface IChannelInfoModal {
   sentenceCount: number;
   open?: boolean;
-  channel?: IItem;
+  channel?: IChannelItem;
   onClose?: () => void;
 }
 
@@ -39,7 +40,7 @@ export const ChannelInfoModal = ({
 };
 interface IWidget {
   sentenceCount: number;
-  channel?: IItem;
+  channel?: IChannelItem;
 }
 const ChannelInfoWidget = ({ sentenceCount, channel }: IWidget) => {
   let totalStrLen = 0;

+ 2 - 2
dashboard-v6/src/components/dict/DictInfoCopyRef.tsx

@@ -1,9 +1,9 @@
 import { Button, message, Segmented, Typography } from "antd";
-import type { SegmentedValue } from "antd/lib/segmented"
+import type { SegmentedValue } from "antd/lib/segmented";
 import { useState } from "react";
 import { CopyOutlined } from "@ant-design/icons";
-import type { IWordByDict } from "./WordCardByDict"
 import { useIntl } from "react-intl";
+import type { IWordByDict } from "../../api/dict";
 const { Text } = Typography;
 
 interface IWidget {

+ 34 - 41
dashboard-v6/src/components/discussion/Discussion.tsx

@@ -1,4 +1,4 @@
-import { useEffect, useState } from "react";
+import { useState } from "react";
 import { ArrowLeftOutlined } from "@ant-design/icons";
 
 import DiscussionTopic from "./DiscussionTopic";
@@ -26,7 +26,7 @@ interface IWidget {
   onTopicReady?: (value: IComment) => void;
 }
 
-const DiscussionWidget = ({
+const Discussion = ({
   resId,
   resType,
   showTopicId,
@@ -35,35 +35,37 @@ const DiscussionWidget = ({
   type = "discussion",
   onTopicReady,
 }: IWidget) => {
-  const [childrenDrawer, setChildrenDrawer] = useState(false);
-  const [topicId, setTopicId] = useState<string>();
-  const [topic, setTopic] = useState<IComment>();
+  // 用户手动点击触发的状态
+  const [manualTopicId, setManualTopicId] = useState<string>();
+  const [manualTopic, setManualTopic] = useState<IComment>();
+  const [manualOpen, setManualOpen] = useState(false);
+
+  // 用于检测 resId 变化时重置
+  const [prevResId, setPrevResId] = useState(resId);
+  if (prevResId !== resId) {
+    setPrevResId(resId);
+    setManualOpen(false);
+    setManualTopicId(undefined);
+    setManualTopic(undefined);
+  }
+
   const [answerCount, setAnswerCount] = useState<IAnswerCount>();
   const [topicTitle, setTopicTitle] = useState<string>();
 
-  useEffect(() => {
-    if (showTopicId) {
-      setChildrenDrawer(true);
-      setTopicId(showTopicId);
-    } else {
-      setChildrenDrawer(false);
-    }
-  }, [showTopicId]);
-
-  
-  useEffect(() => {
-    setChildrenDrawer(false);
-  }, [resId]);
+  // 完全派生,不需要 effect
+  const childrenDrawer = manualOpen || !!showTopicId;
+  const topicId = manualOpen ? manualTopicId : showTopicId;
+  const topic = manualOpen ? manualTopic : undefined;
 
   const showChildrenDrawer = (comment: IComment) => {
     console.debug("discussion comment", comment);
-    setChildrenDrawer(true);
+    setManualOpen(true);
     if (comment.id) {
-      setTopicId(comment.id);
-      setTopic(undefined);
+      setManualTopicId(comment.id);
+      setManualTopic(undefined);
     } else {
-      setTopicId(undefined);
-      setTopic(comment);
+      setManualTopicId(undefined);
+      setManualTopic(comment);
     }
   };
 
@@ -75,7 +77,7 @@ const DiscussionWidget = ({
             <Button
               shape="circle"
               icon={<ArrowLeftOutlined />}
-              onClick={() => setChildrenDrawer(false)}
+              onClick={() => setManualOpen(false)}
             />
             <Text strong style={{ fontSize: 16 }}>
               {topic ? topic.title : topicTitle}
@@ -87,8 +89,10 @@ const DiscussionWidget = ({
             topic={topic}
             focus={focus}
             hideTitle
-            onItemCountChange={(count: number, parent: string) => {
-              setAnswerCount({ id: parent, count: count });
+            onItemCountChange={(total: number, parentId?: string | null) => {
+              if (parentId) {
+                setAnswerCount({ id: parentId, count: total });
+              }
             }}
             onTopicReady={(value: IComment) => {
               setTopicTitle(value.title);
@@ -96,12 +100,8 @@ const DiscussionWidget = ({
                 onTopicReady(value);
               }
             }}
-            onTopicDelete={() => {
-              setChildrenDrawer(false);
-            }}
-            onConvert={() => {
-              setChildrenDrawer(false);
-            }}
+            onTopicDelete={() => setManualOpen(false)}
+            onConvert={() => setManualOpen(false)}
           />
         </div>
       ) : (
@@ -118,18 +118,11 @@ const DiscussionWidget = ({
           onReady={() => {}}
           changedAnswerCount={answerCount}
           onItemCountChange={(count: number) => {
-            store.dispatch(
-              countChange({
-                count: count,
-                resId: resId,
-                resType: resType,
-              })
-            );
+            store.dispatch(countChange({ count, resId, resType }));
           }}
         />
       )}
     </>
   );
 };
-
-export default DiscussionWidget;
+export default Discussion;

+ 11 - 27
dashboard-v6/src/components/discussion/DiscussionTopic.tsx

@@ -2,9 +2,7 @@ import { useEffect, useState } from "react";
 
 import DiscussionTopicInfo from "./DiscussionTopicInfo";
 import DiscussionTopicChildren from "./DiscussionTopicChildren";
-import type { IComment } from "./DiscussionItem"
-import type { TResType } from "./DiscussionListCard"
-import type { TDiscussionType } from "./Discussion"
+import type { IComment, TDiscussionType, TResType } from "../../api/discussion";
 
 interface IWidget {
   resType?: TResType;
@@ -13,10 +11,10 @@ interface IWidget {
   focus?: string;
   hideTitle?: boolean;
   hideReply?: boolean;
-  onItemCountChange?: Function;
-  onTopicReady?: Function;
-  onTopicDelete?: Function;
-  onConvert?: Function;
+  onItemCountChange?: (total: number, parentId?: string | null) => void;
+  onTopicReady?: (value: IComment) => void;
+  onTopicDelete?: (id?: string) => void;
+  onConvert?: (value: TDiscussionType) => void;
 }
 const DiscussionTopicWidget = ({
   resType,
@@ -31,7 +29,6 @@ const DiscussionTopicWidget = ({
   onConvert,
 }: IWidget) => {
   const [count, setCount] = useState<number>();
-  const [_currResId, setCurrResId] = useState<string>();
   const [currTopicId, setCurrTopicId] = useState(topicId);
   const [currTopic, setCurrTopic] = useState<IComment | undefined>(topic);
   useEffect(() => {
@@ -46,23 +43,12 @@ const DiscussionTopicWidget = ({
         hideTitle={hideTitle}
         childrenCount={count}
         onReady={(value: IComment) => {
-          setCurrResId(value.resId);
           setCurrTopic(value);
           console.log("discussion onReady", value);
-          if (typeof onTopicReady !== "undefined") {
-            onTopicReady(value);
-          }
-        }}
-        onDelete={() => {
-          if (typeof onTopicDelete !== "undefined") {
-            onTopicDelete();
-          }
-        }}
-        onConvert={(value: TDiscussionType) => {
-          if (typeof onConvert !== "undefined") {
-            onConvert(value);
-          }
+          onTopicReady?.(value);
         }}
+        onDelete={onTopicDelete}
+        onConvert={onConvert}
       />
       <DiscussionTopicChildren
         topic={currTopic}
@@ -71,12 +57,10 @@ const DiscussionTopicWidget = ({
         focus={focus}
         topicId={topicId}
         hideReply={hideReply}
-        onItemCountChange={(count: number, e: string) => {
+        onItemCountChange={(total: number, parentId?: string | null) => {
           //把新建回答的消息传出去。
-          setCount(count);
-          if (typeof onItemCountChange !== "undefined") {
-            onItemCountChange(count, e);
-          }
+          setCount(total);
+          onItemCountChange?.(total, parentId);
         }}
         onTopicCreate={(value: IComment) => {
           console.log("onTopicCreate", value);

+ 10 - 24
dashboard-v6/src/components/discussion/DiscussionTopicInfo.tsx

@@ -3,19 +3,19 @@ import { useEffect, useState } from "react";
 
 import { get } from "../../request";
 import type { ICommentResponse } from "../../api/Comment";
-import DiscussionItem, { type IComment } from "./DiscussionItem";
-import type { TDiscussionType } from "./Discussion";
+import DiscussionItem from "./DiscussionItem";
+
+import type { IComment, TDiscussionType } from "../../api/discussion";
 
 interface IWidget {
   topicId?: string;
   topic?: IComment;
   childrenCount?: number;
   hideTitle?: boolean;
-  onDelete?: Function;
-  onReply?: Function;
-  onClose?: Function;
-  onReady?: Function;
-  onConvert?: Function;
+  onDelete?: (id?: string) => void;
+  onClose?: (data: IComment) => void;
+  onReady?: (discussion: IComment) => void;
+  onConvert?: (value: TDiscussionType) => void;
 }
 const DiscussionTopicInfoWidget = ({
   topicId,
@@ -24,7 +24,6 @@ const DiscussionTopicInfoWidget = ({
   hideTitle = false,
   onReady,
   onDelete,
-  onReply,
   onClose,
   onConvert,
 }: IWidget) => {
@@ -88,25 +87,12 @@ const DiscussionTopicInfoWidget = ({
           data={data}
           hideTitle={hideTitle}
           onDelete={() => {
-            if (typeof onDelete !== "undefined") {
-              onDelete(data.id);
-            }
-          }}
-          onReply={() => {
-            if (typeof onReply !== "undefined") {
-              onReply(data);
-            }
+            onDelete?.(data.id);
           }}
           onClose={() => {
-            if (typeof onClose !== "undefined") {
-              onClose(data);
-            }
-          }}
-          onConvert={(value: TDiscussionType) => {
-            if (typeof onConvert !== "undefined") {
-              onConvert(value);
-            }
+            onClose?.(data);
           }}
+          onConvert={onConvert}
         />
       ) : (
         <></>

+ 2 - 3
dashboard-v6/src/components/discussion/InteractiveArea.tsx

@@ -1,10 +1,10 @@
 import { useEffect, useState } from "react";
 import { Tabs } from "antd";
 
-import type { TResType } from "./DiscussionListCard"
 import Discussion from "./Discussion";
 import { get } from "../../request";
 import QaBox from "./QaBox";
+import type { TResType } from "../../api/discussion";
 
 interface IInteractive {
   ok: boolean;
@@ -30,7 +30,7 @@ interface IWidget {
 }
 const InteractiveAreaWidget = ({ resId, resType }: IWidget) => {
   const [showQa, setShowQa] = useState(false);
-  const [_qaCanEdit, setQaCanEdit] = useState(false);
+
   const [showHelp, setShowHelp] = useState(false);
   const [showDiscussion, setShowDiscussion] = useState(false);
 
@@ -41,7 +41,6 @@ const InteractiveAreaWidget = ({ resId, resType }: IWidget) => {
           console.debug("interactive", json);
           if (json.data.qa.can_create || json.data.qa.can_reply) {
             setShowQa(true);
-            setQaCanEdit(true);
           } else if (json.data.qa.count > 0) {
             setShowQa(true);
           } else {

+ 4 - 1
dashboard-v6/src/components/editor/Editor.tsx

@@ -93,7 +93,10 @@ export default function Editor({
           articleId={articleId}
           type={articleType}
           channels={channels}
-          onSelect={onChannelSelect}
+          onSelect={(selected) => {
+            console.debug(selected);
+            onChannelSelect?.(selected);
+          }}
         />
       ),
     },

+ 1 - 1
dashboard-v6/src/components/editor/panels/ChannelPanel.tsx

@@ -21,7 +21,7 @@ export default function ChannelPanel({
   onSelect,
 }: ChannelPanelProps) {
   const handleSelect = (selected: IChannel[]) => {
-    console.log("channel selected:", selected);
+    console.log("channel selected hello", selected);
     onSelect?.(selected);
   };
 

+ 11 - 9
dashboard-v6/src/components/share/utils.ts

@@ -5,12 +5,14 @@
 - 4 Collection 文集
 - 5 版本片段
  */
-export enum EResType {
-  pcs = 1,
-  channel = 2,
-  article = 3,
-  collection = 4,
-  workflow = 6,
-  project = 7,
-  modal = 8,
-}
+export const EResType = {
+  pcs: 1,
+  channel: 2,
+  article: 3,
+  collection: 4,
+  workflow: 6,
+  project: 7,
+  modal: 8,
+} as const;
+
+export type EResType = (typeof EResType)[keyof typeof EResType];

+ 17 - 13
dashboard-v6/src/components/task/TaskBuilderChapter.tsx

@@ -121,20 +121,24 @@ const TaskBuilderChapter = ({
               }}
             />
           </Space>
-          <ChapterToc
-            key={2}
-            book={book}
-            para={para}
-            maxLevel={7}
-            onData={(data: IChapterToc[]) => {
-              setChapter(data);
-              if (data.length > 0) {
-                if (!title && data[0].text) {
-                  setTitle(data[0].text);
+          {book && para ? (
+            <ChapterToc
+              key={2}
+              book={book}
+              para={para}
+              maxLevel={7}
+              onData={(data: IChapterToc[]) => {
+                setChapter(data);
+                if (data.length > 0) {
+                  if (!title && data[0].text) {
+                    setTitle(data[0].text);
+                  }
                 }
-              }
-            }}
-          />
+              }}
+            />
+          ) : (
+            <></>
+          )}
         </div>
       ),
     },

+ 4 - 0
dashboard-v6/src/features/editor/Article.tsx

@@ -3,6 +3,7 @@
 // ─────────────────────────────────────────────
 
 import type { ArticleMode } from "../../api/article";
+import type { IChannel } from "../../api/channel";
 import AnthologyTocTree from "../../components/anthology/AnthologyTocTree";
 import TypeArticle from "../../components/article/TypeArticle";
 import Editor from "../../components/editor";
@@ -26,6 +27,7 @@ export interface ArticleEditorProps {
   onAnthologySelect?: (anthologyId: string) => void;
   /** 文章内部触发跳转(type: 'article' | 'anthology' 等) */
   onArticleChange?: (type: string, id: string) => void;
+  onChannelSelect?: (selected: IChannel[]) => void;
 }
 
 // ─────────────────────────────────────────────
@@ -41,6 +43,7 @@ export default function ArticleEditor({
   onArticleClick,
   onAnthologySelect,
   onArticleChange,
+  onChannelSelect,
 }: ArticleEditorProps) {
   const channels = channelId ? channelId.split("_") : undefined;
 
@@ -61,6 +64,7 @@ export default function ArticleEditor({
       articleId={articleId}
       anthologyId={anthologyId}
       channelId={channelId}
+      onChannelSelect={onChannelSelect}
     >
       {({ expandButton }) => (
         <TypeArticle

+ 14 - 1
dashboard-v6/src/pages/workspace/article/show.tsx

@@ -4,7 +4,7 @@ import ArticleEditor from "../../../features/editor/Article";
 
 const Widget = () => {
   const { articleId, anthologyId } = useParams();
-  const [searchParams] = useSearchParams();
+  const [searchParams, setSearchParams] = useSearchParams();
   const navigate = useNavigate();
 
   const mode = searchParams.get("mode") ?? "read";
@@ -42,6 +42,19 @@ const Widget = () => {
           navigate(`/workspace/${type}/${id}`);
         }
       }}
+      onChannelSelect={(selected) => {
+        console.debug("channel hello hello", selected);
+        console.debug("channel changed", selected);
+        const channelsParams = Array.isArray(selected)
+          ? selected.map((item) => item.id)
+          : [];
+        console.debug("channelsParams", channelsParams);
+
+        const newParams = new URLSearchParams(searchParams);
+        newParams.set("channel", channelsParams.join());
+        console.debug("channel set", channelsParams);
+        setSearchParams(newParams);
+      }}
     />
   );
 };

+ 8 - 6
dashboard-v6/src/types/wbw.ts

@@ -25,12 +25,14 @@ export interface IWbwField {
   value: string;
 }
 
-export enum WbwStatus {
-  initiate = 0,
-  auto = 3,
-  apply = 5,
-  manual = 7,
-}
+export const WbwStatus = {
+  initiate: 0,
+  auto: 3,
+  apply: 5,
+  manual: 7,
+} as const;
+
+export type WbwStatus = (typeof WbwStatus)[keyof typeof WbwStatus];
 
 export interface IWbwAttachment {
   id: string;