فهرست منبع

feat(dashboard-v6): 为所有页面设置 i18n 标题,详情页显示「分类名-名称」

- 51 个页面统一在 page 内用 <title> 设置文档标题并走 react-intl
- 详情/编辑页标题为「分类名-名称」,名称来自路由 loader 数据
- 新增 projectLoader / chapterLoader / paraLoader,anthology 下 :articleId 复用 articleLoader
- 新增 pages.* 国际化文案(en-US / zh-Hans)
visuddhinanda 3 روز پیش
والد
کامیت
d29304cdad
60فایلهای تغییر یافته به همراه546 افزوده شده و 106 حذف شده
  1. 39 0
      dashboard-v6/src/api/pali-text.ts
  2. 18 0
      dashboard-v6/src/api/task.ts
  3. 2 0
      dashboard-v6/src/locales/en-US.ts
  4. 16 0
      dashboard-v6/src/locales/en-US/pages.ts
  5. 2 0
      dashboard-v6/src/locales/zh-Hans.ts
  6. 16 0
      dashboard-v6/src/locales/zh-Hans/pages.ts
  7. 9 2
      dashboard-v6/src/pages/dashboard/index.tsx
  8. 8 1
      dashboard-v6/src/pages/dashboard/settings/ai-model/edit.tsx
  9. 8 1
      dashboard-v6/src/pages/dashboard/settings/ai-model/index.tsx
  10. 8 1
      dashboard-v6/src/pages/dashboard/settings/ai-model/log.tsx
  11. 9 2
      dashboard-v6/src/pages/home.tsx
  12. 5 0
      dashboard-v6/src/pages/users/account-info.tsx
  13. 11 1
      dashboard-v6/src/pages/users/change-password.tsx
  14. 14 9
      dashboard-v6/src/pages/users/forgot-password.tsx
  15. 3 0
      dashboard-v6/src/pages/users/logs.tsx
  16. 9 2
      dashboard-v6/src/pages/users/personal/index.tsx
  17. 12 9
      dashboard-v6/src/pages/users/reset-password.tsx
  18. 1 0
      dashboard-v6/src/pages/users/sign-in.tsx
  19. 3 0
      dashboard-v6/src/pages/users/sign-in2.tsx
  20. 11 8
      dashboard-v6/src/pages/users/sign-up.tsx
  21. 10 1
      dashboard-v6/src/pages/workspace/anthology/edit.tsx
  22. 5 1
      dashboard-v6/src/pages/workspace/anthology/index.tsx
  23. 10 2
      dashboard-v6/src/pages/workspace/anthology/show.tsx
  24. 5 1
      dashboard-v6/src/pages/workspace/article/index.tsx
  25. 14 3
      dashboard-v6/src/pages/workspace/article/show.tsx
  26. 9 2
      dashboard-v6/src/pages/workspace/channel/list.tsx
  27. 4 1
      dashboard-v6/src/pages/workspace/channel/setting.tsx
  28. 4 1
      dashboard-v6/src/pages/workspace/channel/show.tsx
  29. 7 2
      dashboard-v6/src/pages/workspace/chat/index.tsx
  30. 6 2
      dashboard-v6/src/pages/workspace/course/edit.tsx
  31. 10 1
      dashboard-v6/src/pages/workspace/course/index.tsx
  32. 15 2
      dashboard-v6/src/pages/workspace/course/show.tsx
  33. 15 2
      dashboard-v6/src/pages/workspace/course/textbook.tsx
  34. 5 1
      dashboard-v6/src/pages/workspace/dict/list.tsx
  35. 5 1
      dashboard-v6/src/pages/workspace/driver/list.tsx
  36. 3 1
      dashboard-v6/src/pages/workspace/home.tsx
  37. 10 1
      dashboard-v6/src/pages/workspace/invite/index.tsx
  38. 15 2
      dashboard-v6/src/pages/workspace/tag/edit.tsx
  39. 12 7
      dashboard-v6/src/pages/workspace/tag/index.tsx
  40. 15 2
      dashboard-v6/src/pages/workspace/tag/show.tsx
  41. 3 1
      dashboard-v6/src/pages/workspace/task/hall.tsx
  42. 3 1
      dashboard-v6/src/pages/workspace/task/pending.tsx
  43. 15 2
      dashboard-v6/src/pages/workspace/task/project-edit.tsx
  44. 11 2
      dashboard-v6/src/pages/workspace/task/project.tsx
  45. 3 1
      dashboard-v6/src/pages/workspace/task/projects.tsx
  46. 8 1
      dashboard-v6/src/pages/workspace/task/tasks.tsx
  47. 8 1
      dashboard-v6/src/pages/workspace/task/workflow.tsx
  48. 10 2
      dashboard-v6/src/pages/workspace/team/edit.tsx
  49. 3 1
      dashboard-v6/src/pages/workspace/team/index.tsx
  50. 10 2
      dashboard-v6/src/pages/workspace/team/show.tsx
  51. 18 7
      dashboard-v6/src/pages/workspace/term/edit.tsx
  52. 8 1
      dashboard-v6/src/pages/workspace/term/list.tsx
  53. 18 7
      dashboard-v6/src/pages/workspace/term/show.tsx
  54. 3 0
      dashboard-v6/src/pages/workspace/tipitaka/bypath.tsx
  55. 15 2
      dashboard-v6/src/pages/workspace/tipitaka/chapter.tsx
  56. 15 2
      dashboard-v6/src/pages/workspace/tipitaka/para.tsx
  57. 5 0
      dashboard-v6/src/pages/workspace/transfer/index.tsx
  58. 2 1
      dashboard-v6/src/routes/anthologyRoutes.ts
  59. 2 0
      dashboard-v6/src/routes/taskRouters.ts
  60. 3 0
      dashboard-v6/src/routes/tipitakaRoutes.ts

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

@@ -1,5 +1,6 @@
 // src/api/pali-text.ts
 import type { MenuProps } from "antd";
+import type { LoaderFunctionArgs } from "react-router";
 import { get } from "../request";
 import type { ArticleMode, IArticleResponse } from "./article";
 import type { IWidgetSentEditInner } from "../components/sentence/SentEdit";
@@ -202,3 +203,41 @@ export const fetchChapterToc = (
     `/api/v2/chapter?view=toc&book=${book}&para=${para}`
   );
 };
+
+export async function chapterLoader({ params }: LoaderFunctionArgs) {
+  const id = params.id;
+
+  if (!id) {
+    throw new Response("Missing chapter id", { status: 400 });
+  }
+
+  const res = await fetchChapter(id, "read");
+
+  if (!res.ok) {
+    throw new Response("Chapter not found", { status: 404 });
+  }
+
+  return res.data;
+}
+
+export async function paraLoader({ params }: LoaderFunctionArgs) {
+  const id = params.id;
+
+  if (!id) {
+    throw new Response("Missing paragraph id", { status: 400 });
+  }
+
+  const [book, para] = id.split("-");
+
+  if (!book || !para) {
+    throw new Response("Invalid paragraph id", { status: 400 });
+  }
+
+  const res = await fetchPara(book, para, "read");
+
+  if (!res.ok) {
+    throw new Response("Paragraph not found", { status: 404 });
+  }
+
+  return res.data;
+}

