Просмотр исходного кода

refactor(discussion): 迁移 antd v6 并接入编辑器右侧面板

- discussion 组件用 App.useApp() 替代静态 message/notification/Modal.confirm
- Editor 响应 redux openPanel 触发,对齐 v4 资源触发式讨论机制
- discussion tab 改用 DiscussionBox(读 redux message)
- SplitLayout 新增 openRequest prop 支持外部打开 tab
visuddhinanda 5 дней назад
Родитель
Сommit
0380aea8e1

+ 2 - 1
dashboard-v6/src/components/discussion/DiscussionCreate.tsx

@@ -1,5 +1,5 @@
 import { useIntl } from "react-intl";
-import { Form, message } from "antd";
+import { App, Form } from "antd";
 import {
   ProForm,
   type ProFormInstance,
@@ -40,6 +40,7 @@ const DiscussionCreateWidget = ({
   onTopicCreated,
 }: IWidget) => {
   const intl = useIntl();
+  const { message } = App.useApp();
   const formRef = useRef<ProFormInstance | undefined>(undefined);
   const _currUser = useAppSelector(_currentUser);
   const [currParent, setCurrParent] = useState(parent);

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

@@ -1,8 +1,6 @@
 import { useIntl } from "react-intl";
-import { Button, Card, Form } from "antd";
-import { message } from "antd";
+import { App, Button, Card, Col, Form, Row, Space } from "antd";
 import { ProForm, ProFormText } from "@ant-design/pro-components";
-import { Col, Row, Space } from "antd";
 import { CloseOutlined } from "@ant-design/icons";
 
 import { put } from "../../request";
@@ -17,6 +15,7 @@ interface IWidget {
 }
 const DiscussionEditWidget = ({ data, onUpdated, onClose }: IWidget) => {
   const intl = useIntl();
+  const { message } = App.useApp();
 
   return (
     <Card

+ 3 - 4
dashboard-v6/src/components/discussion/DiscussionShow.tsx

@@ -1,11 +1,9 @@
 import { useIntl } from "react-intl";
 import {
+  App,
   Button,
   Card,
   Dropdown,
-  message,
-  Modal,
-  notification,
   Space,
   Tag,
   Typography,
@@ -58,9 +56,10 @@ const DiscussionShowWidget = ({
   onConvert,
 }: IWidget) => {
   const intl = useIntl();
+  const { message, notification, modal } = App.useApp();
   const [closed, setClosed] = useState(data.status);
   const showDeleteConfirm = (id: string, resId: string, title: string) => {
-    Modal.confirm({
+    modal.confirm({
       icon: <ExclamationCircleOutlined />,
       title:
         intl.formatMessage({

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

@@ -1,4 +1,4 @@
-import { List, message, Skeleton } from "antd";
+import { App, List, Skeleton } from "antd";
 
 import { useEffect, useState } from "react";
 import { useIntl } from "react-intl";
@@ -44,6 +44,7 @@ const DiscussionTopicChildrenWidget = ({
   onTopicCreate,
 }: IWidget) => {
   const intl = useIntl();
+  const { message } = App.useApp();
   const [data, setData] = useState<IComment[]>([]);
   const [loading, setLoading] = useState(false);
   const [history, setHistory] = useState<ISentHistoryData[]>([]);
@@ -186,7 +187,7 @@ const DiscussionTopicChildrenWidget = ({
       .catch((e) => {
         message.error(e.message);
       });
-  }, [intl, topicId]);
+  }, [intl, message, topicId]);
   return (
     <div>
       {loading ? (

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

@@ -1,4 +1,4 @@
-import { message } from "antd";
+import { App } from "antd";
 import { useEffect, useState } from "react";
 
 import { get } from "../../request";
@@ -27,6 +27,7 @@ const DiscussionTopicInfoWidget = ({
   onClose,
   onConvert,
 }: IWidget) => {
+  const { message } = App.useApp();
   const [data, setData] = useState<IComment | undefined>(topic);
   useEffect(() => {
     setData(topic);
@@ -78,7 +79,7 @@ const DiscussionTopicInfoWidget = ({
       .catch((e) => {
         message.error(e.message);
       });
-  }, [topicId]);
+  }, [message, topicId]);
 
   return (
     <div>

+ 41 - 2
dashboard-v6/src/components/editor/Editor.tsx

@@ -1,5 +1,5 @@
 import { CommentOutlined, SearchOutlined } from "@ant-design/icons";
-import type { ReactNode } from "react";
+import { useEffect, useState, type ReactNode } from "react";
 import SplitLayout, { type RightToolbarTab } from "../general/SplitLayout";
 import ChannelPanel from "./panels/ChannelPanel";
 import DictPanel from "./panels/DictPanel";
@@ -14,8 +14,24 @@ import {
 import ChatPanel from "./panels/ChatPanel";
 import SuggestionPanel from "./panels/SuggestionPanel";
 import GrammarBookPanel from "./panels/GrammarBookPanel";
+import DiscussionBox from "../discussion/DiscussionBox";
 import type { ArticleType } from "../../api/article";
 import type { IChannel } from "../../api/channel";
+import { useAppSelector } from "../../hooks";
+import { openPanel, rightPanel } from "../../reducers/right-panel";
+import store from "../../store";
+
+/**
+ * redux openPanel 面板名 → 右边栏 tab key。
+ * v4 中这些值通过 `openPanel("xxx")` 触发对应面板打开。
+ */
+const OPENABLE_TABS = new Set([
+  "dict",
+  "channel",
+  "discussion",
+  "suggestion",
+  "grammar",
+]);
 
 // ─────────────────────────────────────────────
 // Props
@@ -69,6 +85,28 @@ export default function Editor({
 }: EditorProps) {
   const channels = channelId ? channelId.split("_") : undefined;
 
+  // ── 响应业务层 openPanel 触发(对齐 v4)──
+  // 例如点击句子/wbw 上的讨论按钮 → openDiscussion → openPanel("discussion")
+  const panel = useAppSelector(rightPanel);
+  const [openRequest, setOpenRequest] = useState<{ key: string; seq: number }>();
+  const [prevPanel, setPrevPanel] = useState<string | undefined>(panel);
+
+  // render 阶段把 redux 信号转换为一次性「打开请求」
+  // (React 官方「prop 变化时调整 state」模式,避免 effect 内同步 setState)
+  if (panel !== prevPanel) {
+    setPrevPanel(panel);
+    if (panel !== undefined && OPENABLE_TABS.has(panel)) {
+      setOpenRequest((prev) => ({ key: panel, seq: (prev?.seq ?? 0) + 1 }));
+    }
+  }
+
+  // 消费后复位信号(副作用:更新外部 store,保证下一次相同触发仍能命中)
+  useEffect(() => {
+    if (panel !== undefined) {
+      store.dispatch(openPanel(undefined));
+    }
+  }, [panel]);
+
   // ── 右边栏 tabs(固定业务定义)──
   // content 用独立组件而非内联 JSX,保证 articleId 等 props 变化时正常 re-render
   const rightTabs: RightToolbarTab[] = [
@@ -104,7 +142,7 @@ export default function Editor({
       key: "discussion",
       icon: <CommentOutlined />,
       label: "讨论",
-      content: <SearchPanel articleId={articleId} anthologyId={anthologyId} />,
+      content: <DiscussionBox />,
     },
     {
       key: "suggestion",
@@ -135,6 +173,7 @@ export default function Editor({
       sidebarTitle={sidebarTitle}
       sidebar={sidebar}
       rightTabs={rightTabs}
+      openRequest={openRequest}
     >
       {({ expandButton }) => children({ expandButton })}
     </SplitLayout>

+ 15 - 0
dashboard-v6/src/components/general/SplitLayout/SplitLayout.tsx

@@ -60,6 +60,12 @@ export interface SplitLayoutProps {
    */
   rightTabs?: RightToolbarTab[];
 
+  /**
+   * 外部请求打开某个 tab(业务层通过 redux 等触发)。
+   * `seq` 每次递增代表一次新请求,内部据此去重并打开对应 key 的 tab。
+   */
+  openRequest?: { key: string; seq: number };
+
   /** 右边栏面板默认宽度(px),默认 500 */
   defaultRightSize?: number;
   /** 右边栏面板最小宽度(px),默认 280 */
@@ -77,6 +83,7 @@ export default function SplitLayout({
   sidebar,
   children,
   rightTabs,
+  openRequest,
   defaultRightSize = 500,
   minRightSize = 280,
   maxRightSize = 800,
@@ -133,6 +140,14 @@ export default function SplitLayout({
 
   const closeRightPanel = useCallback(() => setRightActiveKey(null), []);
 
+  // ── 外部打开请求(去重后打开对应 tab)──
+  // 采用 React 官方推荐的「render 阶段调整 state」方式,避免 effect 内同步 setState。
+  const [handledOpenSeq, setHandledOpenSeq] = useState<number | null>(null);
+  if (openRequest && openRequest.seq !== handledOpenSeq) {
+    setHandledOpenSeq(openRequest.seq);
+    setRightActiveKey(openRequest.key);
+  }
+
   // ── Context ──
   const ctx: SplitLayoutContextValue = {
     collapsed,