+ 18 - 0
dashboard-v6/src/api/task.ts

@@ -12,6 +12,8 @@
             $table->timestamps();
  */
 
+import type { LoaderFunctionArgs } from "react-router";
+import { get } from "../request";
 import type { TPrivacy } from "./ai";
 import type { IStudio, IUser } from "./Auth";
 
@@ -257,3 +259,19 @@ export interface IProjectTreeResponse {
   message: string;
   data: { rows: IProjectTreeData[]; count: number };
 }
+
+export async function projectLoader({ params }: LoaderFunctionArgs) {
+  const projectId = params.projectId;
+
+  if (!projectId) {
+    throw new Response("Missing projectId", { status: 400 });
+  }
+
+  const res = await get<IProjectResponse>(`/api/v2/project/${projectId}`);
+
+  if (!res.ok) {
+    throw new Response("Project not found", { status: 404 });
+  }
+
+  return res.data;
+}

+ 2 - 0
dashboard-v6/src/locales/en-US.ts

@@ -9,6 +9,7 @@ import course from "./en-US/course";
 import message from "./en-US/message";
 import label from "./en-US/label";
 import error from "./en-US/error";
+import pages from "./en-US/pages";
 
 const items = {
   "columns.library.title": "Library",
@@ -57,6 +58,7 @@ const items = {
   ...message,
   ...label,
   ...error,
+  ...pages,
 };
 
 export default items;

+ 16 - 0
dashboard-v6/src/locales/en-US/pages.ts

@@ -0,0 +1,16 @@
+const items = {
+  "pages.dashboard.title": "Dashboard",
+  "pages.ai-model.edit.title": "Edit AI Model",
+  "pages.ai-model.log.title": "AI Model Log",
+  "pages.users.account-info.title": "Account Info",
+  "pages.users.change-password.title": "Change Password",
+  "pages.users.personal.title": "Personal",
+  "pages.tipitaka.title": "Tipitaka",
+  "pages.workspace.home.title": "Welcome to WikiPali",
+  "pages.team.title": "Team",
+  "pages.task.project.title": "Project",
+  "pages.tipitaka.chapter.title": "Chapter",
+  "pages.tipitaka.para.title": "Paragraph",
+};
+
+export default items;

+ 2 - 0
dashboard-v6/src/locales/zh-Hans.ts

@@ -9,6 +9,7 @@ import course from "./zh-Hans/course";
 import message from "./zh-Hans/message";
 import label from "./zh-Hans/label";
 import error from "./zh-Hans/error";
+import pages from "./zh-Hans/pages";
 
 const items = {
   "columns.library.title": "藏经阁",
@@ -57,6 +58,7 @@ const items = {
   ...message,
   ...label,
   ...error,
+  ...pages,
 };
 
 export default items;

+ 16 - 0
dashboard-v6/src/locales/zh-Hans/pages.ts

@@ -0,0 +1,16 @@
+const items = {
+  "pages.dashboard.title": "控制台",
+  "pages.ai-model.edit.title": "编辑 AI 模型",
+  "pages.ai-model.log.title": "AI 模型日志",
+  "pages.users.account-info.title": "账户信息",
+  "pages.users.change-password.title": "修改密码",
+  "pages.users.personal.title": "个人中心",
+  "pages.tipitaka.title": "圣典",
+  "pages.workspace.home.title": "欢迎来到wikipali",
+  "pages.team.title": "团队",
+  "pages.task.project.title": "项目",
+  "pages.tipitaka.chapter.title": "章节",
+  "pages.tipitaka.para.title": "段落",
+};
+
+export default items;

+ 9 - 2
dashboard-v6/src/pages/dashboard/index.tsx

@@ -1,6 +1,13 @@
+import { useIntl } from "react-intl";
+
 const Widget = () => {
-  // TODO
-  return <>dashboard index</>;
+  const intl = useIntl();
+  return (
+    <>
+      <title>{intl.formatMessage({ id: "pages.dashboard.title" })}</title>
+      dashboard index
+    </>
+  );
 };
 
 export default Widget;

+ 8 - 1
dashboard-v6/src/pages/dashboard/settings/ai-model/edit.tsx

@@ -1,4 +1,5 @@
 import { useParams } from "react-router";
+import { useIntl } from "react-intl";
 import AiModelEdit from "../../../../components/ai-model/AiModelEdit";
 
 import { useAppSelector } from "../../../../hooks";
@@ -7,8 +8,14 @@ import { currentUser } from "../../../../reducers/current-user";
 const Widget = () => {
   const currUser = useAppSelector(currentUser);
   const { id } = useParams();
+  const intl = useIntl();
 
-  return <AiModelEdit studioName={currUser?.realName} modelId={id} />;
+  return (
+    <>
+      <title>{intl.formatMessage({ id: "pages.ai-model.edit.title" })}</title>
+      <AiModelEdit studioName={currUser?.realName} modelId={id} />
+    </>
+  );
 };
 
 export default Widget;

+ 8 - 1
dashboard-v6/src/pages/dashboard/settings/ai-model/index.tsx

@@ -1,11 +1,18 @@
+import { useIntl } from "react-intl";
 import AiModelList from "../../../../components/ai-model/AiModelList";
 import { useAppSelector } from "../../../../hooks";
 import { currentUser } from "../../../../reducers/current-user";
 
 const Widget = () => {
   const currUser = useAppSelector(currentUser);
+  const intl = useIntl();
 
-  return <AiModelList studioName={currUser?.realName} />;
+  return (
+    <>
+      <title>{intl.formatMessage({ id: "buttons.ai-models" })}</title>
+      <AiModelList studioName={currUser?.realName} />
+    </>
+  );
 };
 
 export default Widget;

+ 8 - 1
dashboard-v6/src/pages/dashboard/settings/ai-model/log.tsx

@@ -1,11 +1,18 @@
 import { useParams } from "react-router";
+import { useIntl } from "react-intl";
 
 import AiModelLogList from "../../../../components/ai-model/AiModelLogList";
 
 const Widget = () => {
   const { id } = useParams();
+  const intl = useIntl();
 
-  return <AiModelLogList modelId={id} />;
+  return (
+    <>
+      <title>{intl.formatMessage({ id: "pages.ai-model.log.title" })}</title>
+      <AiModelLogList modelId={id} />
+    </>
+  );
 };
 
 export default Widget;

+ 9 - 2
dashboard-v6/src/pages/home.tsx

@@ -1,6 +1,13 @@
+import { useIntl } from "react-intl";
+
 const Widget = () => {
-  // TODO
-  return <>home</>;
+  const intl = useIntl();
+  return (
+    <>
+      <title>{intl.formatMessage({ id: "columns.library.home.title" })}</title>
+      home
+    </>
+  );
 };
 
 export default Widget;

+ 5 - 0
dashboard-v6/src/pages/users/account-info.tsx

@@ -1,3 +1,4 @@
+import { useIntl } from "react-intl";
 import ChangePassword from "../../components/users/ChangePassword";
 import Profile from "../../components/users/Profile";
 import { useAppSelector } from "../../hooks";
@@ -5,10 +6,14 @@ import { currentUser } from "../../reducers/current-user";
 
 const Widget = () => {
   const user = useAppSelector(currentUser);
+  const intl = useIntl();
   console.log(user?.avatar);
 
   return (
     <div>
+      <title>
+        {intl.formatMessage({ id: "pages.users.account-info.title" })}
+      </title>
       <h3> account info</h3>
       <ChangePassword />
       <Profile />

+ 11 - 1
dashboard-v6/src/pages/users/change-password.tsx

@@ -1,5 +1,15 @@
+import { useIntl } from "react-intl";
+
 const Widget = () => {
-  return <div>change password</div>;
+  const intl = useIntl();
+  return (
+    <div>
+      <title>
+        {intl.formatMessage({ id: "pages.users.change-password.title" })}
+      </title>
+      change password
+    </div>
+  );
 };
 
 export default Widget;

+ 14 - 9
dashboard-v6/src/pages/users/forgot-password.tsx

@@ -6,15 +6,20 @@ import { useIntl } from "react-intl";
 const Widget = () => {
   const intl = useIntl();
   return (
-    <Card
-      title={intl.formatMessage({
-        id: "buttons.forgot.password",
-      })}
-    >
-      <ForgotPassword />
-      <Divider />
-      <NonSignInSharedLinks />
-    </Card>
+    <>
+      <title>
+        {intl.formatMessage({ id: "nut.users.forgot-password.title" })}
+      </title>
+      <Card
+        title={intl.formatMessage({
+          id: "buttons.forgot.password",
+        })}
+      >
+        <ForgotPassword />
+        <Divider />
+        <NonSignInSharedLinks />
+      </Card>
+    </>
   );
 };
 

+ 3 - 0
dashboard-v6/src/pages/users/logs.tsx

@@ -1,8 +1,11 @@
+import { useIntl } from "react-intl";
 import Logs from "../../components/users/Logs";
 
 const Widget = () => {
+  const intl = useIntl();
   return (
     <div>
+      <title>{intl.formatMessage({ id: "nut.users.logs.title" })}</title>
       logs
       <br />
       <Logs />

+ 9 - 2
dashboard-v6/src/pages/users/personal/index.tsx

@@ -1,6 +1,13 @@
+import { useIntl } from "react-intl";
+
 const Widget = () => {
-  // TODO
-  return <>User personal</>;
+  const intl = useIntl();
+  return (
+    <>
+      <title>{intl.formatMessage({ id: "pages.users.personal.title" })}</title>
+      User personal
+    </>
+  );
 };
 
 export default Widget;

+ 12 - 9
dashboard-v6/src/pages/users/reset-password.tsx

@@ -9,15 +9,18 @@ const Widget = () => {
   const intl = useIntl();
 
   return (
-    <Card
-      title={intl.formatMessage({
-        id: "buttons.reset.password",
-      })}
-    >
-      <ResetPassword token={token} />
-      <Divider />
-      <NonSignInSharedLinks />
-    </Card>
+    <>
+      <title>{intl.formatMessage({ id: "buttons.reset.password" })}</title>
+      <Card
+        title={intl.formatMessage({
+          id: "buttons.reset.password",
+        })}
+      >
+        <ResetPassword token={token} />
+        <Divider />
+        <NonSignInSharedLinks />
+      </Card>
+    </>
   );
 };
 

+ 1 - 0
dashboard-v6/src/pages/users/sign-in.tsx

@@ -7,6 +7,7 @@ const Widget = () => {
   const intl = useIntl();
   return (
     <div>
+      <title>{intl.formatMessage({ id: "nut.users.sign-in.title" })}</title>
       <Card
         title={intl.formatMessage({
           id: "nut.users.sign-in.title",

+ 3 - 0
dashboard-v6/src/pages/users/sign-in2.tsx

@@ -3,6 +3,7 @@ import { LoginForm, ProFormText } from "@ant-design/pro-components";
 import { message } from "antd";
 import { UserOutlined, LockOutlined } from "@ant-design/icons";
 import { useNavigate } from "react-router";
+import { useIntl } from "react-intl";
 
 interface loginRequest {
   username: string;
@@ -23,6 +24,7 @@ const fakeLogin = async (values: loginRequest) => {
 const Login: React.FC = () => {
   const [loading, setLoading] = useState(false);
   const navigate = useNavigate();
+  const intl = useIntl();
   const handleSubmit = async (values: loginRequest) => {
     setLoading(true);
     const ok = await fakeLogin(values);
@@ -47,6 +49,7 @@ const Login: React.FC = () => {
           "linear-gradient(135deg,#f0f5ff 0%, #ffffff 50%, #f0f5ff 100%)",
       }}
     >
+      <title>{intl.formatMessage({ id: "nut.users.sign-in.title" })}</title>
       <LoginForm
         title="登陆"
         subTitle="wikipali 巴利语文献编辑平台"

+ 11 - 8
dashboard-v6/src/pages/users/sign-up.tsx

@@ -9,14 +9,17 @@ const Widget = () => {
   const { token } = useParams(); //url 参数
 
   return (
-    <Card
-      title={intl.formatMessage({
-        id: "buttons.sign-up",
-      })}
-    >
-      <SignUp token={token} />
-      <SharedLinks />
-    </Card>
+    <>
+      <title>{intl.formatMessage({ id: "nut.users.sign-up.title" })}</title>
+      <Card
+        title={intl.formatMessage({
+          id: "buttons.sign-up",
+        })}
+      >
+        <SignUp token={token} />
+        <SharedLinks />
+      </Card>
+    </>
   );
 };
 

+ 10 - 1
dashboard-v6/src/pages/workspace/anthology/edit.tsx

@@ -1,4 +1,5 @@
-import { useParams } from "react-router";
+import { useMatches, useParams } from "react-router";
+import { useIntl } from "react-intl";
 
 import AnthologyTocEdit from "../../../components/anthology/AnthologyTocEdit";
 import { useAppSelector } from "../../../hooks";
@@ -7,9 +8,17 @@ import { currentUser } from "../../../reducers/current-user";
 const Widget = () => {
   const { anthologyId } = useParams(); //url 参数
   const user = useAppSelector(currentUser);
+  const intl = useIntl();
+  const matches = useMatches() as {
+    data?: { title?: string; name?: string; word?: string };
+  }[];
+  const data = [...matches].reverse().find((m) => m.data)?.data;
+  const name = data?.title ?? data?.name ?? data?.word;
+  const prefix = intl.formatMessage({ id: "columns.studio.anthology.title" });
 
   return (
     <>
+      <title>{name ? `${prefix}-${name}` : prefix}</title>
       <AnthologyTocEdit id={anthologyId} editorStudioName={user?.realName} />
     </>
   );

+ 5 - 1
dashboard-v6/src/pages/workspace/anthology/index.tsx

@@ -1,3 +1,4 @@
+import { useIntl } from "react-intl";
 import { useAppSelector } from "../../../hooks";
 import { currentUser } from "../../../reducers/current-user";
 import AnthologyList from "../../../components/anthology/AnthologyList";
@@ -7,6 +8,7 @@ const Widget = () => {
   const user = useAppSelector(currentUser);
   const studioName = user?.realName;
   const navigate = useNavigate();
+  const intl = useIntl();
 
   const [searchParams, setSearchParams] = useSearchParams();
 
@@ -35,7 +37,9 @@ const Widget = () => {
   console.debug("anthology list", studioName);
   return (
     <>
-      <title>anthology</title>
+      <title>
+        {intl.formatMessage({ id: "columns.studio.anthology.title" })}
+      </title>
       <AnthologyList
         tab={tab}
         page={page}

+ 10 - 2
dashboard-v6/src/pages/workspace/anthology/show.tsx

@@ -1,4 +1,5 @@
-import { useNavigate, useParams, useSearchParams } from "react-router";
+import { useMatches, useNavigate, useParams, useSearchParams } from "react-router";
+import { useIntl } from "react-intl";
 
 import AnthologyReader from "../../../components/anthology/AnthologyReader";
 
@@ -6,13 +7,20 @@ const Widget = () => {
   const { anthologyId } = useParams(); //url 参数
   const navigate = useNavigate();
   const [searchParams] = useSearchParams();
+  const intl = useIntl();
+  const matches = useMatches() as {
+    data?: { title?: string; name?: string; word?: string };
+  }[];
+  const data = [...matches].reverse().find((m) => m.data)?.data;
+  const name = data?.title ?? data?.name ?? data?.word;
+  const prefix = intl.formatMessage({ id: "columns.studio.anthology.title" });
 
   const channelId = searchParams.get("channel");
   const channels = channelId ? channelId.split("_") : undefined;
 
   return (
     <>
-      <title>{"anthology-"}</title>
+      <title>{name ? `${prefix}-${name}` : prefix}</title>
       <AnthologyReader
         channels={channels}
         id={anthologyId}

+ 5 - 1
dashboard-v6/src/pages/workspace/article/index.tsx

@@ -1,3 +1,4 @@
+import { useIntl } from "react-intl";
 import { useAppSelector } from "../../../hooks";
 import { currentUser } from "../../../reducers/current-user";
 import { useSearchParams } from "react-router";
@@ -6,6 +7,7 @@ import ArticleList from "../../../components/article/ArticleList";
 const Widget = () => {
   const user = useAppSelector(currentUser);
   const studioName = user?.realName;
+  const intl = useIntl();
 
   const [searchParams, setSearchParams] = useSearchParams();
 
@@ -34,7 +36,9 @@ const Widget = () => {
   console.debug("article list", studioName);
   return (
     <>
-      <title>article</title>
+      <title>
+        {intl.formatMessage({ id: "columns.studio.article.title" })}
+      </title>
       <ArticleList
         tab={tab}
         page={page}

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

@@ -1,4 +1,5 @@
-import { useNavigate, useParams, useSearchParams } from "react-router";
+import { useMatches, useNavigate, useParams, useSearchParams } from "react-router";
+import { useIntl } from "react-intl";
 import type { ArticleMode } from "../../../api/article";
 import ArticleEditor from "../../../features/editor/Article";
 
@@ -6,13 +7,22 @@ const Widget = () => {
   const { articleId, anthologyId } = useParams();
   const [searchParams, setSearchParams] = useSearchParams();
   const navigate = useNavigate();
+  const intl = useIntl();
+  const matches = useMatches() as {
+    data?: { title?: string; name?: string; word?: string };
+  }[];
+  const data = [...matches].reverse().find((m) => m.data)?.data;
+  const name = data?.title ?? data?.name ?? data?.word;
+  const prefix = intl.formatMessage({ id: "columns.studio.article.title" });
 
   const mode = searchParams.get("mode") ?? "read";
   const channelId = searchParams.get("channel");
   const anthology = searchParams.get("anthology");
 
   return (
-    <ArticleEditor
+    <>
+      <title>{name ? `${prefix}-${name}` : prefix}</title>
+      <ArticleEditor
       articleId={articleId}
       anthologyId={anthologyId}
       anthology={anthology}
@@ -49,7 +59,8 @@ const Widget = () => {
         newParams.set("channel", channelsParams);
         setSearchParams(newParams);
       }}
-    />
+      />
+    </>
   );
 };
 

+ 9 - 2
dashboard-v6/src/pages/workspace/channel/list.tsx

@@ -1,4 +1,5 @@
 import { useNavigate } from "react-router";
+import { useIntl } from "react-intl";
 
 import ChannelTable from "../../../components/channel/ChannelTable";
 import type { IChannel } from "../../../api/channel";
@@ -9,15 +10,21 @@ const Widget = () => {
   const user = useAppSelector(currentUser);
   const studioName = user?.realName;
   const navigate = useNavigate();
+  const intl = useIntl();
   console.debug("channel list", studioName);
   return (
-    <ChannelTable
+    <>
+      <title>
+        {intl.formatMessage({ id: "columns.studio.channel.title" })}
+      </title>
+      <ChannelTable
       studioName={studioName}
       onSelect={(channel: IChannel) => {
         const url = `/workspace/channel/${channel.id}`;
         navigate(url);
       }}
-    />
+      />
+    </>
   );
 };
 

+ 4 - 1
dashboard-v6/src/pages/workspace/channel/setting.tsx

@@ -20,13 +20,16 @@ const Widget = () => {
   const { type } = useParams();
   const { id } = useParams();
   const [title, setTitle] = useState("");
+  const channelTitle = intl.formatMessage({
+    id: "columns.studio.channel.title",
+  });
   const navigate = useNavigate();
   const user = useAppSelector(currentUser);
   const studioName = user?.realName;
 
   return (
     <>
-      <title>{"channel-" + title}</title>
+      <title>{title ? `${channelTitle}-${title}` : channelTitle}</title>
       <Card
         title={title}
         extra={

+ 4 - 1
dashboard-v6/src/pages/workspace/channel/show.tsx

@@ -19,6 +19,9 @@ const Widget = () => {
   const { channelId } = useParams(); //url 参数
   const [title, setTitle] = useState<string>();
   const intl = useIntl();
+  const channelTitle = intl.formatMessage({
+    id: "columns.studio.channel.title",
+  });
   // const [articleOpen, setArticleOpen] = useState(false);
   //const [param, setParam] = useState<IArticleParam>();
 
@@ -30,7 +33,7 @@ const Widget = () => {
   }, [channelId]);
   return (
     <>
-      <title>{"channel-" + title}</title>
+      <title>{title ? `${channelTitle}-${title}` : channelTitle}</title>
       <Card
         title={title}
         extra={

+ 7 - 2
dashboard-v6/src/pages/workspace/chat/index.tsx

@@ -10,6 +10,7 @@ import {
   XRequest,
 } from "@ant-design/x-sdk";
 import { Button, Flex, Tooltip } from "antd";
+import { useIntl } from "react-intl";
 import React from "react";
 
 /**
@@ -80,6 +81,7 @@ const role: BubbleListProps["role"] = {
 };
 
 const App = () => {
+  const intl = useIntl();
   const [content, setContent] = React.useState("");
   // 创建OpenAI聊天提供者:配置请求参数和模型
   // Create OpenAI chat provider: configure request parameters and model
@@ -199,7 +201,9 @@ const App = () => {
   };
 
   return (
-    <Flex vertical gap="middle">
+    <>
+      <title>{intl.formatMessage({ id: "labels.ai-assistant" })}</title>
+      <Flex vertical gap="middle">
       {/* 状态和控制区域:显示当前状态并提供操作按钮 */}
       {/* Status and control area: display current status and provide action buttons */}
       <Flex vertical gap="middle">
@@ -296,7 +300,8 @@ const App = () => {
           setContent("");
         }}
       />
-    </Flex>
+      </Flex>
+    </>
   );
 };
 

+ 6 - 2
dashboard-v6/src/pages/workspace/course/edit.tsx

@@ -18,9 +18,14 @@ const Widget = () => {
   const studioName = user?.realName;
   const [title, setTitle] = useState("loading");
   const [selected, setSelected] = useState<string>();
+  const courseTitle = intl.formatMessage({
+    id: "columns.studio.course.title",
+  });
   return (
     <>
-      <title>{title}</title>
+      <title>
+        {title && title !== "loading" ? `${courseTitle}-${title}` : courseTitle}
+      </title>
       <Card>
         <Tabs
           defaultActiveKey="info"
@@ -34,7 +39,6 @@ const Widget = () => {
                   courseId={courseId}
                   onTitleChange={(title: string) => {
                     setTitle(title);
-                    document.title = `${title}`;
                   }}
                 />
               ),

+ 10 - 1
dashboard-v6/src/pages/workspace/course/index.tsx

@@ -1,3 +1,4 @@
+import { useIntl } from "react-intl";
 import { useAppSelector } from "../../../hooks";
 import { currentUser } from "../../../reducers/current-user";
 
@@ -6,7 +7,15 @@ import List from "../../../components/course/List";
 const Widget = () => {
   const user = useAppSelector(currentUser);
   const studioName = user?.realName;
-  return <List studioName={studioName} />;
+  const intl = useIntl();
+  return (
+    <>
+      <title>
+        {intl.formatMessage({ id: "columns.studio.course.title" })}
+      </title>
+      <List studioName={studioName} />
+    </>
+  );
 };
 
 export default Widget;

+ 15 - 2
dashboard-v6/src/pages/workspace/course/show.tsx

@@ -1,12 +1,25 @@
 //课程详情页面
-import { useParams } from "react-router";
+import { useMatches, useParams } from "react-router";
+import { useIntl } from "react-intl";
 
 import Course from "../../../components/course/Course";
 
 const Widget = () => {
   const { courseId } = useParams(); //url 参数
+  const intl = useIntl();
+  const matches = useMatches() as {
+    data?: { title?: string; name?: string; word?: string };
+  }[];
+  const data = [...matches].reverse().find((m) => m.data)?.data;
+  const name = data?.title ?? data?.name ?? data?.word;
+  const prefix = intl.formatMessage({ id: "columns.studio.course.title" });
 
-  return <Course id={courseId} />;
+  return (
+    <>
+      <title>{name ? `${prefix}-${name}` : prefix}</title>
+      <Course id={courseId} />
+    </>
+  );
 };
 
 export default Widget;

+ 15 - 2
dashboard-v6/src/pages/workspace/course/textbook.tsx

@@ -1,12 +1,25 @@
 //课程详情页面
-import { useParams } from "react-router";
+import { useMatches, useParams } from "react-router";
+import { useIntl } from "react-intl";
 
 import TextBookEditor from "../../../features/editor/TextBook";
 
 const Widget = () => {
   const { courseId, articleId } = useParams(); //url 参数
+  const intl = useIntl();
+  const matches = useMatches() as {
+    data?: { title?: string; name?: string; word?: string };
+  }[];
+  const data = [...matches].reverse().find((m) => m.data)?.data;
+  const name = data?.title ?? data?.name ?? data?.word;
+  const prefix = intl.formatMessage({ id: "columns.studio.course.title" });
 
-  return <TextBookEditor courseId={courseId} articleId={articleId} />;
+  return (
+    <>
+      <title>{name ? `${prefix}-${name}` : prefix}</title>
+      <TextBookEditor courseId={courseId} articleId={articleId} />
+    </>
+  );
 };
 
 export default Widget;

+ 5 - 1
dashboard-v6/src/pages/workspace/dict/list.tsx

@@ -1,3 +1,4 @@
+import { useIntl } from "react-intl";
 import UserDictList from "../../../components/dict/UserDictList";
 import { useAppSelector } from "../../../hooks";
 import { currentUser } from "../../../reducers/current-user";
@@ -5,9 +6,12 @@ import { currentUser } from "../../../reducers/current-user";
 const Widget = () => {
   const user = useAppSelector(currentUser);
   const studioName = user?.realName;
+  const intl = useIntl();
   return (
     <div>
-      <title>dict</title>
+      <title>
+        {intl.formatMessage({ id: "columns.studio.userdict.title" })}
+      </title>
       <UserDictList studioName={studioName} />
     </div>
   );

+ 5 - 1
dashboard-v6/src/pages/workspace/driver/list.tsx

@@ -1,3 +1,4 @@
+import { useIntl } from "react-intl";
 import AttachmentList from "../../../components/attachment/AttachmentList";
 import { useAppSelector } from "../../../hooks";
 import { currentUser } from "../../../reducers/current-user";
@@ -5,10 +6,13 @@ import { currentUser } from "../../../reducers/current-user";
 const Widget = () => {
   const user = useAppSelector(currentUser);
   const studioName = user?.realName;
+  const intl = useIntl();
 
   return (
     <div>
-      <title>driver</title>
+      <title>
+        {intl.formatMessage({ id: "columns.studio.attachment.title" })}
+      </title>
       <AttachmentList studioName={studioName} />
     </div>
   );

+ 3 - 1
dashboard-v6/src/pages/workspace/home.tsx

@@ -1,5 +1,6 @@
 // src/pages/workspace/home.tsx
 import { useEffect, useState, type CSSProperties } from "react";
+import { useIntl } from "react-intl";
 import WorkspaceHero from "../../components/workspace/home/WorkspaceHero";
 import SectionPanel from "../../components/workspace/home/SectionPanel";
 import ModuleGrid from "../../components/workspace/home/ModuleGrid";
@@ -15,6 +16,7 @@ export default function WorkspaceHome() {
   const [recentItems, setRecentItems] = useState<RecentItem[]>([]);
   const user = useAppSelector(currentUser);
   const navigate = useNavigate();
+  const intl = useIntl();
 
   useEffect(() => {
     if (!user) {
@@ -26,7 +28,7 @@ export default function WorkspaceHome() {
 
   return (
     <div style={styles.page}>
-      <title>欢迎来到wikipali</title>
+      <title>{intl.formatMessage({ id: "pages.workspace.home.title" })}</title>
       <WorkspaceHero />
 
       <div style={styles.content}>

+ 10 - 1
dashboard-v6/src/pages/workspace/invite/index.tsx

@@ -1,3 +1,4 @@
+import { useIntl } from "react-intl";
 import InviteList from "../../../components/invite/InviteList";
 import { useAppSelector } from "../../../hooks";
 import { currentUser } from "../../../reducers/current-user";
@@ -5,8 +6,16 @@ import { currentUser } from "../../../reducers/current-user";
 const Widget = () => {
   const user = useAppSelector(currentUser);
   const studioName = user?.realName;
+  const intl = useIntl();
 
-  return <InviteList studioName={studioName} />;
+  return (
+    <>
+      <title>
+        {intl.formatMessage({ id: "columns.studio.invite.title" })}
+      </title>
+      <InviteList studioName={studioName} />
+    </>
+  );
 };
 
 export default Widget;

+ 15 - 2
dashboard-v6/src/pages/workspace/tag/edit.tsx

@@ -1,4 +1,5 @@
-import { useParams } from "react-router";
+import { useMatches, useParams } from "react-router";
+import { useIntl } from "react-intl";
 
 import TagCreate from "../../../components/tag/TagCreate";
 import { useAppSelector } from "../../../hooks";
@@ -8,8 +9,20 @@ const Widget = () => {
   const { tagId } = useParams(); //url 参数
   const user = useAppSelector(currentUser);
   const studioName = user?.realName;
+  const intl = useIntl();
+  const matches = useMatches() as {
+    data?: { title?: string; name?: string; word?: string };
+  }[];
+  const data = [...matches].reverse().find((m) => m.data)?.data;
+  const name = data?.title ?? data?.name ?? data?.word;
+  const prefix = intl.formatMessage({ id: "columns.studio.tag.title" });
 
-  return <TagCreate studio={studioName} tagId={tagId} />;
+  return (
+    <>
+      <title>{name ? `${prefix}-${name}` : prefix}</title>
+      <TagCreate studio={studioName} tagId={tagId} />
+    </>
+  );
 };
 
 export default Widget;

+ 12 - 7
dashboard-v6/src/pages/workspace/tag/index.tsx

@@ -1,4 +1,5 @@
 import { useNavigate } from "react-router";
+import { useIntl } from "react-intl";
 
 import TagList from "../../../components/tag/TagList";
 
@@ -9,14 +10,18 @@ const Widget = () => {
   const user = useAppSelector(currentUser);
   const studioName = user?.realName;
   const navigate = useNavigate();
+  const intl = useIntl();
   return (
-    <TagList
-      studioName={studioName}
-      onSelect={(tag) => {
-        const url = `/workspace/tag/${tag.id}`;
-        navigate(url);
-      }}
-    />
+    <>
+      <title>{intl.formatMessage({ id: "columns.studio.tag.title" })}</title>
+      <TagList
+        studioName={studioName}
+        onSelect={(tag) => {
+          const url = `/workspace/tag/${tag.id}`;
+          navigate(url);
+        }}
+      />
+    </>
   );
 };
 

+ 15 - 2
dashboard-v6/src/pages/workspace/tag/show.tsx

@@ -1,11 +1,24 @@
-import { useParams } from "react-router";
+import { useMatches, useParams } from "react-router";
+import { useIntl } from "react-intl";
 
 import TagShow from "../../../components/tag/TagShow";
 
 const Widget = () => {
   const { tagId } = useParams(); //url 参数
+  const intl = useIntl();
+  const matches = useMatches() as {
+    data?: { title?: string; name?: string; word?: string };
+  }[];
+  const data = [...matches].reverse().find((m) => m.data)?.data;
+  const name = data?.title ?? data?.name ?? data?.word;
+  const prefix = intl.formatMessage({ id: "columns.studio.tag.title" });
 
-  return <TagShow tagId={tagId} />;
+  return (
+    <>
+      <title>{name ? `${prefix}-${name}` : prefix}</title>
+      <TagShow tagId={tagId} />
+    </>
+  );
 };
 
 export default Widget;

+ 3 - 1
dashboard-v6/src/pages/workspace/task/hall.tsx

@@ -1,12 +1,14 @@
+import { useIntl } from "react-intl";
 import TaskList from "../../../components/task/TaskList";
 import { useAppSelector } from "../../../hooks";
 import { currentUser } from "../../../reducers/current-user";
 
 const Widget = () => {
   const currUser = useAppSelector(currentUser);
+  const intl = useIntl();
   return currUser ? (
     <>
-      <title>task hall</title>
+      <title>{intl.formatMessage({ id: "labels.task.hall" })}</title>
       <TaskList
         studioName={currUser.realName}
         status={["published"]}

+ 3 - 1
dashboard-v6/src/pages/workspace/task/pending.tsx

@@ -1,12 +1,14 @@
+import { useIntl } from "react-intl";
 import TaskList from "../../../components/task/TaskList";
 import { useAppSelector } from "../../../hooks";
 import { currentUser } from "../../../reducers/current-user";
 
 const Widget = () => {
   const currUser = useAppSelector(currentUser);
+  const intl = useIntl();
   return currUser ? (
     <>
-      <title>task hall</title>
+      <title>{intl.formatMessage({ id: "labels.task.mine" })}</title>
       <TaskList
         studioName={currUser.realName}
         status={["published"]}

+ 15 - 2
dashboard-v6/src/pages/workspace/task/project-edit.tsx

@@ -1,4 +1,5 @@
-import { useParams } from "react-router";
+import { useMatches, useParams } from "react-router";
+import { useIntl } from "react-intl";
 
 import ProjectEdit from "../../../components/task/ProjectEdit";
 import { useAppSelector } from "../../../hooks";
@@ -8,8 +9,20 @@ const Widget = () => {
   const currUser = useAppSelector(currentUser);
 
   const { projectId } = useParams();
+  const intl = useIntl();
+  const matches = useMatches() as {
+    data?: { title?: string; name?: string; word?: string };
+  }[];
+  const data = [...matches].reverse().find((m) => m.data)?.data;
+  const name = data?.title ?? data?.name ?? data?.word;
+  const prefix = intl.formatMessage({ id: "pages.task.project.title" });
 
-  return <ProjectEdit studioName={currUser?.realName} projectId={projectId} />;
+  return (
+    <>
+      <title>{name ? `${prefix}-${name}` : prefix}</title>
+      <ProjectEdit studioName={currUser?.realName} projectId={projectId} />
+    </>
+  );
 };
 
 export default Widget;

+ 11 - 2
dashboard-v6/src/pages/workspace/task/project.tsx

@@ -1,4 +1,5 @@
-import { useNavigate, useParams } from "react-router";
+import { useMatches, useNavigate, useParams } from "react-router";
+import { useIntl } from "react-intl";
 
 import ProjectWithTasks from "../../../components/task/ProjectWithTasks";
 import { useAppSelector } from "../../../hooks";
@@ -9,9 +10,17 @@ const Widget = () => {
 
   const { projectId } = useParams();
   const navigate = useNavigate();
+  const intl = useIntl();
+  const matches = useMatches() as {
+    data?: { title?: string; name?: string; word?: string };
+  }[];
+  const data = [...matches].reverse().find((m) => m.data)?.data;
+  const name = data?.title ?? data?.name ?? data?.word;
+  const prefix = intl.formatMessage({ id: "pages.task.project.title" });
+
   return (
     <>
-      <title>project</title>
+      <title>{name ? `${prefix}-${name}` : prefix}</title>
       <ProjectWithTasks
         studioName={currUser?.realName}
         projectId={projectId}

+ 3 - 1
dashboard-v6/src/pages/workspace/task/projects.tsx

@@ -1,13 +1,15 @@
+import { useIntl } from "react-intl";
 import TaskProjects from "../../../components/task/ProjectTable";
 import { useAppSelector } from "../../../hooks";
 import { currentUser } from "../../../reducers/current-user";
 
 const Widget = () => {
   const currUser = useAppSelector(currentUser);
+  const intl = useIntl();
 
   return (
     <>
-      <title>project</title>
+      <title>{intl.formatMessage({ id: "labels.task.my.project" })}</title>
       <TaskProjects studioName={currUser?.realName} />
     </>
   );

+ 8 - 1
dashboard-v6/src/pages/workspace/task/tasks.tsx

@@ -1,11 +1,18 @@
+import { useIntl } from "react-intl";
 import MyTasks from "../../../components/task/MyTasks";
 import { useAppSelector } from "../../../hooks";
 import { currentUser } from "../../../reducers/current-user";
 
 const Widget = () => {
   const currUser = useAppSelector(currentUser);
+  const intl = useIntl();
 
-  return <MyTasks studioName={currUser?.realName} />;
+  return (
+    <>
+      <title>{intl.formatMessage({ id: "labels.task.mine" })}</title>
+      <MyTasks studioName={currUser?.realName} />
+    </>
+  );
 };
 
 export default Widget;

+ 8 - 1
dashboard-v6/src/pages/workspace/task/workflow.tsx

@@ -1,11 +1,18 @@
+import { useIntl } from "react-intl";
 import Workflow from "../../../components/task/Workflow";
 import { useAppSelector } from "../../../hooks";
 import { currentUser } from "../../../reducers/current-user";
 
 const Widget = () => {
   const currUser = useAppSelector(currentUser);
+  const intl = useIntl();
 
-  return <Workflow studioName={currUser?.realName} />;
+  return (
+    <>
+      <title>{intl.formatMessage({ id: "labels.task.workflows" })}</title>
+      <Workflow studioName={currUser?.realName} />
+    </>
+  );
 };
 
 export default Widget;

+ 10 - 2
dashboard-v6/src/pages/workspace/team/edit.tsx

@@ -1,13 +1,21 @@
-import { useParams } from "react-router";
+import { useMatches, useParams } from "react-router";
+import { useIntl } from "react-intl";
 
 import GroupEdit from "../../../features/group/GroupEdit";
 
 const Widget = () => {
   const { teamId } = useParams(); //url 参数
+  const intl = useIntl();
+  const matches = useMatches() as {
+    data?: { title?: string; name?: string; word?: string };
+  }[];
+  const data = [...matches].reverse().find((m) => m.data)?.data;
+  const name = data?.title ?? data?.name ?? data?.word;
+  const prefix = intl.formatMessage({ id: "pages.team.title" });
 
   return (
     <>
-      <title>Team Space</title>
+      <title>{name ? `${prefix}-${name}` : prefix}</title>
       <GroupEdit groupId={teamId} />
     </>
   );

+ 3 - 1
dashboard-v6/src/pages/workspace/team/index.tsx

@@ -1,4 +1,5 @@
 import { useNavigate } from "react-router";
+import { useIntl } from "react-intl";
 
 import { useAppSelector } from "../../../hooks";
 import { currentUser } from "../../../reducers/current-user";
@@ -8,10 +9,11 @@ const Widget = () => {
   const user = useAppSelector(currentUser);
   const studioName = user?.realName;
   const navigate = useNavigate();
+  const intl = useIntl();
   console.debug("channel list", studioName);
   return (
     <>
-      <title>Team Space</title>
+      <title>{intl.formatMessage({ id: "pages.team.title" })}</title>
       <GroupList
         studioName={studioName}
         onSelect={(id) => {

+ 10 - 2
dashboard-v6/src/pages/workspace/team/show.tsx

@@ -1,14 +1,22 @@
-import { useNavigate, useParams } from "react-router";
+import { useMatches, useNavigate, useParams } from "react-router";
+import { useIntl } from "react-intl";
 
 import GroupShow from "../../../features/group/GroupShow";
 
 const Widget = () => {
   const { teamId } = useParams(); //url 参数
   const navigate = useNavigate();
+  const intl = useIntl();
+  const matches = useMatches() as {
+    data?: { title?: string; name?: string; word?: string };
+  }[];
+  const data = [...matches].reverse().find((m) => m.data)?.data;
+  const name = data?.title ?? data?.name ?? data?.word;
+  const prefix = intl.formatMessage({ id: "pages.team.title" });
 
   return (
     <>
-      <title>Team Space</title>
+      <title>{name ? `${prefix}-${name}` : prefix}</title>
       <GroupShow
         teamId={teamId}
         onSetting={() => navigate(`/workspace/team/${teamId}/setting`)}

+ 18 - 7
dashboard-v6/src/pages/workspace/term/edit.tsx

@@ -1,18 +1,29 @@
-import { useNavigate, useParams } from "react-router";
+import { useMatches, useNavigate, useParams } from "react-router";
+import { useIntl } from "react-intl";
 
 import TermEdit from "../../../components/term/TermEdit";
 
 const Widget = () => {
   const { id } = useParams();
   const navigate = useNavigate();
+  const intl = useIntl();
+  const matches = useMatches() as {
+    data?: { title?: string; name?: string; word?: string };
+  }[];
+  const data = [...matches].reverse().find((m) => m.data)?.data;
+  const name = data?.title ?? data?.name ?? data?.word;
+  const prefix = intl.formatMessage({ id: "columns.studio.term.title" });
 
   return (
-    <TermEdit
-      id={id}
-      onUpdate={() => {
-        navigate(`/workspace/term/${id}`);
-      }}
-    />
+    <>
+      <title>{name ? `${prefix}-${name}` : prefix}</title>
+      <TermEdit
+        id={id}
+        onUpdate={() => {
+          navigate(`/workspace/term/${id}`);
+        }}
+      />
+    </>
   );
 };
 

+ 8 - 1
dashboard-v6/src/pages/workspace/term/list.tsx

@@ -1,3 +1,4 @@
+import { useIntl } from "react-intl";
 import TermList from "../../../components/term/TermList";
 import { useAppSelector } from "../../../hooks";
 import { currentUser } from "../../../reducers/current-user";
@@ -5,8 +6,14 @@ import { currentUser } from "../../../reducers/current-user";
 const Widget = () => {
   const user = useAppSelector(currentUser);
   const studioName = user?.realName;
+  const intl = useIntl();
 
-  return <TermList studioName={studioName} />;
+  return (
+    <>
+      <title>{intl.formatMessage({ id: "columns.studio.term.title" })}</title>
+      <TermList studioName={studioName} />
+    </>
+  );
 };
 
 export default Widget;

+ 18 - 7
dashboard-v6/src/pages/workspace/term/show.tsx

@@ -1,18 +1,29 @@
-import { useNavigate, useParams } from "react-router";
+import { useMatches, useNavigate, useParams } from "react-router";
+import { useIntl } from "react-intl";
 
 import TermEditor from "../../../features/editor/Term";
 
 const Widget = () => {
   const { id } = useParams();
   const navigate = useNavigate();
+  const intl = useIntl();
+  const matches = useMatches() as {
+    data?: { title?: string; name?: string; word?: string };
+  }[];
+  const data = [...matches].reverse().find((m) => m.data)?.data;
+  const name = data?.title ?? data?.name ?? data?.word;
+  const prefix = intl.formatMessage({ id: "columns.studio.term.title" });
 
   return (
-    <TermEditor
-      termId={id}
-      onEdit={() => {
-        navigate(`/workspace/term/${id}/edit`);
-      }}
-    />
+    <>
+      <title>{name ? `${prefix}-${name}` : prefix}</title>
+      <TermEditor
+        termId={id}
+        onEdit={() => {
+          navigate(`/workspace/term/${id}/edit`);
+        }}
+      />
+    </>
   );
 };
 

+ 3 - 0
dashboard-v6/src/pages/workspace/tipitaka/bypath.tsx

@@ -1,5 +1,6 @@
 import { useNavigate, useParams } from "react-router";
 import { useEffect, useMemo, useState } from "react";
+import { useIntl } from "react-intl";
 import { Affix, Drawer, Row, Col } from "antd";
 
 import type { IPaliBookListResponse } from "../../../api/Corpus";
@@ -42,6 +43,7 @@ const getTagByPath = (
 const Widget = () => {
   const { root, path } = useParams();
   const navigate = useNavigate();
+  const intl = useIntl();
 
   // 1. 基础数据状态(必须保留的 State)
   const [tocData, setTocData] = useState<IPaliBookListResponse[]>([]);
@@ -73,6 +75,7 @@ const Widget = () => {
 
   return (
     <>
+      <title>{intl.formatMessage({ id: "pages.tipitaka.title" })}</title>
       <Row>
         <Col flex="auto"></Col>
         <Col flex="1260px">

+ 15 - 2
dashboard-v6/src/pages/workspace/tipitaka/chapter.tsx

@@ -1,9 +1,11 @@
 import {
   useLocation,
+  useMatches,
   useNavigate,
   useParams,
   useSearchParams,
 } from "react-router";
+import { useIntl } from "react-intl";
 import type { ArticleMode } from "../../../api/article";
 import ChapterEditor from "../../../features/editor/Chapter";
 
@@ -12,11 +14,21 @@ const Widget = () => {
   const [searchParams] = useSearchParams();
   const navigate = useNavigate();
   const { search } = useLocation();
+  const intl = useIntl();
+  const matches = useMatches() as {
+    data?: { title?: string; name?: string; word?: string };
+  }[];
+  const data = [...matches].reverse().find((m) => m.data)?.data;
+  const name = data?.title ?? data?.name ?? data?.word;
+  const prefix = intl.formatMessage({ id: "pages.tipitaka.chapter.title" });
+
   const mode = searchParams.get("mode") ?? "read";
   const channelId = searchParams.get("channel");
 
   return (
-    <ChapterEditor
+    <>
+      <title>{name ? `${prefix}-${name}` : prefix}</title>
+      <ChapterEditor
       chapterId={id}
       mode={mode as ArticleMode}
       channelId={channelId}
@@ -39,7 +51,8 @@ const Widget = () => {
           navigate(`/${url}${urlSearch}`);
         }
       }}
-    />
+      />
+    </>
   );
 };
 

+ 15 - 2
dashboard-v6/src/pages/workspace/tipitaka/para.tsx

@@ -1,9 +1,11 @@
 import {
   useLocation,
+  useMatches,
   useNavigate,
   useParams,
   useSearchParams,
 } from "react-router";
+import { useIntl } from "react-intl";
 import type { ArticleMode } from "../../../api/article";
 import ParaEditor from "../../../features/editor/Paragraph";
 
@@ -12,11 +14,21 @@ const Widget = () => {
   const [searchParams] = useSearchParams();
   const navigate = useNavigate();
   const { search } = useLocation();
+  const intl = useIntl();
+  const matches = useMatches() as {
+    data?: { title?: string; name?: string; word?: string };
+  }[];
+  const data = [...matches].reverse().find((m) => m.data)?.data;
+  const name = data?.title ?? data?.name ?? data?.word;
+  const prefix = intl.formatMessage({ id: "pages.tipitaka.para.title" });
+
   const mode = searchParams.get("mode") ?? "read";
   const channelId = searchParams.get("channel");
 
   return (
-    <ParaEditor
+    <>
+      <title>{name ? `${prefix}-${name}` : prefix}</title>
+      <ParaEditor
       chapterId={id}
       mode={mode as ArticleMode}
       channelId={channelId}
@@ -39,7 +51,8 @@ const Widget = () => {
           navigate(`/${url}${urlSearch}`);
         }
       }}
-    />
+      />
+    </>
   );
 };
 

+ 5 - 0
dashboard-v6/src/pages/workspace/transfer/index.tsx

@@ -1,3 +1,4 @@
+import { useIntl } from "react-intl";
 import TransferList from "../../../components/transfer/TransferList";
 import { useAppSelector } from "../../../hooks";
 import { currentUser } from "../../../reducers/current-user";
@@ -5,8 +6,12 @@ import { currentUser } from "../../../reducers/current-user";
 const Widget = () => {
   const user = useAppSelector(currentUser);
   const studioName = user?.realName;
+  const intl = useIntl();
   return (
     <>
+      <title>
+        {intl.formatMessage({ id: "columns.studio.transfer.title" })}
+      </title>
       <TransferList studioName={studioName} />
     </>
   );

+ 2 - 1
dashboard-v6/src/routes/anthologyRoutes.ts

@@ -1,7 +1,7 @@
 // src/routes/anthologyRoutes.ts
 import { lazy } from "react";
 import type { RouteObject } from "react-router";
-import { anthologyLoader } from "../api/article";
+import { anthologyLoader, articleLoader } from "../api/article";
 
 const WorkspaceAnthologyList = lazy(
   () => import("../pages/workspace/anthology")
@@ -40,6 +40,7 @@ const anthologyRoutes: RouteObject[] = [
           },
           {
             path: ":articleId",
+            loader: articleLoader,
             handle: { id: "workspace.anthology.article", crumb: "article" },
             Component: WorkspaceArticleShow,
           },

+ 2 - 0
dashboard-v6/src/routes/taskRouters.ts

@@ -1,6 +1,7 @@
 // src/routes/taskRouters.ts
 import { lazy } from "react";
 import type { RouteObject } from "react-router";
+import { projectLoader } from "../api/task";
 
 // 懒加载页面组件
 const hall = lazy(() => import("../pages/workspace/task/hall"));
@@ -39,6 +40,7 @@ const taskRoutes: RouteObject[] = [
           {
             path: ":projectId",
             Component: project,
+            loader: projectLoader,
             handle: { id: "workspace.task.project", crumb: "project" }, // ✅ 加这里
             children: [
               {

+ 3 - 0
dashboard-v6/src/routes/tipitakaRoutes.ts

@@ -1,6 +1,7 @@
 // src/routes/tipitakaRoutes.ts
 import { lazy } from "react";
 import type { RouteObject } from "react-router";
+import { chapterLoader, paraLoader } from "../api/pali-text";
 
 const WorkspaceTipitaka = lazy(
   () => import("../pages/workspace/tipitaka/bypath")
@@ -46,6 +47,7 @@ const tipitakaRoutes: RouteObject[] = [
           {
             path: ":id",
             Component: WorkspaceTipitakaChapter,
+            loader: chapterLoader,
             handle: { id: "workspace.tipitaka", crumb: "chapter" },
           },
         ],
@@ -56,6 +58,7 @@ const tipitakaRoutes: RouteObject[] = [
           {
             path: ":id",
             Component: WorkspaceTipitakaPara,
+            loader: paraLoader,
             handle: { id: "workspace.tipitaka", crumb: "para" },
           },
         ],