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

Merge pull request #1038 from visuddhinanda/agile

组件目录结构改变。把studio library 下面目录移动到 components
visuddhinanda 3 лет назад
Родитель
Сommit
c0eeaab522
58 измененных файлов с 2059 добавлено и 2049 удалено
  1. 65 0
      dashboard/src/components/anthology/AnthologyCreate.tsx
  2. 61 0
      dashboard/src/components/article/ArticleCreate.tsx
  3. 4 3
      dashboard/src/components/article/ProTabs.tsx
  4. 3 3
      dashboard/src/components/article/TermShell.tsx
  5. 4 1
      dashboard/src/components/auth/setting/SettingArticle.tsx
  6. 0 0
      dashboard/src/components/blog/BlogNav.tsx
  7. 0 0
      dashboard/src/components/blog/Profile.tsx
  8. 0 0
      dashboard/src/components/blog/TimeLine.tsx
  9. 0 0
      dashboard/src/components/blog/TopArticleCard.tsx
  10. 0 0
      dashboard/src/components/blog/TopArticles.tsx
  11. 62 0
      dashboard/src/components/channel/ChannelCreate.tsx
  12. 0 0
      dashboard/src/components/channel/ChannelTypeSelect.tsx
  13. 1 1
      dashboard/src/components/corpus/ChapterCard.tsx
  14. 2 2
      dashboard/src/components/corpus/ChapterInChannel.tsx
  15. 0 0
      dashboard/src/components/dict/Confidence.tsx
  16. 0 0
      dashboard/src/components/dict/DictCreate.tsx
  17. 76 0
      dashboard/src/components/dict/DictEdit.tsx
  18. 120 0
      dashboard/src/components/dict/DictEditInner.tsx
  19. 0 0
      dashboard/src/components/dict/SelectCase.tsx
  20. 0 0
      dashboard/src/components/general/LangSelect.tsx
  21. 0 0
      dashboard/src/components/general/TimeShow.tsx
  22. 0 0
      dashboard/src/components/group/GroupCreate.tsx
  23. 0 0
      dashboard/src/components/group/GroupFile.tsx
  24. 0 0
      dashboard/src/components/group/GroupMember.tsx
  25. 15 12
      dashboard/src/components/nut/WbwTest.tsx
  26. 0 70
      dashboard/src/components/studio/anthology/AnthologyCreate.tsx
  27. 0 62
      dashboard/src/components/studio/article/ArticleCreate.tsx
  28. 0 66
      dashboard/src/components/studio/channel/ChannelCreate.tsx
  29. 0 78
      dashboard/src/components/studio/dict/DictEdit.tsx
  30. 0 121
      dashboard/src/components/studio/dict/DictEditInner.tsx
  31. 3 0
      dashboard/src/components/template/MdTpl.tsx
  32. 1 1
      dashboard/src/components/template/SentEdit/EditInfo.tsx
  33. 1 1
      dashboard/src/components/template/Term.tsx
  34. 19 9
      dashboard/src/components/template/Wbw/WbwCase.tsx
  35. 1 1
      dashboard/src/components/template/Wbw/WbwDetailBasic.tsx
  36. 6 4
      dashboard/src/components/template/Wbw/WbwWord.tsx
  37. 6 0
      dashboard/src/components/template/Wbw/wbw.css
  38. 30 19
      dashboard/src/components/template/WbwSent.tsx
  39. 2 2
      dashboard/src/components/term/TermCreate.tsx
  40. 1 1
      dashboard/src/components/term/TermEditInner.tsx
  41. 71 15
      dashboard/src/locales/zh-Hans/dict/index.ts
  42. 3 3
      dashboard/src/pages/library/article/show.tsx
  43. 9 9
      dashboard/src/pages/library/blog/anthology.tsx
  44. 1 1
      dashboard/src/pages/library/blog/course.tsx
  45. 26 26
      dashboard/src/pages/library/blog/overview.tsx
  46. 8 8
      dashboard/src/pages/library/blog/term.tsx
  47. 8 8
      dashboard/src/pages/library/blog/translation.tsx
  48. 142 144
      dashboard/src/pages/studio/anthology/edit.tsx
  49. 194 205
      dashboard/src/pages/studio/anthology/list.tsx
  50. 116 124
      dashboard/src/pages/studio/article/edit.tsx
  51. 211 224
      dashboard/src/pages/studio/article/list.tsx
  52. 73 80
      dashboard/src/pages/studio/channel/edit.tsx
  53. 243 255
      dashboard/src/pages/studio/channel/list.tsx
  54. 291 295
      dashboard/src/pages/studio/dict/list.tsx
  55. 149 162
      dashboard/src/pages/studio/group/list.tsx
  56. 29 31
      dashboard/src/pages/studio/group/show.tsx
  57. 1 1
      dashboard/src/pages/studio/term/list.tsx
  58. 1 1
      dashboard/src/reducers/command.ts

+ 65 - 0
dashboard/src/components/anthology/AnthologyCreate.tsx

@@ -0,0 +1,65 @@
+import { ProForm, ProFormText } from "@ant-design/pro-components";
+import { useIntl } from "react-intl";
+import { message } from "antd";
+
+import LangSelect from "../general/LangSelect";
+import { IAnthologyCreateRequest, IAnthologyResponse } from "../api/Article";
+import { post } from "../../request";
+
+interface IFormData {
+  title: string;
+  lang: string;
+  studio: string;
+}
+
+type IWidgetAnthologyCreate = {
+  studio?: string;
+};
+const Widget = (prop: IWidgetAnthologyCreate) => {
+  const intl = useIntl();
+
+  return (
+    <ProForm<IFormData>
+      onFinish={async (values: IFormData) => {
+        // TODO
+        values.studio = prop.studio ? prop.studio : "";
+        console.log(values);
+        const res = await post<IAnthologyCreateRequest, IAnthologyResponse>(
+          `/v2/anthology`,
+          values
+        );
+        console.log(res);
+        if (res.ok) {
+          message.success(intl.formatMessage({ id: "flashes.success" }));
+        } else {
+          message.error(res.message);
+        }
+      }}
+    >
+      <ProForm.Group>
+        <ProFormText
+          width="md"
+          name="title"
+          required
+          label={intl.formatMessage({
+            id: "forms.fields.title.label",
+          })}
+          rules={[
+            {
+              required: true,
+              message: intl.formatMessage({
+                id: "forms.message.title.required",
+              }),
+              max: 255,
+            },
+          ]}
+        />
+      </ProForm.Group>
+      <ProForm.Group>
+        <LangSelect />
+      </ProForm.Group>
+    </ProForm>
+  );
+};
+
+export default Widget;

+ 61 - 0
dashboard/src/components/article/ArticleCreate.tsx

@@ -0,0 +1,61 @@
+import { useIntl } from "react-intl";
+import { ProForm, ProFormText } from "@ant-design/pro-components";
+import { message } from "antd";
+
+import LangSelect from "../general/LangSelect";
+import { post } from "../../request";
+import { IArticleCreateRequest, IArticleResponse } from "../api/Article";
+
+interface IFormData {
+  title: string;
+  lang: string;
+  studio: string;
+}
+
+type IWidgetArticleCreate = {
+  studio?: string;
+};
+const Widget = (prop: IWidgetArticleCreate) => {
+  const intl = useIntl();
+
+  return (
+    <ProForm<IFormData>
+      onFinish={async (values: IFormData) => {
+        console.log(values);
+        values.studio = prop.studio ? prop.studio : "";
+        const res = await post<IArticleCreateRequest, IArticleResponse>(
+          `/v2/article`,
+          values
+        );
+        console.log(res);
+        if (res.ok) {
+          message.success(intl.formatMessage({ id: "flashes.success" }));
+        } else {
+          message.error(res.message);
+        }
+      }}
+    >
+      <ProForm.Group>
+        <ProFormText
+          width="md"
+          name="title"
+          required
+          label={intl.formatMessage({ id: "channel.name" })}
+          rules={[
+            {
+              required: true,
+              message: intl.formatMessage({
+                id: "channel.create.message.noname",
+              }),
+            },
+          ]}
+        />
+      </ProForm.Group>
+      <ProForm.Group>
+        <LangSelect />
+      </ProForm.Group>
+    </ProForm>
+  );
+};
+
+export default Widget;

+ 4 - 3
dashboard/src/components/library/article/ProTabs.tsx → dashboard/src/components/article/ProTabs.tsx

@@ -2,9 +2,10 @@ import { useRef, useState } from "react";
 import { Switch } from "antd";
 import { Radio, Space } from "antd";
 import { SettingOutlined, ShoppingCartOutlined } from "@ant-design/icons";
-import SettingArticle from "../../auth/setting/SettingArticle";
-import DictComponent from "../../dict/DictComponent";
-import { DictIcon, TermIcon } from "../../../assets/icon";
+
+import SettingArticle from "../auth/setting/SettingArticle";
+import DictComponent from "../dict/DictComponent";
+import { DictIcon, TermIcon } from "../../assets/icon";
 import TermShell from "./TermShell";
 
 const setting = (

+ 3 - 3
dashboard/src/components/library/article/TermShell.tsx → dashboard/src/components/article/TermShell.tsx

@@ -1,8 +1,8 @@
 import { useEffect, useState } from "react";
-import { useAppSelector } from "../../../hooks";
-import { message } from "../../../reducers/command";
+import { useAppSelector } from "../../hooks";
+import { message } from "../../reducers/command";
 
-import TermCreate, { IWidgetDictCreate } from "../../studio/term/TermCreate";
+import TermCreate, { IWidgetDictCreate } from "../term/TermCreate";
 
 const Widget = () => {
   const [termProps, setTermProps] = useState<IWidgetDictCreate>();

+ 4 - 1
dashboard/src/components/auth/setting/SettingArticle.tsx

@@ -5,12 +5,15 @@ import SettingItem from "./SettingItem";
 const Widget = () => {
   return (
     <div>
-      <Divider>翻译</Divider>
+      <Divider>阅读</Divider>
       <SettingItem data={SettingFind("setting.display.original")} />
       <SettingItem data={SettingFind("setting.layout.direction")} />
       <SettingItem data={SettingFind("setting.layout.paragraph")} />
       <SettingItem data={SettingFind("setting.pali.script1")} />
       <SettingItem data={SettingFind("setting.pali.script2")} />
+      <Divider>翻译</Divider>
+
+      <Divider>逐词解析</Divider>
     </div>
   );
 };

+ 0 - 0
dashboard/src/components/library/blog/BlogNav.tsx → dashboard/src/components/blog/BlogNav.tsx


+ 0 - 0
dashboard/src/components/library/blog/Profile.tsx → dashboard/src/components/blog/Profile.tsx


+ 0 - 0
dashboard/src/components/library/blog/TimeLine.tsx → dashboard/src/components/blog/TimeLine.tsx


+ 0 - 0
dashboard/src/components/library/blog/TopArticleCard.tsx → dashboard/src/components/blog/TopArticleCard.tsx


+ 0 - 0
dashboard/src/components/library/blog/TopArticles.tsx → dashboard/src/components/blog/TopArticles.tsx


+ 62 - 0
dashboard/src/components/channel/ChannelCreate.tsx

@@ -0,0 +1,62 @@
+import { useIntl } from "react-intl";
+import { ProForm, ProFormText } from "@ant-design/pro-components";
+import { message } from "antd";
+
+import ChannelTypeSelect from "./ChannelTypeSelect";
+import { post } from "../../request";
+import { IApiResponseChannel } from "../api/Channel";
+import LangSelect from "../general/LangSelect";
+
+interface IFormData {
+  name: string;
+  type: string;
+  lang: string;
+  studio: string;
+}
+
+type IWidgetChannelCreate = {
+  studio: string | undefined;
+};
+const Widget = (prop: IWidgetChannelCreate) => {
+  const intl = useIntl();
+
+  return (
+    <ProForm<IFormData>
+      onFinish={async (values: IFormData) => {
+        // TODO
+        console.log(values);
+        values.studio = prop.studio ? prop.studio : "";
+        const res: IApiResponseChannel = await post(`/v2/channel`, values);
+        console.log(res);
+        if (res.ok) {
+          message.success(intl.formatMessage({ id: "flashes.success" }));
+        } else {
+          message.error(res.message);
+        }
+      }}
+    >
+      <ProForm.Group>
+        <ProFormText
+          width="md"
+          name="name"
+          required
+          label={intl.formatMessage({ id: "channel.name" })}
+          rules={[
+            {
+              required: true,
+              message: intl.formatMessage({
+                id: "channel.create.message.noname",
+              }),
+            },
+          ]}
+        />
+      </ProForm.Group>
+      <ProForm.Group>
+        <ChannelTypeSelect />
+        <LangSelect />
+      </ProForm.Group>
+    </ProForm>
+  );
+};
+
+export default Widget;

+ 0 - 0
dashboard/src/components/studio/channel/ChannelTypeSelect.tsx → dashboard/src/components/channel/ChannelTypeSelect.tsx


+ 1 - 1
dashboard/src/components/corpus/ChapterCard.tsx

@@ -1,6 +1,6 @@
 import { Row, Col } from "antd";
 import { Typography } from "antd";
-import TimeShow from "../utilities/TimeShow";
+import TimeShow from "../general/TimeShow";
 import TocPath from "../corpus/TocPath";
 import TagArea from "../tag/TagArea";
 import type { TagNode } from "../tag/TagArea";

+ 2 - 2
dashboard/src/components/corpus/ChapterInChannel.tsx

@@ -1,9 +1,9 @@
-import { Col, Layout, Progress, Row, Space, Tabs } from "antd";
+import { Col, Progress, Row, Space, Tabs } from "antd";
 import { Typography } from "antd";
 import { LikeOutlined, EyeOutlined } from "@ant-design/icons";
 import { ChannelInfoProps } from "../api/Channel";
 import ChannelListItem from "../channel/ChannelListItem";
-import TimeShow from "../utilities/TimeShow";
+import TimeShow from "../general/TimeShow";
 import { useIntl } from "react-intl";
 import { Link } from "react-router-dom";
 

+ 0 - 0
dashboard/src/components/studio/Confidence.tsx → dashboard/src/components/dict/Confidence.tsx


+ 0 - 0
dashboard/src/components/studio/dict/DictCreate.tsx → dashboard/src/components/dict/DictCreate.tsx


+ 76 - 0
dashboard/src/components/dict/DictEdit.tsx

@@ -0,0 +1,76 @@
+import { ProForm } from "@ant-design/pro-components";
+
+import { useIntl } from "react-intl";
+import { message } from "antd";
+
+import DictEditInner from "./DictEditInner";
+import { IDictFormData } from "./DictCreate";
+import { IApiResponseDict, IDictlDataRequest } from "../api/Dict";
+import { get, put } from "../../request";
+import { useEffect } from "react";
+
+type IWidgetDictEdit = {
+  wordId: number;
+};
+const Widget = (prop: IWidgetDictEdit) => {
+  const intl = useIntl();
+  useEffect(() => {});
+
+  return (
+    <>
+      <ProForm<IDictFormData>
+        onFinish={async (values: IDictFormData) => {
+          // TODO
+          console.log(values);
+          const request: IDictlDataRequest = {
+            id: values.id,
+            word: values.word,
+            type: values.type,
+            grammar: values.grammar,
+            mean: values.meaning,
+            parent: values.parent,
+            note: values.note,
+            factors: values.factors,
+            factormean: values.factormeaning,
+            language: values.lang,
+            confidence: values.confidence,
+          };
+          const res = await put<IDictlDataRequest, IApiResponseDict>(
+            `/v2/userdict/${prop.wordId}`,
+            request
+          );
+          console.log(res);
+          if (res.ok) {
+            message.success(intl.formatMessage({ id: "flashes.success" }));
+          } else {
+            message.success(res.message);
+          }
+        }}
+        formKey="dict_edit"
+        request={async () => {
+          const res: IApiResponseDict = await get(
+            `/v2/userdict/${prop.wordId}`
+          );
+          return {
+            id: res.data.id,
+            wordId: res.data.id,
+            word: res.data.word,
+            type: res.data.type,
+            grammar: res.data.grammar,
+            parent: res.data.parent,
+            meaning: res.data.mean,
+            note: res.data.note,
+            factors: res.data.factors,
+            factormeaning: res.data.factormean,
+            lang: res.data.language,
+            confidence: res.data.confidence,
+          };
+        }}
+      >
+        <DictEditInner />
+      </ProForm>
+    </>
+  );
+};
+
+export default Widget;

+ 120 - 0
dashboard/src/components/dict/DictEditInner.tsx

@@ -0,0 +1,120 @@
+import {
+  ProForm,
+  ProFormText,
+  ProFormTextArea,
+} from "@ant-design/pro-components";
+
+import { useIntl } from "react-intl";
+
+import LangSelect from "../general/LangSelect";
+import SelectCase from "./SelectCase";
+import Confidence from "./Confidence";
+
+type IWidgetDictCreate = {
+  word?: string;
+};
+const Widget = (prop: IWidgetDictCreate) => {
+  const intl = useIntl();
+  /*
+	const onLangChange = (value: string) => {
+		console.log(`selected ${value}`);
+	};
+
+	const onLangSearch = (value: string) => {
+		console.log("search:", value);
+	};
+	*/
+  return (
+    <>
+      <ProForm.Group>
+        <ProFormText
+          width="md"
+          name="word"
+          initialValue={prop.word}
+          required
+          label={intl.formatMessage({ id: "dict.fields.word.label" })}
+          rules={[
+            {
+              required: true,
+              message: intl.formatMessage({
+                id: "channel.create.message.noname",
+              }),
+            },
+          ]}
+        />
+      </ProForm.Group>
+      <ProForm.Group>
+        <div>语法信息</div>
+        <SelectCase />
+      </ProForm.Group>
+      <ProForm.Group>
+        <ProFormText
+          width="md"
+          name="type"
+          label={intl.formatMessage({
+            id: "dict.fields.type.label",
+          })}
+        />
+        <ProFormText
+          width="md"
+          name="grammar"
+          label={intl.formatMessage({
+            id: "dict.fields.grammar.label",
+          })}
+        />
+      </ProForm.Group>
+      <ProForm.Group>
+        <ProFormText
+          width="md"
+          name="parent"
+          label={intl.formatMessage({
+            id: "dict.fields.parent.label",
+          })}
+        />
+      </ProForm.Group>
+      <ProForm.Group>
+        <LangSelect />
+      </ProForm.Group>
+      <ProForm.Group>
+        <ProFormText
+          width="md"
+          name="meaning"
+          label={intl.formatMessage({
+            id: "dict.fields.meaning.label",
+          })}
+        />
+      </ProForm.Group>
+      <ProForm.Group>
+        <ProFormText
+          width="md"
+          name="factors"
+          label={intl.formatMessage({
+            id: "dict.fields.factors.label",
+          })}
+        />
+      </ProForm.Group>
+      <ProForm.Group>
+        <ProFormText
+          width="md"
+          name="factormeaning"
+          label={intl.formatMessage({
+            id: "dict.fields.factormeaning.label",
+          })}
+        />
+      </ProForm.Group>
+      <ProForm.Group>
+        <ProFormTextArea
+          name="note"
+          label={intl.formatMessage({
+            id: "forms.fields.note.label",
+          })}
+        />
+      </ProForm.Group>
+      <ProForm.Group>
+        <Confidence />
+      </ProForm.Group>
+    </>
+  );
+};
+
+export default Widget;

+ 0 - 0
dashboard/src/components/studio/SelectCase.tsx → dashboard/src/components/dict/SelectCase.tsx


+ 0 - 0
dashboard/src/components/studio/LangSelect.tsx → dashboard/src/components/general/LangSelect.tsx


+ 0 - 0
dashboard/src/components/utilities/TimeShow.tsx → dashboard/src/components/general/TimeShow.tsx


+ 0 - 0
dashboard/src/components/studio/group/GroupCreate.tsx → dashboard/src/components/group/GroupCreate.tsx


+ 0 - 0
dashboard/src/components/studio/group/GroupFile.tsx → dashboard/src/components/group/GroupFile.tsx


+ 0 - 0
dashboard/src/components/studio/group/GroupMember.tsx → dashboard/src/components/group/GroupMember.tsx


+ 15 - 12
dashboard/src/components/nut/WbwTest.tsx

@@ -1,5 +1,5 @@
 import { IWbw } from "../template/Wbw/WbwWord";
-import WbwSent from "../template/WbwSent";
+import { WbwSentCtl } from "../template/WbwSent";
 
 const Widget = () => {
   let wbwData: IWbw[] = [];
@@ -22,18 +22,21 @@ const Widget = () => {
       confidence: 1,
     });
   }
+
   return (
-    <div style={{ width: 700 }}>
-      <WbwSent
-        data={wbwData}
-        display="block"
-        fields={{
-          meaning: true,
-          factors: true,
-          factorMeaning: true,
-          case: true,
-        }}
-      />
+    <div>
+      <div style={{ width: 700 }}>
+        <WbwSentCtl
+          data={wbwData}
+          display="block"
+          fields={{
+            meaning: true,
+            factors: true,
+            factorMeaning: true,
+            case: true,
+          }}
+        />
+      </div>
     </div>
   );
 };

+ 0 - 70
dashboard/src/components/studio/anthology/AnthologyCreate.tsx

@@ -1,70 +0,0 @@
-import {
-	ProForm,
-	ProFormText,
-	ProFormSelect,
-} from "@ant-design/pro-components";
-import { useIntl } from "react-intl";
-import { message } from "antd";
-import LangSelect from "../LangSelect";
-import { IAnthologyCreateRequest, IAnthologyResponse } from "../../api/Article";
-import { post } from "../../../request";
-
-interface IFormData {
-	title: string;
-	lang: string;
-	studio: string;
-}
-
-type IWidgetAnthologyCreate = {
-	studio?: string;
-};
-const Widget = (prop: IWidgetAnthologyCreate) => {
-	const intl = useIntl();
-
-	return (
-		<ProForm<IFormData>
-			onFinish={async (values: IFormData) => {
-				// TODO
-				values.studio = prop.studio ? prop.studio : "";
-				console.log(values);
-				const res = await post<
-					IAnthologyCreateRequest,
-					IAnthologyResponse
-				>(`/v2/anthology`, values);
-				console.log(res);
-				if (res.ok) {
-					message.success(
-						intl.formatMessage({ id: "flashes.success" })
-					);
-				} else {
-					message.error(res.message);
-				}
-			}}
-		>
-			<ProForm.Group>
-				<ProFormText
-					width="md"
-					name="title"
-					required
-					label={intl.formatMessage({
-						id: "forms.fields.title.label",
-					})}
-					rules={[
-						{
-							required: true,
-							message: intl.formatMessage({
-								id: "forms.message.title.required",
-							}),
-							max: 255,
-						},
-					]}
-				/>
-			</ProForm.Group>
-			<ProForm.Group>
-				<LangSelect />
-			</ProForm.Group>
-		</ProForm>
-	);
-};
-
-export default Widget;

+ 0 - 62
dashboard/src/components/studio/article/ArticleCreate.tsx

@@ -1,62 +0,0 @@
-import { ProForm, ProFormText } from "@ant-design/pro-components";
-import { useIntl } from "react-intl";
-import { message } from "antd";
-import LangSelect from "../LangSelect";
-import { post } from "../../../request";
-import { IArticleCreateRequest, IArticleResponse } from "../../api/Article";
-
-interface IFormData {
-	title: string;
-	lang: string;
-	studio: string;
-}
-
-type IWidgetArticleCreate = {
-	studio?: string;
-};
-const Widget = (prop: IWidgetArticleCreate) => {
-	const intl = useIntl();
-
-	return (
-		<ProForm<IFormData>
-			onFinish={async (values: IFormData) => {
-				console.log(values);
-				values.studio = prop.studio ? prop.studio : "";
-				const res = await post<IArticleCreateRequest, IArticleResponse>(
-					`/v2/article`,
-					values
-				);
-				console.log(res);
-				if (res.ok) {
-					message.success(
-						intl.formatMessage({ id: "flashes.success" })
-					);
-				} else {
-					message.error(res.message);
-				}
-			}}
-		>
-			<ProForm.Group>
-				<ProFormText
-					width="md"
-					name="title"
-					required
-					label={intl.formatMessage({ id: "channel.name" })}
-					rules={[
-						{
-							required: true,
-							message: intl.formatMessage({
-								id: "channel.create.message.noname",
-							}),
-						},
-					]}
-				/>
-			</ProForm.Group>
-			<ProForm.Group>
-				<LangSelect />
-			</ProForm.Group>
-		</ProForm>
-	);
-};
-
-export default Widget;

+ 0 - 66
dashboard/src/components/studio/channel/ChannelCreate.tsx

@@ -1,66 +0,0 @@
-import { ProForm, ProFormText } from "@ant-design/pro-components";
-import { useIntl } from "react-intl";
-import { message } from "antd";
-import ChannelTypeSelect from "./ChannelTypeSelect";
-import { post } from "../../../request";
-import { IApiResponseChannel } from "../../api/Channel";
-import LangSelect from "../LangSelect";
-
-interface IFormData {
-	name: string;
-	type: string;
-	lang: string;
-	studio: string;
-}
-
-type IWidgetChannelCreate = {
-	studio: string | undefined;
-};
-const Widget = (prop: IWidgetChannelCreate) => {
-	const intl = useIntl();
-
-	return (
-		<ProForm<IFormData>
-			onFinish={async (values: IFormData) => {
-				// TODO
-				console.log(values);
-				values.studio = prop.studio ? prop.studio : "";
-				const res: IApiResponseChannel = await post(
-					`/v2/channel`,
-					values
-				);
-				console.log(res);
-				if (res.ok) {
-					message.success(
-						intl.formatMessage({ id: "flashes.success" })
-					);
-				} else {
-					message.error(res.message);
-				}
-			}}
-		>
-			<ProForm.Group>
-				<ProFormText
-					width="md"
-					name="name"
-					required
-					label={intl.formatMessage({ id: "channel.name" })}
-					rules={[
-						{
-							required: true,
-							message: intl.formatMessage({
-								id: "channel.create.message.noname",
-							}),
-						},
-					]}
-				/>
-			</ProForm.Group>
-			<ProForm.Group>
-				<ChannelTypeSelect />
-				<LangSelect />
-			</ProForm.Group>
-		</ProForm>
-	);
-};
-
-export default Widget;

+ 0 - 78
dashboard/src/components/studio/dict/DictEdit.tsx

@@ -1,78 +0,0 @@
-import { ProForm } from "@ant-design/pro-components";
-
-import { useIntl } from "react-intl";
-import { message } from "antd";
-
-import DictEditInner from "./DictEditInner";
-import { IDictFormData } from "./DictCreate";
-import { IApiResponseDict, IDictlDataRequest } from "../../api/Dict";
-import { get, put } from "../../../request";
-import { useEffect } from "react";
-
-type IWidgetDictEdit = {
-	wordId: number;
-};
-const Widget = (prop: IWidgetDictEdit) => {
-	const intl = useIntl();
-	useEffect(() => {});
-
-	return (
-		<>
-			<ProForm<IDictFormData>
-				onFinish={async (values: IDictFormData) => {
-					// TODO
-					console.log(values);
-					const request: IDictlDataRequest = {
-						id: values.id,
-						word: values.word,
-						type: values.type,
-						grammar: values.grammar,
-						mean: values.meaning,
-						parent: values.parent,
-						note: values.note,
-						factors: values.factors,
-						factormean: values.factormeaning,
-						language: values.lang,
-						confidence: values.confidence,
-					};
-					const res = await put<IDictlDataRequest, IApiResponseDict>(
-						`/v2/userdict/${prop.wordId}`,
-						request
-					);
-					console.log(res);
-					if (res.ok) {
-						message.success(
-							intl.formatMessage({ id: "flashes.success" })
-						);
-					} else {
-						message.success(res.message);
-					}
-				}}
-				formKey="dict_edit"
-				request={async () => {
-					const res: IApiResponseDict = await get(
-						`/v2/userdict/${prop.wordId}`
-					);
-					return {
-						id: res.data.id,
-						wordId: res.data.id,
-						word: res.data.word,
-						type: res.data.type,
-						grammar: res.data.grammar,
-						parent: res.data.parent,
-						meaning: res.data.mean,
-						note: res.data.note,
-						factors: res.data.factors,
-						factormeaning: res.data.factormean,
-						lang: res.data.language,
-						confidence: res.data.confidence,
-					};
-				}}
-			>
-				<DictEditInner />
-			</ProForm>
-		</>
-	);
-};
-
-export default Widget;

+ 0 - 121
dashboard/src/components/studio/dict/DictEditInner.tsx

@@ -1,121 +0,0 @@
-import {
-	ProForm,
-	ProFormSlider,
-	ProFormText,
-	ProFormTextArea,
-} from "@ant-design/pro-components";
-
-import { useIntl } from "react-intl";
-
-import LangSelect from "../LangSelect";
-import SelectCase from "../SelectCase";
-import Confidence from "../Confidence";
-
-type IWidgetDictCreate = {
-	word?: string;
-};
-const Widget = (prop: IWidgetDictCreate) => {
-	const intl = useIntl();
-	/*
-	const onLangChange = (value: string) => {
-		console.log(`selected ${value}`);
-	};
-
-	const onLangSearch = (value: string) => {
-		console.log("search:", value);
-	};
-	*/
-	return (
-		<>
-			<ProForm.Group>
-				<ProFormText
-					width="md"
-					name="word"
-					initialValue={prop.word}
-					required
-					label={intl.formatMessage({ id: "dict.fields.word.label" })}
-					rules={[
-						{
-							required: true,
-							message: intl.formatMessage({
-								id: "channel.create.message.noname",
-							}),
-						},
-					]}
-				/>
-			</ProForm.Group>
-			<ProForm.Group>
-				<div>语法信息</div>
-				<SelectCase />
-			</ProForm.Group>
-			<ProForm.Group>
-				<ProFormText
-					width="md"
-					name="type"
-					label={intl.formatMessage({
-						id: "dict.fields.type.label",
-					})}
-				/>
-				<ProFormText
-					width="md"
-					name="grammar"
-					label={intl.formatMessage({
-						id: "dict.fields.grammar.label",
-					})}
-				/>
-			</ProForm.Group>
-			<ProForm.Group>
-				<ProFormText
-					width="md"
-					name="parent"
-					label={intl.formatMessage({
-						id: "dict.fields.parent.label",
-					})}
-				/>
-			</ProForm.Group>
-			<ProForm.Group>
-				<LangSelect />
-			</ProForm.Group>
-			<ProForm.Group>
-				<ProFormText
-					width="md"
-					name="meaning"
-					label={intl.formatMessage({
-						id: "dict.fields.meaning.label",
-					})}
-				/>
-			</ProForm.Group>
-			<ProForm.Group>
-				<ProFormText
-					width="md"
-					name="factors"
-					label={intl.formatMessage({
-						id: "dict.fields.factors.label",
-					})}
-				/>
-			</ProForm.Group>
-			<ProForm.Group>
-				<ProFormText
-					width="md"
-					name="factormeaning"
-					label={intl.formatMessage({
-						id: "dict.fields.factormeaning.label",
-					})}
-				/>
-			</ProForm.Group>
-			<ProForm.Group>
-				<ProFormTextArea
-					name="note"
-					label={intl.formatMessage({
-						id: "forms.fields.note.label",
-					})}
-				/>
-			</ProForm.Group>
-			<ProForm.Group>
-				<Confidence />
-			</ProForm.Group>
-		</>
-	);
-};
-
-export default Widget;

+ 3 - 0
dashboard/src/components/template/MdTpl.tsx

@@ -3,6 +3,7 @@ import Quote from "./Quote";
 import SentEdit from "./SentEdit";
 import SentRead from "./SentRead";
 import Term from "./Term";
+import WbwSent from "./WbwSent";
 import Wd from "./Wd";
 
 interface IWidgetMdTpl {
@@ -19,6 +20,8 @@ const Widget = ({ tpl, props }: IWidgetMdTpl) => {
       return <SentRead props={props ? props : ""} />;
     case "sentedit":
       return <SentEdit props={props ? props : ""} />;
+	  case "wbw_sent":
+		return <WbwSent props={props ? props : ""} />;
     case "wd":
       return <Wd props={props ? props : ""} />;
     case "quote":

+ 1 - 1
dashboard/src/components/template/SentEdit/EditInfo.tsx

@@ -1,7 +1,7 @@
 import { Typography } from "antd";
 import { Space } from "antd";
 import User from "../../auth/User";
-import TimeShow from "../../utilities/TimeShow";
+import TimeShow from "../../general/TimeShow";
 import { ISentence } from "../SentEdit";
 
 const { Text } = Typography;

+ 1 - 1
dashboard/src/components/template/Term.tsx

@@ -2,7 +2,7 @@ import { ProCard } from "@ant-design/pro-components";
 import { Button, Popover } from "antd";
 import { SearchOutlined } from "@ant-design/icons";
 import { Typography } from "antd";
-import TermCreate, { IWidgetDictCreate } from "../studio/term/TermCreate";
+import TermCreate, { IWidgetDictCreate } from "../term/TermCreate";
 import { command } from "../../reducers/command";
 import store from "../../store";
 

+ 19 - 9
dashboard/src/components/template/Wbw/WbwCase.tsx

@@ -13,6 +13,7 @@ interface IWidget {
 }
 const Widget = ({ data, onSplit }: IWidget) => {
   const intl = useIntl();
+  const showSplit: boolean = data.factors?.value.includes("+") ? true : false;
   return (
     <div className="wbw_word_item" style={{ display: "flex" }}>
       <Text type="secondary">
@@ -20,18 +21,27 @@ const Widget = ({ data, onSplit }: IWidget) => {
           {data.case?.value.map((item, id) => {
             return (
               <span key={id} className="case">
-                {intl.formatMessage({ id: `dict.fields.type.${item}.label` })}
+                {intl.formatMessage({
+                  id: `dict.fields.type.${item}.short.label`,
+                })}
               </span>
             );
           })}
-          <Button
-            icon={<SwapOutlined />}
-            onClick={() => {
-              if (typeof onSplit !== "undefined") {
-                onSplit();
-              }
-            }}
-          />
+          {showSplit ? (
+            <Button
+              className="wbw_split"
+              size="small"
+              shape="circle"
+              icon={<SwapOutlined />}
+              onClick={() => {
+                if (typeof onSplit !== "undefined") {
+                  onSplit(true);
+                }
+              }}
+            />
+          ) : (
+            <></>
+          )}
         </div>
       </Text>
     </div>

+ 1 - 1
dashboard/src/components/template/Wbw/WbwDetailBasic.tsx

@@ -3,7 +3,7 @@ import { useIntl } from "react-intl";
 import { Divider, Form, Select, Input } from "antd";
 import { Collapse } from "antd";
 
-import SelectCase from "../../studio/SelectCase";
+import SelectCase from "../../dict/SelectCase";
 import { IWbw } from "./WbwWord";
 import WbwMeaningSelect from "./WbwMeaningSelect";
 

+ 6 - 4
dashboard/src/components/template/Wbw/WbwWord.tsx

@@ -1,4 +1,4 @@
-import { useState } from "react";
+import { useState, useEffect } from "react";
 import WbwCase from "./WbwCase";
 import { bookMarkColor } from "./WbwDetailBookMark";
 import WbwFactorMeaning from "./WbwFactorMeaning";
@@ -85,7 +85,9 @@ const Widget = ({
   onSplit,
 }: IWidget) => {
   const [wordData, setWordData] = useState(data);
-
+  useEffect(() => {
+    setWordData(data);
+  }, [data]);
   const styleWbw: React.CSSProperties = {
     display: display === "block" ? "block" : "flex",
   };
@@ -129,10 +131,10 @@ const Widget = ({
         {fields?.case ? (
           <WbwCase
             data={wordData}
-            onSplit={() => {
+            onSplit={(e: boolean) => {
               console.log("onSplit", wordData.factors?.value);
               if (typeof onSplit !== "undefined") {
-                onSplit();
+                onSplit(e);
               }
             }}
           />

+ 6 - 0
dashboard/src/components/template/Wbw/wbw.css

@@ -3,6 +3,12 @@
   padding-right: 0;
   max-width: 60vw;
 }
+.wbw_split {
+  visibility: hidden;
+}
+.wbw_word:hover .wbw_split {
+  visibility: visible;
+}
 .block .pali {
   font-weight: 500;
   font-size: 110%;

+ 30 - 19
dashboard/src/components/template/WbwSent.tsx

@@ -6,7 +6,7 @@ interface IWidget {
   display?: "block" | "inline";
   fields?: IWbwFields;
 }
-const Widget = ({ data, display, fields }: IWidget) => {
+export const WbwSentCtl = ({ data, display, fields }: IWidget) => {
   const [wordData, setWordData] = useState(data);
 
   return (
@@ -23,24 +23,27 @@ const Widget = ({ data, display, fields }: IWidget) => {
               console.log("word id", id);
               //TODO update
             }}
-            onSplit={() => {
-              console.log("word id", id, wordData[id].factors?.value);
-              const newData: IWbw[] = JSON.parse(JSON.stringify(wordData));
-
-              const children: IWbw[] | undefined = wordData[id].factors?.value
-                .split("+")
-                .map((item, id) => {
-                  return {
-                    word: { value: item, status: 5 },
-                    real: { value: item, status: 5 },
-                    confidence: 1,
-                  };
-                });
-              if (typeof children !== "undefined") {
-                console.log("children", children);
-                newData.splice(id + 1, 0, ...children);
-                console.log("new-data", newData);
-                setWordData(newData);
+            onSplit={(isSplit: boolean) => {
+              if (isSplit) {
+                //拆分
+                const newData: IWbw[] = JSON.parse(JSON.stringify(wordData));
+                const children: IWbw[] | undefined = wordData[id].factors?.value
+                  .split("+")
+                  .map((item) => {
+                    return {
+                      word: { value: item, status: 5 },
+                      real: { value: item, status: 5 },
+                      confidence: 1,
+                    };
+                  });
+                if (typeof children !== "undefined") {
+                  console.log("children", children);
+                  newData.splice(id + 1, 0, ...children);
+                  console.log("new-data", newData);
+                  setWordData(newData);
+                }
+              } else {
+                //合并
               }
             }}
           />
@@ -50,4 +53,12 @@ const Widget = ({ data, display, fields }: IWidget) => {
   );
 };
 
+interface IWidgetWbwSent {
+  props: string;
+}
+const Widget = ({ props }: IWidgetWbwSent) => {
+  const prop = JSON.parse(atob(props)) as IWidget;
+  return <WbwSentCtl {...prop} />;
+};
+
 export default Widget;

+ 2 - 2
dashboard/src/components/studio/term/TermCreate.tsx → dashboard/src/components/term/TermCreate.tsx

@@ -8,8 +8,8 @@ import {
 } from "@ant-design/pro-components";
 import { PlusOutlined } from "@ant-design/icons";
 
-import { ITermResponse, ITermCreateResponse } from "../../api/Term";
-import { get } from "../../../request";
+import { ITermResponse, ITermCreateResponse } from "../api/Term";
+import { get } from "../../request";
 
 import TermEditInner from "./TermEditInner";
 

+ 1 - 1
dashboard/src/components/studio/term/TermEditInner.tsx → dashboard/src/components/term/TermEditInner.tsx

@@ -7,7 +7,7 @@ import {
   ProFormTextArea,
 } from "@ant-design/pro-components";
 
-import LangSelect from "../LangSelect";
+import LangSelect from "../general/LangSelect";
 import { DefaultOptionType } from "antd/lib/select";
 
 interface IWidget {

+ 71 - 15
dashboard/src/locales/zh-Hans/dict/index.ts

@@ -12,64 +12,120 @@ const items = {
   "dict.fields.confidence.label": "信心指数",
   "dict.fields.dictname.label": "字典名称",
   "dict.fields.type.n.label": "名词",
+  "dict.fields.type.n.short.label": "名",
   "dict.fields.type.ti.label": "三性",
+  "dict.fields.type.ti.short.label": "三",
   "dict.fields.type.v.label": "动词",
+  "dict.fields.type.v.short.label": "动",
   "dict.fields.type.ind.label": "不变",
-  "dict.fields.type.m.label": "阳",
-  "dict.fields.type.nt.label": "中",
-  "dict.fields.type.f.label": "阴",
+  "dict.fields.type.ind.short.label": "不",
+  "dict.fields.type.m.label": "阳性",
+  "dict.fields.type.m.short.label": "阳",
+  "dict.fields.type.nt.label": "中性",
+  "dict.fields.type.nt.short.label": "中",
+  "dict.fields.type.f.label": "阴性",
+  "dict.fields.type.f.short.label": "阴",
   "dict.fields.type.sg.label": "单数",
+  "dict.fields.type.sg.short.label": "单",
   "dict.fields.type.pl.label": "复数",
+  "dict.fields.type.pl.short.label": "复",
   "dict.fields.type.nom.label": "主格",
+  "dict.fields.type.nom.short.label": "主",
   "dict.fields.type.acc.label": "宾格",
+  "dict.fields.type.acc.short.label": "宾",
   "dict.fields.type.gen.label": "属格",
+  "dict.fields.type.gen.short.label": "属",
   "dict.fields.type.dat.label": "为格",
+  "dict.fields.type.dat.short.label": "为",
   "dict.fields.type.inst.label": "工具格",
+  "dict.fields.type.inst.short.label": "具",
   "dict.fields.type.voc.label": "呼格",
+  "dict.fields.type.voc.short.label": "呼",
   "dict.fields.type.abl.label": "来源格",
+  "dict.fields.type.abl.short.label": "源",
   "dict.fields.type.base.label": "词干",
+  "dict.fields.type.base.short.label": "干",
   "dict.fields.type.imp.label": "命令",
+  "dict.fields.type.imp.short.label": "命令",
   "dict.fields.type.cond.label": "条件",
+  "dict.fields.type.cond.short.label": "条件",
   "dict.fields.type.opt.label": "愿望",
+  "dict.fields.type.opt.short.label": "愿望",
   "dict.fields.type.pres.label": "现",
+  "dict.fields.type.pres.short.label": "现",
   "dict.fields.type.aor.label": "过",
+  "dict.fields.type.aor.short.label": "过",
   "dict.fields.type.pf.label": "完",
+  "dict.fields.type.pf.short.label": "完",
   "dict.fields.type.fut.label": "将",
-  "dict.fields.type.act.label": "主",
-  "dict.fields.type.refl.label": "反",
+  "dict.fields.type.fut.short.label": "将",
+  "dict.fields.type.act.label": "主动",
+  "dict.fields.type.act.short.label": "主动",
+  "dict.fields.type.refl.label": "反照",
+  "dict.fields.type.refl.short.label": "反",
   "dict.fields.type.1p.label": "第一",
+  "dict.fields.type.1p.short.label": "一",
   "dict.fields.type.2p.label": "第二",
+  "dict.fields.type.2p.short.label": "二",
   "dict.fields.type.3p.label": "第三",
+  "dict.fields.type.3p.short.label": "三",
   "dict.fields.type.prp.label": "现分",
+  "dict.fields.type.prp.short.label": "现分",
   "dict.fields.type.prpp.label": "被现分",
+  "dict.fields.type.prpp.short.label": "被现分",
   "dict.fields.type.pp.label": "过分",
+  "dict.fields.type.pp.short.label": "过分",
   "dict.fields.type.ppa.label": "主过分",
+  "dict.fields.type.ppa.short.label": "主过分",
   "dict.fields.type.ppp.label": "被过分",
+  "dict.fields.type.ppp.short.label": "被过分",
   "dict.fields.type.futp.label": "未分",
-  "dict.fields.type.fpa.label": "主未分",
-  "dict.fields.type.fpp.label": "未被分",
+  "dict.fields.type.futp.short.label": "未分",
+  "dict.fields.type.fpa.short.label": "主未分",
+  "dict.fields.type.fpp.short.label": "未被分",
   "dict.fields.type.grd.label": "义务",
+  "dict.fields.type.grd.short.label": "义务",
   "dict.fields.type.pass.label": "被动",
+  "dict.fields.type.pass.short.label": "被动",
   "dict.fields.type.caus.label": "使役",
+  "dict.fields.type.caus.short.label": "使役",
   "dict.fields.type.desid.label": "意欲",
+  "dict.fields.type.desid.short.label": "意欲",
   "dict.fields.type.intens.label": "强意",
+  "dict.fields.type.intens.short.label": "强意",
   "dict.fields.type.denom.label": "名动",
+  "dict.fields.type.denom.short.label": "名动",
   "dict.fields.type.ger.label": "连续",
+  "dict.fields.type.ger.short.label": "连续",
   "dict.fields.type.abs.label": "绝对",
+  "dict.fields.type.abs.short.label": "绝对",
   "dict.fields.type.inf.label": "不定",
-  "dict.fields.type.adj.label": "形",
-  "dict.fields.type.pron.label": "代",
-  "dict.fields.type.num.label": "数",
-  "dict.fields.type.adv.label": "副",
-  "dict.fields.type.conj.label": "连",
-  "dict.fields.type.prep.label": "介",
+  "dict.fields.type.inf.short.label": "不定",
+  "dict.fields.type.adj.label": "形容词",
+  "dict.fields.type.adj.short.label": "形",
+  "dict.fields.type.pron.label": "代词",
+  "dict.fields.type.pron.short.label": "代",
+  "dict.fields.type.num.label": "数词",
+  "dict.fields.type.num.short.label": "数",
+  "dict.fields.type.adv.label": "副词",
+  "dict.fields.type.adv.short.label": "副",
+  "dict.fields.type.conj.label": "连词",
+  "dict.fields.type.conj.short.label": "连",
+  "dict.fields.type.prep.label": "介词",
+  "dict.fields.type.prep.short.label": "介",
   "dict.fields.type.interj.label": "感叹",
+  "dict.fields.type.interj.short.label": "感",
   "dict.fields.type.pre.label": "前缀",
-  "dict.fields.type.suf.label": "后缀",
+  "dict.fields.type.pre.short.label": "前",
+  "dict.fields.type.suf.short.label": "后",
   "dict.fields.type.end.label": "语尾",
-  "dict.fields.type.part.label": "合",
+  "dict.fields.type.end.short.label": "尾",
+  "dict.fields.type.part.label": "组份",
+  "dict.fields.type.part.short.label": "合",
   "dict.fields.type.un.label": "连音",
+  "dict.fields.type.un.short.label": "连音",
   "dict.fields.type.none.label": "无",
+  "dict.fields.type.none.short.label": "",
 };
 
 export default items;

+ 3 - 3
dashboard/src/pages/library/article/show.tsx

@@ -4,7 +4,7 @@ import { useParams } from "react-router-dom";
 import Article, { ArticleMode } from "../../../components/article/Article";
 import ArticleCard from "../../../components/article/ArticleCard";
 import ArticleTabs from "../../../components/article/ArticleTabs";
-import ProTabs from "../../../components/library/article/ProTabs";
+import ProTabs from "../../../components/article/ProTabs";
 
 /**
  * type:
@@ -41,8 +41,8 @@ const Widget = () => {
       <div style={{ width: `calc(100% - ${rightBarWidth})`, display: "flex" }}>
         <div style={{ flex: 5 }}>
           <ArticleCard
-		  type={type}
-		  articleId={id}
+            type={type}
+            articleId={id}
             onModeChange={(e: ArticleMode) => {
               setArticleMode(e);
             }}

+ 9 - 9
dashboard/src/pages/library/blog/anthology.tsx

@@ -1,17 +1,17 @@
 import { useParams } from "react-router-dom";
 import AnthologyList from "../../../components/article/AnthologyList";
-import BlogNav from "../../../components/library/blog/BlogNav";
+import BlogNav from "../../../components/blog/BlogNav";
 
 const Widget = () => {
-	// TODO
-	const { studio } = useParams(); //url 参数
+  // TODO
+  const { studio } = useParams(); //url 参数
 
-	return (
-		<>
-			<BlogNav selectedKey="anthology" studio={studio ? studio : ""} />
-			<AnthologyList view="public_studio" id={studio ? studio : ""} />
-		</>
-	);
+  return (
+    <>
+      <BlogNav selectedKey="anthology" studio={studio ? studio : ""} />
+      <AnthologyList view="public_studio" id={studio ? studio : ""} />
+    </>
+  );
 };
 
 export default Widget;

+ 1 - 1
dashboard/src/pages/library/blog/course.tsx

@@ -1,5 +1,5 @@
 import { useParams } from "react-router-dom";
-import BlogNav from "../../../components/library/blog/BlogNav";
+import BlogNav from "../../../components/blog/BlogNav";
 
 const Widget = () => {
 	// TODO

+ 26 - 26
dashboard/src/pages/library/blog/overview.tsx

@@ -1,37 +1,37 @@
 import { useParams } from "react-router-dom";
 import { Row, Col } from "antd";
 import { Affix } from "antd";
-import BlogNav from "../../../components/library/blog/BlogNav";
-import Profile from "../../../components/library/blog/Profile";
-import AuthorTimeLine from "../../../components/library/blog/TimeLine";
-import TopArticles from "../../../components/library/blog/TopArticles";
+import BlogNav from "../../../components/blog/BlogNav";
+import Profile from "../../../components/blog/Profile";
+import AuthorTimeLine from "../../../components/blog/TimeLine";
+import TopArticles from "../../../components/blog/TopArticles";
 
 const Widget = () => {
-	// TODO
-	const { studio } = useParams(); //url 参数
+  // TODO
+  const { studio } = useParams(); //url 参数
 
-	return (
-		<>
-			<Affix offsetTop={0}>
-				<BlogNav selectedKey="overview" studio={studio ? studio : ""} />
-			</Affix>
+  return (
+    <>
+      <Affix offsetTop={0}>
+        <BlogNav selectedKey="overview" studio={studio ? studio : ""} />
+      </Affix>
 
-			<Row>
-				<Col flex="300px">
-					<Profile />
-				</Col>
+      <Row>
+        <Col flex="300px">
+          <Profile />
+        </Col>
 
-				<Col flex="900px">
-					<div>
-						<TopArticles studio={studio ? studio : ""} />
-					</div>
-					<div>
-						<AuthorTimeLine />
-					</div>
-				</Col>
-			</Row>
-		</>
-	);
+        <Col flex="900px">
+          <div>
+            <TopArticles studio={studio ? studio : ""} />
+          </div>
+          <div>
+            <AuthorTimeLine />
+          </div>
+        </Col>
+      </Row>
+    </>
+  );
 };
 
 export default Widget;

+ 8 - 8
dashboard/src/pages/library/blog/term.tsx

@@ -1,15 +1,15 @@
 import { useParams } from "react-router-dom";
-import BlogNav from "../../../components/library/blog/BlogNav";
+import BlogNav from "../../../components/blog/BlogNav";
 
 const Widget = () => {
-	// TODO
-	const { studio } = useParams(); //url 参数
+  // TODO
+  const { studio } = useParams(); //url 参数
 
-	return (
-		<>
-			<BlogNav selectedKey="term" studio={studio ? studio : ""} />
-		</>
-	);
+  return (
+    <>
+      <BlogNav selectedKey="term" studio={studio ? studio : ""} />
+    </>
+  );
 };
 
 export default Widget;

+ 8 - 8
dashboard/src/pages/library/blog/translation.tsx

@@ -1,15 +1,15 @@
 import { useParams } from "react-router-dom";
-import BlogNav from "../../../components/library/blog/BlogNav";
+import BlogNav from "../../../components/blog/BlogNav";
 
 const Widget = () => {
-	// TODO
-	const { studio } = useParams(); //url 参数
+  // TODO
+  const { studio } = useParams(); //url 参数
 
-	return (
-		<>
-			<BlogNav selectedKey="palicanon" studio={studio ? studio : ""} />
-		</>
-	);
+  return (
+    <>
+      <BlogNav selectedKey="palicanon" studio={studio ? studio : ""} />
+    </>
+  );
 };
 
 export default Widget;

+ 142 - 144
dashboard/src/pages/studio/anthology/edit.tsx

@@ -2,169 +2,167 @@ import { useParams } from "react-router-dom";
 import { useIntl } from "react-intl";
 
 import {
-	ProForm,
-	ProFormText,
-	ProFormTextArea,
+  ProForm,
+  ProFormText,
+  ProFormTextArea,
 } from "@ant-design/pro-components";
 import { Card, Col, message, Row } from "antd";
 
 import EditableTree from "../../../components/studio/EditableTree";
 import type { ListNodeData } from "../../../components/studio/EditableTree";
-import LangSelect from "../../../components/studio/LangSelect";
+import LangSelect from "../../../components/general/LangSelect";
 import PublicitySelect from "../../../components/studio/PublicitySelect";
 import {
-	IAnthologyDataRequest,
-	IAnthologyResponse,
+  IAnthologyDataRequest,
+  IAnthologyResponse,
 } from "../../../components/api/Article";
 import { get, put } from "../../../request";
 import { useState } from "react";
 import GoBack from "../../../components/studio/GoBack";
 
 interface IFormData {
-	title: string;
-	subtitle: string;
-	summary: string;
-	lang: string;
-	status: number;
+  title: string;
+  subtitle: string;
+  summary: string;
+  lang: string;
+  status: number;
 }
 
 const Widget = () => {
-	const listdata: ListNodeData[] = [];
-	const intl = useIntl();
-	const [tocData, setTocData] = useState(listdata);
-	const [title, setTitle] = useState("");
-	const { studioname, anthology_id } = useParams(); //url 参数
-	let treeList: ListNodeData[] = [];
+  const listdata: ListNodeData[] = [];
+  const intl = useIntl();
+  const [tocData, setTocData] = useState(listdata);
+  const [title, setTitle] = useState("");
+  const { studioname, anthology_id } = useParams(); //url 参数
+  let treeList: ListNodeData[] = [];
 
-	return (
-		<>
-			<Card
-				title={
-					<GoBack
-						to={`/studio/${studioname}/anthology/list`}
-						title={title}
-					/>
-				}
-			>
-				<Row>
-					<Col>
-						<ProForm<IFormData>
-							onFinish={async (values: IFormData) => {
-								// TODO
-								console.log(values);
+  return (
+    <>
+      <Card
+        title={
+          <GoBack to={`/studio/${studioname}/anthology/list`} title={title} />
+        }
+      >
+        <Row>
+          <Col>
+            <ProForm<IFormData>
+              onFinish={async (values: IFormData) => {
+                // TODO
+                console.log(values);
 
-								const request: IAnthologyDataRequest = {
-									title: values.title,
-									subtitle: values.subtitle,
-									summary: values.summary,
-									article_list: treeList.map((item) => {
-										return {
-											article: item.key,
-											title: item.title,
-											level: item.level.toString(),
-											children: 0,
-										};
-									}),
-									status: values.status,
-									lang: values.lang,
-								};
-								console.log(request);
-								const res = await put<
-									IAnthologyDataRequest,
-									IAnthologyResponse
-								>(`/v2/anthology/${anthology_id}`, request);
-								console.log(res);
-								if (res.ok) {
-									message.success(
-										intl.formatMessage({
-											id: "flashes.success",
-										})
-									);
-								} else {
-									message.error(res.message);
-								}
-							}}
-							request={async () => {
-								const res = await get<IAnthologyResponse>(
-									`/v2/anthology/${anthology_id}`
-								);
-								const toc: ListNodeData[] =
-									res.data.article_list.map((item) => {
-										return {
-											key: item.article,
-											title: item.title,
-											level: parseInt(item.level),
-										};
-									});
-								setTocData(toc);
-								treeList = toc;
-								setTitle(res.data.title);
-								return {
-									title: res.data.title,
-									subtitle: res.data.subtitle,
-									summary: res.data.summary,
-									lang: res.data.lang,
-									status: res.data.status,
-								};
-							}}
-						>
-							<ProForm.Group>
-								<ProFormText
-									width="md"
-									name="title"
-									required
-									label={intl.formatMessage({
-										id: "forms.fields.title.label",
-									})}
-									rules={[
-										{
-											required: true,
-											message: intl.formatMessage({
-												id: "forms.message.title.required",
-											}),
-										},
-									]}
-								/>
-							</ProForm.Group>
-							<ProForm.Group>
-								<ProFormText
-									width="md"
-									name="subtitle"
-									label={intl.formatMessage({
-										id: "forms.fields.subtitle.label",
-									})}
-								/>
-							</ProForm.Group>
-							<ProForm.Group>
-								<ProFormTextArea
-									name="summary"
-									width="md"
-									label={intl.formatMessage({
-										id: "forms.fields.summary.label",
-									})}
-								/>
-							</ProForm.Group>
+                const request: IAnthologyDataRequest = {
+                  title: values.title,
+                  subtitle: values.subtitle,
+                  summary: values.summary,
+                  article_list: treeList.map((item) => {
+                    return {
+                      article: item.key,
+                      title: item.title,
+                      level: item.level.toString(),
+                      children: 0,
+                    };
+                  }),
+                  status: values.status,
+                  lang: values.lang,
+                };
+                console.log(request);
+                const res = await put<
+                  IAnthologyDataRequest,
+                  IAnthologyResponse
+                >(`/v2/anthology/${anthology_id}`, request);
+                console.log(res);
+                if (res.ok) {
+                  message.success(
+                    intl.formatMessage({
+                      id: "flashes.success",
+                    })
+                  );
+                } else {
+                  message.error(res.message);
+                }
+              }}
+              request={async () => {
+                const res = await get<IAnthologyResponse>(
+                  `/v2/anthology/${anthology_id}`
+                );
+                const toc: ListNodeData[] = res.data.article_list.map(
+                  (item) => {
+                    return {
+                      key: item.article,
+                      title: item.title,
+                      level: parseInt(item.level),
+                    };
+                  }
+                );
+                setTocData(toc);
+                treeList = toc;
+                setTitle(res.data.title);
+                return {
+                  title: res.data.title,
+                  subtitle: res.data.subtitle,
+                  summary: res.data.summary,
+                  lang: res.data.lang,
+                  status: res.data.status,
+                };
+              }}
+            >
+              <ProForm.Group>
+                <ProFormText
+                  width="md"
+                  name="title"
+                  required
+                  label={intl.formatMessage({
+                    id: "forms.fields.title.label",
+                  })}
+                  rules={[
+                    {
+                      required: true,
+                      message: intl.formatMessage({
+                        id: "forms.message.title.required",
+                      }),
+                    },
+                  ]}
+                />
+              </ProForm.Group>
+              <ProForm.Group>
+                <ProFormText
+                  width="md"
+                  name="subtitle"
+                  label={intl.formatMessage({
+                    id: "forms.fields.subtitle.label",
+                  })}
+                />
+              </ProForm.Group>
+              <ProForm.Group>
+                <ProFormTextArea
+                  name="summary"
+                  width="md"
+                  label={intl.formatMessage({
+                    id: "forms.fields.summary.label",
+                  })}
+                />
+              </ProForm.Group>
 
-							<ProForm.Group>
-								<LangSelect />
-							</ProForm.Group>
-							<ProForm.Group>
-								<PublicitySelect />
-							</ProForm.Group>
-						</ProForm>
-					</Col>
-					<Col>
-						<EditableTree
-							treeData={tocData}
-							onChange={(data: ListNodeData[]) => {
-								treeList = data;
-							}}
-						/>
-					</Col>
-				</Row>
-			</Card>
-		</>
-	);
+              <ProForm.Group>
+                <LangSelect />
+              </ProForm.Group>
+              <ProForm.Group>
+                <PublicitySelect />
+              </ProForm.Group>
+            </ProForm>
+          </Col>
+          <Col>
+            <EditableTree
+              treeData={tocData}
+              onChange={(data: ListNodeData[]) => {
+                treeList = data;
+              }}
+            />
+          </Col>
+        </Row>
+      </Card>
+    </>
+  );
 };
 
 export default Widget;

+ 194 - 205
dashboard/src/pages/studio/anthology/list.tsx

@@ -8,7 +8,7 @@ import type { MenuProps } from "antd";
 import { Button, Dropdown, Menu, Popover } from "antd";
 import { SearchOutlined } from "@ant-design/icons";
 
-import AnthologyCreate from "../../../components/studio/anthology/AnthologyCreate";
+import AnthologyCreate from "../../../components/anthology/AnthologyCreate";
 import { IAnthologyListResponse } from "../../../components/api/Article";
 import { get } from "../../../request";
 import { PublicityValueEnum } from "../../../components/studio/table";
@@ -16,220 +16,209 @@ import { PublicityValueEnum } from "../../../components/studio/table";
 const { Text } = Typography;
 
 const onMenuClick: MenuProps["onClick"] = (e) => {
-	console.log("click", e);
+  console.log("click", e);
 };
 
 const menu = (
-	<Menu
-		onClick={onMenuClick}
-		items={[
-			{
-				key: "1",
-				label: "在藏经阁中打开",
-				icon: <SearchOutlined />,
-			},
-			{
-				key: "2",
-				label: "分享",
-				icon: <SearchOutlined />,
-			},
-		]}
-	/>
+  <Menu
+    onClick={onMenuClick}
+    items={[
+      {
+        key: "1",
+        label: "在藏经阁中打开",
+        icon: <SearchOutlined />,
+      },
+      {
+        key: "2",
+        label: "分享",
+        icon: <SearchOutlined />,
+      },
+    ]}
+  />
 );
 
 interface IItem {
-	sn: number;
-	id: string;
-	title: string;
-	subtitle: string;
-	publicity: number;
-	articles: number;
-	createdAt: number;
+  sn: number;
+  id: string;
+  title: string;
+  subtitle: string;
+  publicity: number;
+  articles: number;
+  createdAt: number;
 }
 
 const Widget = () => {
-	const intl = useIntl();
-	const { studioname } = useParams();
-	const anthologyCreate = <AnthologyCreate studio={studioname} />;
-	return (
-		<>
-			<ProTable<IItem>
-				columns={[
-					{
-						title: intl.formatMessage({
-							id: "dict.fields.sn.label",
-						}),
-						dataIndex: "sn",
-						key: "sn",
-						width: 50,
-						search: false,
-					},
-					{
-						title: intl.formatMessage({
-							id: "forms.fields.title.label",
-						}),
-						dataIndex: "title",
-						key: "title",
-						tip: "过长会自动收缩",
-						ellipsis: true,
-						render: (text, row, index, action) => {
-							return (
-								<div>
-									<div>
-										<Link
-											to={`/studio/${studioname}/anthology/${row.id}/edit`}
-										>
-											{row.title}
-										</Link>
-									</div>
-									<Text type="secondary">{row.subtitle}</Text>
-								</div>
-							);
-						},
-					},
-					{
-						title: intl.formatMessage({
-							id: "forms.fields.publicity.label",
-						}),
-						dataIndex: "publicity",
-						key: "publicity",
-						width: 100,
-						search: false,
-						filters: true,
-						onFilter: true,
-						valueEnum: PublicityValueEnum(),
-					},
-					{
-						title: intl.formatMessage({
-							id: "article.fields.article.count.label",
-						}),
-						dataIndex: "articles",
-						key: "articles",
-						width: 100,
-						search: false,
-						sorter: (a, b) => a.articles - b.articles,
-					},
-					{
-						title: intl.formatMessage({
-							id: "forms.fields.created-at.label",
-						}),
-						key: "created-at",
-						width: 100,
-						search: false,
-						dataIndex: "createdAt",
-						valueType: "date",
-						sorter: (a, b) => a.createdAt - b.createdAt,
-					},
-					{
-						title: intl.formatMessage({ id: "buttons.option" }),
-						key: "option",
-						width: 120,
-						valueType: "option",
-						render: (text, row, index, action) => [
-							<Dropdown.Button
-								type="link"
-								key={index}
-								overlay={menu}
-							>
-								{intl.formatMessage({
-									id: "buttons.edit",
-								})}
-							</Dropdown.Button>,
-						],
-					},
-				]}
-				rowSelection={{
-					// 自定义选择项参考: https://ant.design/components/table-cn/#components-table-demo-row-selection-custom
-					// 注释该行则默认不显示下拉选项
-					selections: [Table.SELECTION_ALL, Table.SELECTION_INVERT],
-				}}
-				tableAlertRender={({
-					selectedRowKeys,
-					selectedRows,
-					onCleanSelected,
-				}) => (
-					<Space size={24}>
-						<span>
-							{intl.formatMessage({ id: "buttons.selected" })}{" "}
-							{selectedRowKeys.length}
-							<Button
-								type="link"
-								style={{ marginInlineStart: 8 }}
-								onClick={onCleanSelected}
-							>
-								{intl.formatMessage({ id: "buttons.unselect" })}
-							</Button>
-						</span>
-					</Space>
-				)}
-				tableAlertOptionRender={() => {
-					return (
-						<Space size={16}>
-							<Button type="link">批量删除</Button>
-						</Space>
-					);
-				}}
-				request={async (params = {}, sorter, filter) => {
-					// TODO
-					console.log(params, sorter, filter);
-					let url = `/v2/anthology?view=studio&name=${studioname}`;
-					const offset =
-						((params.current ? params.current : 1) - 1) *
-						(params.pageSize ? params.pageSize : 20);
-					url += `&limit=${params.pageSize}&offset=${offset}`;
-					if (typeof params.keyword !== "undefined") {
-						url +=
-							"&search=" + (params.keyword ? params.keyword : "");
-					}
+  const intl = useIntl();
+  const { studioname } = useParams();
+  const anthologyCreate = <AnthologyCreate studio={studioname} />;
+  return (
+    <>
+      <ProTable<IItem>
+        columns={[
+          {
+            title: intl.formatMessage({
+              id: "dict.fields.sn.label",
+            }),
+            dataIndex: "sn",
+            key: "sn",
+            width: 50,
+            search: false,
+          },
+          {
+            title: intl.formatMessage({
+              id: "forms.fields.title.label",
+            }),
+            dataIndex: "title",
+            key: "title",
+            tip: "过长会自动收缩",
+            ellipsis: true,
+            render: (text, row, index, action) => {
+              return (
+                <div>
+                  <div>
+                    <Link to={`/studio/${studioname}/anthology/${row.id}/edit`}>
+                      {row.title}
+                    </Link>
+                  </div>
+                  <Text type="secondary">{row.subtitle}</Text>
+                </div>
+              );
+            },
+          },
+          {
+            title: intl.formatMessage({
+              id: "forms.fields.publicity.label",
+            }),
+            dataIndex: "publicity",
+            key: "publicity",
+            width: 100,
+            search: false,
+            filters: true,
+            onFilter: true,
+            valueEnum: PublicityValueEnum(),
+          },
+          {
+            title: intl.formatMessage({
+              id: "article.fields.article.count.label",
+            }),
+            dataIndex: "articles",
+            key: "articles",
+            width: 100,
+            search: false,
+            sorter: (a, b) => a.articles - b.articles,
+          },
+          {
+            title: intl.formatMessage({
+              id: "forms.fields.created-at.label",
+            }),
+            key: "created-at",
+            width: 100,
+            search: false,
+            dataIndex: "createdAt",
+            valueType: "date",
+            sorter: (a, b) => a.createdAt - b.createdAt,
+          },
+          {
+            title: intl.formatMessage({ id: "buttons.option" }),
+            key: "option",
+            width: 120,
+            valueType: "option",
+            render: (text, row, index, action) => [
+              <Dropdown.Button type="link" key={index} overlay={menu}>
+                {intl.formatMessage({
+                  id: "buttons.edit",
+                })}
+              </Dropdown.Button>,
+            ],
+          },
+        ]}
+        rowSelection={{
+          // 自定义选择项参考: https://ant.design/components/table-cn/#components-table-demo-row-selection-custom
+          // 注释该行则默认不显示下拉选项
+          selections: [Table.SELECTION_ALL, Table.SELECTION_INVERT],
+        }}
+        tableAlertRender={({
+          selectedRowKeys,
+          selectedRows,
+          onCleanSelected,
+        }) => (
+          <Space size={24}>
+            <span>
+              {intl.formatMessage({ id: "buttons.selected" })}{" "}
+              {selectedRowKeys.length}
+              <Button
+                type="link"
+                style={{ marginInlineStart: 8 }}
+                onClick={onCleanSelected}
+              >
+                {intl.formatMessage({ id: "buttons.unselect" })}
+              </Button>
+            </span>
+          </Space>
+        )}
+        tableAlertOptionRender={() => {
+          return (
+            <Space size={16}>
+              <Button type="link">批量删除</Button>
+            </Space>
+          );
+        }}
+        request={async (params = {}, sorter, filter) => {
+          // TODO
+          console.log(params, sorter, filter);
+          let url = `/v2/anthology?view=studio&name=${studioname}`;
+          const offset =
+            ((params.current ? params.current : 1) - 1) *
+            (params.pageSize ? params.pageSize : 20);
+          url += `&limit=${params.pageSize}&offset=${offset}`;
+          if (typeof params.keyword !== "undefined") {
+            url += "&search=" + (params.keyword ? params.keyword : "");
+          }
 
-					const res = await get<IAnthologyListResponse>(url);
-					const items: IItem[] = res.data.rows.map((item, id) => {
-						const date = new Date(item.created_at);
-						return {
-							sn: id + 1,
-							id: item.uid,
-							title: item.title,
-							subtitle: item.subtitle,
-							publicity: item.status,
-							articles: item.childrenNumber,
-							createdAt: date.getTime(),
-						};
-					});
-					console.log(items);
-					return {
-						total: res.data.count,
-						succcess: true,
-						data: items,
-					};
-				}}
-				rowKey="id"
-				bordered
-				pagination={{
-					showQuickJumper: true,
-					showSizeChanger: true,
-				}}
-				search={false}
-				options={{
-					search: true,
-				}}
-				toolBarRender={() => [
-					<Popover
-						content={anthologyCreate}
-						title="new article"
-						placement="bottomRight"
-					>
-						<Button
-							key="button"
-							icon={<PlusOutlined />}
-							type="primary"
-						>
-							{intl.formatMessage({ id: "buttons.create" })}
-						</Button>
-					</Popover>,
-				]}
-			/>
-		</>
-	);
+          const res = await get<IAnthologyListResponse>(url);
+          const items: IItem[] = res.data.rows.map((item, id) => {
+            const date = new Date(item.created_at);
+            return {
+              sn: id + 1,
+              id: item.uid,
+              title: item.title,
+              subtitle: item.subtitle,
+              publicity: item.status,
+              articles: item.childrenNumber,
+              createdAt: date.getTime(),
+            };
+          });
+          console.log(items);
+          return {
+            total: res.data.count,
+            succcess: true,
+            data: items,
+          };
+        }}
+        rowKey="id"
+        bordered
+        pagination={{
+          showQuickJumper: true,
+          showSizeChanger: true,
+        }}
+        search={false}
+        options={{
+          search: true,
+        }}
+        toolBarRender={() => [
+          <Popover
+            content={anthologyCreate}
+            title="new article"
+            placement="bottomRight"
+          >
+            <Button key="button" icon={<PlusOutlined />} type="primary">
+              {intl.formatMessage({ id: "buttons.create" })}
+            </Button>
+          </Popover>,
+        ]}
+      />
+    </>
+  );
 };
 
 export default Widget;

+ 116 - 124
dashboard/src/pages/studio/article/edit.tsx

@@ -1,144 +1,136 @@
 import { useParams } from "react-router-dom";
 import { useIntl } from "react-intl";
 import {
-	ProForm,
-	ProFormText,
-	ProFormTextArea,
+  ProForm,
+  ProFormText,
+  ProFormTextArea,
 } from "@ant-design/pro-components";
 import { Card, message } from "antd";
+
 import { get, put } from "../../../request";
 import {
-	IArticleDataRequest,
-	IArticleResponse,
+  IArticleDataRequest,
+  IArticleResponse,
 } from "../../../components/api/Article";
-import LangSelect from "../../../components/studio/LangSelect";
+import LangSelect from "../../../components/general/LangSelect";
 import PublicitySelect from "../../../components/studio/PublicitySelect";
 import GoBack from "../../../components/studio/GoBack";
 import { useState } from "react";
 
 interface IFormData {
-	uid: string;
-	title: string;
-	subtitle: string;
-	summary: string;
-	content: string;
-	content_type: string;
-	status: number;
-	lang: string;
+  uid: string;
+  title: string;
+  subtitle: string;
+  summary: string;
+  content: string;
+  content_type: string;
+  status: number;
+  lang: string;
 }
 
 const Widget = () => {
-	const intl = useIntl();
-	const { studioname, articleid } = useParams(); //url 参数
-	const [title, setTitle] = useState("loading");
-	return (
-		<Card
-			title={
-				<GoBack
-					to={`/studio/${studioname}/article/list`}
-					title={title}
-				/>
-			}
-		>
-			<ProForm<IFormData>
-				onFinish={async (values: IFormData) => {
-					// TODO
+  const intl = useIntl();
+  const { studioname, articleid } = useParams(); //url 参数
+  const [title, setTitle] = useState("loading");
+  return (
+    <Card
+      title={<GoBack to={`/studio/${studioname}/article/list`} title={title} />}
+    >
+      <ProForm<IFormData>
+        onFinish={async (values: IFormData) => {
+          // TODO
 
-					const request = {
-						uid: articleid ? articleid : "",
-						title: values.title,
-						subtitle: values.subtitle,
-						summary: values.summary,
-						content: values.content,
-						content_type: "markdown",
-						status: values.status,
-						lang: values.lang,
-					};
-					console.log(request);
-					const res = await put<
-						IArticleDataRequest,
-						IArticleResponse
-					>(`/v2/article/${articleid}`, request);
-					console.log(res);
-					if (res.ok) {
-						message.success(
-							intl.formatMessage({ id: "flashes.success" })
-						);
-					} else {
-						message.error(res.message);
-					}
-				}}
-				request={async () => {
-					const res = await get<IArticleResponse>(
-						`/v2/article/${articleid}`
-					);
-					setTitle(res.data.title);
-					return {
-						uid: res.data.uid,
-						title: res.data.title,
-						subtitle: res.data.subtitle,
-						summary: res.data.summary,
-						content: res.data.content,
-						content_type: res.data.content_type,
-						lang: res.data.lang,
-						status: res.data.status,
-					};
-				}}
-			>
-				<ProForm.Group>
-					<ProFormText
-						width="md"
-						name="title"
-						required
-						label={intl.formatMessage({
-							id: "forms.fields.title.label",
-						})}
-						rules={[
-							{
-								required: true,
-								message: intl.formatMessage({
-									id: "forms.message.title.required",
-								}),
-							},
-						]}
-					/>
-				</ProForm.Group>
-				<ProForm.Group>
-					<ProFormText
-						width="md"
-						name="subtitle"
-						label={intl.formatMessage({
-							id: "forms.fields.subtitle.label",
-						})}
-					/>
-				</ProForm.Group>
-				<ProForm.Group>
-					<ProFormTextArea
-						name="summary"
-						width="md"
-						label={intl.formatMessage({
-							id: "forms.fields.summary.label",
-						})}
-					/>
-				</ProForm.Group>
-				<ProForm.Group>
-					<LangSelect />
-				</ProForm.Group>
-				<ProForm.Group>
-					<PublicitySelect />
-				</ProForm.Group>
-				<ProForm.Group>
-					<ProFormTextArea
-						name="content"
-						width="md"
-						label={intl.formatMessage({
-							id: "forms.fields.content.label",
-						})}
-					/>
-				</ProForm.Group>
-			</ProForm>
-		</Card>
-	);
+          const request = {
+            uid: articleid ? articleid : "",
+            title: values.title,
+            subtitle: values.subtitle,
+            summary: values.summary,
+            content: values.content,
+            content_type: "markdown",
+            status: values.status,
+            lang: values.lang,
+          };
+          console.log(request);
+          const res = await put<IArticleDataRequest, IArticleResponse>(
+            `/v2/article/${articleid}`,
+            request
+          );
+          console.log(res);
+          if (res.ok) {
+            message.success(intl.formatMessage({ id: "flashes.success" }));
+          } else {
+            message.error(res.message);
+          }
+        }}
+        request={async () => {
+          const res = await get<IArticleResponse>(`/v2/article/${articleid}`);
+          setTitle(res.data.title);
+          return {
+            uid: res.data.uid,
+            title: res.data.title,
+            subtitle: res.data.subtitle,
+            summary: res.data.summary,
+            content: res.data.content,
+            content_type: res.data.content_type,
+            lang: res.data.lang,
+            status: res.data.status,
+          };
+        }}
+      >
+        <ProForm.Group>
+          <ProFormText
+            width="md"
+            name="title"
+            required
+            label={intl.formatMessage({
+              id: "forms.fields.title.label",
+            })}
+            rules={[
+              {
+                required: true,
+                message: intl.formatMessage({
+                  id: "forms.message.title.required",
+                }),
+              },
+            ]}
+          />
+        </ProForm.Group>
+        <ProForm.Group>
+          <ProFormText
+            width="md"
+            name="subtitle"
+            label={intl.formatMessage({
+              id: "forms.fields.subtitle.label",
+            })}
+          />
+        </ProForm.Group>
+        <ProForm.Group>
+          <ProFormTextArea
+            name="summary"
+            width="md"
+            label={intl.formatMessage({
+              id: "forms.fields.summary.label",
+            })}
+          />
+        </ProForm.Group>
+        <ProForm.Group>
+          <LangSelect />
+        </ProForm.Group>
+        <ProForm.Group>
+          <PublicitySelect />
+        </ProForm.Group>
+        <ProForm.Group>
+          <ProFormTextArea
+            name="content"
+            width="md"
+            label={intl.formatMessage({
+              id: "forms.fields.content.label",
+            })}
+          />
+        </ProForm.Group>
+      </ProForm>
+    </Card>
+  );
 };
 
 export default Widget;

+ 211 - 224
dashboard/src/pages/studio/article/list.tsx

@@ -5,244 +5,231 @@ import { Space, Button, Popover, Dropdown, MenuProps, Menu, Table } from "antd";
 import { ProTable } from "@ant-design/pro-components";
 import { PlusOutlined, SearchOutlined } from "@ant-design/icons";
 
-import ArticleCreate from "../../../components/studio/article/ArticleCreate";
+import ArticleCreate from "../../../components/article/ArticleCreate";
 import { get } from "../../../request";
 import { IArticleListResponse } from "../../../components/api/Article";
 import { PublicityValueEnum } from "../../../components/studio/table";
 const onMenuClick: MenuProps["onClick"] = (e) => {
-	console.log("click", e);
+  console.log("click", e);
 };
 
 const menu = (
-	<Menu
-		onClick={onMenuClick}
-		items={[
-			{
-				key: "1",
-				label: "在藏经阁中打开",
-				icon: <SearchOutlined />,
-			},
-			{
-				key: "2",
-				label: "分享",
-				icon: <SearchOutlined />,
-			},
-			{
-				key: "3",
-				label: "删除",
-				icon: <SearchOutlined />,
-			},
-		]}
-	/>
+  <Menu
+    onClick={onMenuClick}
+    items={[
+      {
+        key: "1",
+        label: "在藏经阁中打开",
+        icon: <SearchOutlined />,
+      },
+      {
+        key: "2",
+        label: "分享",
+        icon: <SearchOutlined />,
+      },
+      {
+        key: "3",
+        label: "删除",
+        icon: <SearchOutlined />,
+      },
+    ]}
+  />
 );
 interface DataItem {
-	sn: number;
-	id: string;
-	title: string;
-	subtitle: string;
-	summary: string;
-	publicity: number;
-	createdAt: number;
+  sn: number;
+  id: string;
+  title: string;
+  subtitle: string;
+  summary: string;
+  publicity: number;
+  createdAt: number;
 }
 const Widget = () => {
-	const intl = useIntl(); //i18n
-	const { studioname } = useParams(); //url 参数
-	const articleCreate = <ArticleCreate studio={studioname} />;
+  const intl = useIntl(); //i18n
+  const { studioname } = useParams(); //url 参数
+  const articleCreate = <ArticleCreate studio={studioname} />;
 
-	return (
-		<>
-			<ProTable<DataItem>
-				columns={[
-					{
-						title: intl.formatMessage({
-							id: "dict.fields.sn.label",
-						}),
-						dataIndex: "sn",
-						key: "sn",
-						width: 50,
-						search: false,
-					},
-					{
-						title: intl.formatMessage({
-							id: "forms.fields.title.label",
-						}),
-						dataIndex: "title",
-						key: "title",
-						tip: "过长会自动收缩",
-						ellipsis: true,
-						render: (text, row, index, action) => {
-							return (
-								<Link
-									to={`/studio/${studioname}/article/${row.id}/edit`}
-								>
-									{row.title}
-								</Link>
-							);
-						},
-					},
-					{
-						title: intl.formatMessage({
-							id: "forms.fields.subtitle.label",
-						}),
-						dataIndex: "subtitle",
-						key: "subtitle",
-						tip: "过长会自动收缩",
-						ellipsis: true,
-					},
-					{
-						title: intl.formatMessage({
-							id: "forms.fields.summary.label",
-						}),
-						dataIndex: "summary",
-						key: "summary",
-						tip: "过长会自动收缩",
-						ellipsis: true,
-					},
+  return (
+    <>
+      <ProTable<DataItem>
+        columns={[
+          {
+            title: intl.formatMessage({
+              id: "dict.fields.sn.label",
+            }),
+            dataIndex: "sn",
+            key: "sn",
+            width: 50,
+            search: false,
+          },
+          {
+            title: intl.formatMessage({
+              id: "forms.fields.title.label",
+            }),
+            dataIndex: "title",
+            key: "title",
+            tip: "过长会自动收缩",
+            ellipsis: true,
+            render: (text, row, index, action) => {
+              return (
+                <Link to={`/studio/${studioname}/article/${row.id}/edit`}>
+                  {row.title}
+                </Link>
+              );
+            },
+          },
+          {
+            title: intl.formatMessage({
+              id: "forms.fields.subtitle.label",
+            }),
+            dataIndex: "subtitle",
+            key: "subtitle",
+            tip: "过长会自动收缩",
+            ellipsis: true,
+          },
+          {
+            title: intl.formatMessage({
+              id: "forms.fields.summary.label",
+            }),
+            dataIndex: "summary",
+            key: "summary",
+            tip: "过长会自动收缩",
+            ellipsis: true,
+          },
 
-					{
-						title: intl.formatMessage({
-							id: "forms.fields.publicity.label",
-						}),
-						dataIndex: "publicity",
-						key: "publicity",
-						width: 100,
-						search: false,
-						filters: true,
-						onFilter: true,
-						valueEnum: PublicityValueEnum(),
-					},
-					{
-						title: intl.formatMessage({
-							id: "forms.fields.created-at.label",
-						}),
-						key: "created-at",
-						width: 100,
-						search: false,
-						dataIndex: "createdAt",
-						valueType: "date",
-						sorter: (a, b) => a.createdAt - b.createdAt,
-					},
-					{
-						title: intl.formatMessage({ id: "buttons.option" }),
-						key: "option",
-						width: 120,
-						valueType: "option",
-						render: (text, row, index, action) => {
-							return [
-								<Dropdown.Button
-									key={index}
-									type="link"
-									overlay={menu}
-								>
-									<Link
-										to={`/studio/${studioname}/article/${row.id}/edit`}
-									>
-										{intl.formatMessage({
-											id: "buttons.edit",
-										})}
-									</Link>
-								</Dropdown.Button>,
-							];
-						},
-					},
-				]}
-				rowSelection={{
-					// 自定义选择项参考: https://ant.design/components/table-cn/#components-table-demo-row-selection-custom
-					// 注释该行则默认不显示下拉选项
-					selections: [Table.SELECTION_ALL, Table.SELECTION_INVERT],
-				}}
-				tableAlertRender={({
-					selectedRowKeys,
-					selectedRows,
-					onCleanSelected,
-				}) => (
-					<Space size={24}>
-						<span>
-							{intl.formatMessage({ id: "buttons.selected" })}
-							{selectedRowKeys.length}
-							<Button
-								type="link"
-								style={{ marginInlineStart: 8 }}
-								onClick={onCleanSelected}
-							>
-								{intl.formatMessage({
-									id: "buttons.unselect",
-								})}
-							</Button>
-						</span>
-					</Space>
-				)}
-				tableAlertOptionRender={() => {
-					return (
-						<Space size={16}>
-							<Button type="link">
-								{intl.formatMessage({
-									id: "buttons.delete.all",
-								})}
-							</Button>
-						</Space>
-					);
-				}}
-				request={async (params = {}, sorter, filter) => {
-					// TODO
-					console.log(params, sorter, filter);
-					let url = `/v2/article?view=studio&name=${studioname}`;
-					const offset =
-						((params.current ? params.current : 1) - 1) *
-						(params.pageSize ? params.pageSize : 20);
-					url += `&limit=${params.pageSize}&offset=${offset}`;
-					if (typeof params.keyword !== "undefined") {
-						url +=
-							"&search=" + (params.keyword ? params.keyword : "");
-					}
+          {
+            title: intl.formatMessage({
+              id: "forms.fields.publicity.label",
+            }),
+            dataIndex: "publicity",
+            key: "publicity",
+            width: 100,
+            search: false,
+            filters: true,
+            onFilter: true,
+            valueEnum: PublicityValueEnum(),
+          },
+          {
+            title: intl.formatMessage({
+              id: "forms.fields.created-at.label",
+            }),
+            key: "created-at",
+            width: 100,
+            search: false,
+            dataIndex: "createdAt",
+            valueType: "date",
+            sorter: (a, b) => a.createdAt - b.createdAt,
+          },
+          {
+            title: intl.formatMessage({ id: "buttons.option" }),
+            key: "option",
+            width: 120,
+            valueType: "option",
+            render: (text, row, index, action) => {
+              return [
+                <Dropdown.Button key={index} type="link" overlay={menu}>
+                  <Link to={`/studio/${studioname}/article/${row.id}/edit`}>
+                    {intl.formatMessage({
+                      id: "buttons.edit",
+                    })}
+                  </Link>
+                </Dropdown.Button>,
+              ];
+            },
+          },
+        ]}
+        rowSelection={{
+          // 自定义选择项参考: https://ant.design/components/table-cn/#components-table-demo-row-selection-custom
+          // 注释该行则默认不显示下拉选项
+          selections: [Table.SELECTION_ALL, Table.SELECTION_INVERT],
+        }}
+        tableAlertRender={({
+          selectedRowKeys,
+          selectedRows,
+          onCleanSelected,
+        }) => (
+          <Space size={24}>
+            <span>
+              {intl.formatMessage({ id: "buttons.selected" })}
+              {selectedRowKeys.length}
+              <Button
+                type="link"
+                style={{ marginInlineStart: 8 }}
+                onClick={onCleanSelected}
+              >
+                {intl.formatMessage({
+                  id: "buttons.unselect",
+                })}
+              </Button>
+            </span>
+          </Space>
+        )}
+        tableAlertOptionRender={() => {
+          return (
+            <Space size={16}>
+              <Button type="link">
+                {intl.formatMessage({
+                  id: "buttons.delete.all",
+                })}
+              </Button>
+            </Space>
+          );
+        }}
+        request={async (params = {}, sorter, filter) => {
+          // TODO
+          console.log(params, sorter, filter);
+          let url = `/v2/article?view=studio&name=${studioname}`;
+          const offset =
+            ((params.current ? params.current : 1) - 1) *
+            (params.pageSize ? params.pageSize : 20);
+          url += `&limit=${params.pageSize}&offset=${offset}`;
+          if (typeof params.keyword !== "undefined") {
+            url += "&search=" + (params.keyword ? params.keyword : "");
+          }
 
-					const res = await get<IArticleListResponse>(url);
-					const items: DataItem[] = res.data.rows.map((item, id) => {
-						const date = new Date(item.created_at);
-						return {
-							sn: id + 1,
-							id: item.uid,
-							title: item.title,
-							subtitle: item.subtitle,
-							summary: item.summary,
-							publicity: item.status,
-							createdAt: date.getTime(),
-						};
-					});
-					return {
-						total: res.data.count,
-						succcess: true,
-						data: items,
-					};
-				}}
-				rowKey="id"
-				bordered
-				pagination={{
-					showQuickJumper: true,
-					showSizeChanger: true,
-				}}
-				search={false}
-				options={{
-					search: true,
-				}}
-				toolBarRender={() => [
-					<Popover
-						content={articleCreate}
-						title="Create"
-						placement="bottomRight"
-					>
-						<Button
-							key="button"
-							icon={<PlusOutlined />}
-							type="primary"
-						>
-							{intl.formatMessage({ id: "buttons.create" })}
-						</Button>
-					</Popover>,
-				]}
-			/>
-		</>
-	);
+          const res = await get<IArticleListResponse>(url);
+          const items: DataItem[] = res.data.rows.map((item, id) => {
+            const date = new Date(item.created_at);
+            return {
+              sn: id + 1,
+              id: item.uid,
+              title: item.title,
+              subtitle: item.subtitle,
+              summary: item.summary,
+              publicity: item.status,
+              createdAt: date.getTime(),
+            };
+          });
+          return {
+            total: res.data.count,
+            succcess: true,
+            data: items,
+          };
+        }}
+        rowKey="id"
+        bordered
+        pagination={{
+          showQuickJumper: true,
+          showSizeChanger: true,
+        }}
+        search={false}
+        options={{
+          search: true,
+        }}
+        toolBarRender={() => [
+          <Popover
+            content={articleCreate}
+            title="Create"
+            placement="bottomRight"
+          >
+            <Button key="button" icon={<PlusOutlined />} type="primary">
+              {intl.formatMessage({ id: "buttons.create" })}
+            </Button>
+          </Popover>,
+        ]}
+      />
+    </>
+  );
 };
 
 export default Widget;

+ 73 - 80
dashboard/src/pages/studio/channel/edit.tsx

@@ -1,100 +1,93 @@
 import { useParams } from "react-router-dom";
 import {
-	ProForm,
-	ProFormText,
-	ProFormTextArea,
+  ProForm,
+  ProFormText,
+  ProFormTextArea,
 } from "@ant-design/pro-components";
 import { useIntl } from "react-intl";
 import { Card, message, Space } from "antd";
 import { IApiResponseChannel } from "../../../components/api/Channel";
 import { get, put } from "../../../request";
-import ChannelTypeSelect from "../../../components/studio/channel/ChannelTypeSelect";
-import LangSelect from "../../../components/studio/LangSelect";
+import ChannelTypeSelect from "../../../components/channel/ChannelTypeSelect";
+import LangSelect from "../../../components/general/LangSelect";
 import PublicitySelect from "../../../components/studio/PublicitySelect";
 import GoBack from "../../../components/studio/GoBack";
 import { useState } from "react";
 
 interface IFormData {
-	name: string;
-	type: string;
-	lang: string;
-	summary: string;
-	status: number;
-	studio: string;
+  name: string;
+  type: string;
+  lang: string;
+  summary: string;
+  status: number;
+  studio: string;
 }
 const Widget = () => {
-	const intl = useIntl();
-	const { channelid } = useParams(); //url 参数
-	const { studioname } = useParams();
-	const [title, setTitle] = useState("");
+  const intl = useIntl();
+  const { channelid } = useParams(); //url 参数
+  const { studioname } = useParams();
+  const [title, setTitle] = useState("");
 
-	return (
-		<Card
-			title={
-				<GoBack
-					to={`/studio/${studioname}/channel/list`}
-					title={title}
-				/>
-			}
-		>
-			<Space>{channelid}</Space>
-			<ProForm<IFormData>
-				onFinish={async (values: IFormData) => {
-					// TODO
-					console.log(values);
-					const res = await put(`/v2/channel/${channelid}`, values);
-					console.log(res);
-					message.success(
-						intl.formatMessage({ id: "flashes.success" })
-					);
-				}}
-				formKey="channel_edit"
-				request={async () => {
-					const res: IApiResponseChannel = await get(
-						`/v2/channel/${channelid}`
-					);
-					setTitle(res.data.name);
-					return {
-						name: res.data.name,
-						type: res.data.type,
-						lang: res.data.lang,
-						summary: res.data.summary,
-						status: res.data.status,
-						studio: studioname ? studioname : "",
-					};
-				}}
-			>
-				<ProForm.Group>
-					<ProFormText
-						width="md"
-						name="name"
-						required
-						label={intl.formatMessage({ id: "channel.name" })}
-						rules={[
-							{
-								required: true,
-								message: intl.formatMessage({
-									id: "channel.create.message.noname",
-								}),
-							},
-						]}
-					/>
-				</ProForm.Group>
+  return (
+    <Card
+      title={<GoBack to={`/studio/${studioname}/channel/list`} title={title} />}
+    >
+      <Space>{channelid}</Space>
+      <ProForm<IFormData>
+        onFinish={async (values: IFormData) => {
+          // TODO
+          console.log(values);
+          const res = await put(`/v2/channel/${channelid}`, values);
+          console.log(res);
+          message.success(intl.formatMessage({ id: "flashes.success" }));
+        }}
+        formKey="channel_edit"
+        request={async () => {
+          const res: IApiResponseChannel = await get(
+            `/v2/channel/${channelid}`
+          );
+          setTitle(res.data.name);
+          return {
+            name: res.data.name,
+            type: res.data.type,
+            lang: res.data.lang,
+            summary: res.data.summary,
+            status: res.data.status,
+            studio: studioname ? studioname : "",
+          };
+        }}
+      >
+        <ProForm.Group>
+          <ProFormText
+            width="md"
+            name="name"
+            required
+            label={intl.formatMessage({ id: "channel.name" })}
+            rules={[
+              {
+                required: true,
+                message: intl.formatMessage({
+                  id: "channel.create.message.noname",
+                }),
+              },
+            ]}
+          />
+        </ProForm.Group>
 
-				<ProForm.Group>
-					<ChannelTypeSelect />
-					<LangSelect />
-				</ProForm.Group>
-				<ProForm.Group>
-					<PublicitySelect />
-				</ProForm.Group>
+        <ProForm.Group>
+          <ChannelTypeSelect />
+          <LangSelect />
+        </ProForm.Group>
+        <ProForm.Group>
+          <PublicitySelect />
+        </ProForm.Group>
 
-				<ProForm.Group>
-					<ProFormTextArea width="md" name="summary" label="简介" />
-				</ProForm.Group>
-			</ProForm>
-		</Card>
-	);
+        <ProForm.Group>
+          <ProFormTextArea width="md" name="summary" label="简介" />
+        </ProForm.Group>
+      </ProForm>
+    </Card>
+  );
 };
 
 export default Widget;

+ 243 - 255
dashboard/src/pages/studio/channel/list.tsx

@@ -8,276 +8,264 @@ import type { MenuProps } from "antd";
 import { Button, Dropdown, Menu, Popover } from "antd";
 import { SearchOutlined } from "@ant-design/icons";
 
-import ChannelCreate from "../../../components/studio/channel/ChannelCreate";
+import ChannelCreate from "../../../components/channel/ChannelCreate";
 import { get } from "../../../request";
 import { IApiResponseChannelList } from "../../../components/api/Channel";
 import { PublicityValueEnum } from "../../../components/studio/table";
 
 const onMenuClick: MenuProps["onClick"] = (e) => {
-	console.log("click", e);
+  console.log("click", e);
 };
 
 const menu = (
-	<Menu
-		onClick={onMenuClick}
-		items={[
-			{
-				key: "1",
-				label: "在藏经阁中打开",
-				icon: <SearchOutlined />,
-			},
-			{
-				key: "2",
-				label: "分享",
-				icon: <SearchOutlined />,
-			},
-			{
-				key: "3",
-				label: "删除",
-				icon: <SearchOutlined />,
-			},
-		]}
-	/>
+  <Menu
+    onClick={onMenuClick}
+    items={[
+      {
+        key: "1",
+        label: "在藏经阁中打开",
+        icon: <SearchOutlined />,
+      },
+      {
+        key: "2",
+        label: "分享",
+        icon: <SearchOutlined />,
+      },
+      {
+        key: "3",
+        label: "删除",
+        icon: <SearchOutlined />,
+      },
+    ]}
+  />
 );
 
 interface IItem {
-	id: number;
-	uid: string;
-	title: string;
-	summary: string;
-	type: string;
-	publicity: number;
-	createdAt: number;
+  id: number;
+  uid: string;
+  title: string;
+  summary: string;
+  type: string;
+  publicity: number;
+  createdAt: number;
 }
 
 const Widget = () => {
-	const intl = useIntl();
-	const { studioname } = useParams();
-	const channelCreate = <ChannelCreate studio={studioname} />;
-	return (
-		<>
-			<ProTable<IItem>
-				columns={[
-					{
-						title: intl.formatMessage({
-							id: "dict.fields.sn.label",
-						}),
-						dataIndex: "id",
-						key: "id",
-						width: 50,
-						search: false,
-					},
-					{
-						title: intl.formatMessage({
-							id: "forms.fields.title.label",
-						}),
-						dataIndex: "title",
-						key: "title",
-						tip: "过长会自动收缩",
-						ellipsis: true,
-						render: (text, row, index, action) => {
-							return (
-								<Link
-									to={`/studio/${studioname}/channel/${row.uid}/edit`}
-								>
-									{row.title}
-								</Link>
-							);
-						},
-					},
-					{
-						title: intl.formatMessage({
-							id: "forms.fields.summary.label",
-						}),
-						dataIndex: "summary",
-						key: "summary",
-						tip: "过长会自动收缩",
-						ellipsis: true,
-					},
-					{
-						title: intl.formatMessage({
-							id: "forms.fields.type.label",
-						}),
-						dataIndex: "type",
-						key: "type",
-						width: 100,
-						search: false,
-						filters: true,
-						onFilter: true,
-						valueEnum: {
-							all: {
-								text: intl.formatMessage({
-									id: "channel.type.all.title",
-								}),
-								status: "Default",
-							},
-							translation: {
-								text: intl.formatMessage({
-									id: "channel.type.translation.label",
-								}),
-								status: "Success",
-							},
-							nissaya: {
-								text: intl.formatMessage({
-									id: "channel.type.nissaya.label",
-								}),
-								status: "Processing",
-							},
-							commentary: {
-								text: intl.formatMessage({
-									id: "channel.type.commentary.label",
-								}),
-								status: "Default",
-							},
-							original: {
-								text: intl.formatMessage({
-									id: "channel.type.original.label",
-								}),
-								status: "Default",
-							},
-							general: {
-								text: intl.formatMessage({
-									id: "channel.type.general.label",
-								}),
-								status: "Default",
-							},
-						},
-					},
-					{
-						title: intl.formatMessage({
-							id: "forms.fields.publicity.label",
-						}),
-						dataIndex: "publicity",
-						key: "publicity",
-						width: 100,
-						search: false,
-						filters: true,
-						onFilter: true,
-						valueEnum: PublicityValueEnum(),
-					},
-					{
-						title: intl.formatMessage({
-							id: "forms.fields.created-at.label",
-						}),
-						key: "created-at",
-						width: 100,
-						search: false,
-						dataIndex: "createdAt",
-						valueType: "date",
-						sorter: (a, b) => a.createdAt - b.createdAt,
-					},
-					{
-						title: intl.formatMessage({ id: "buttons.option" }),
-						key: "option",
-						width: 120,
-						valueType: "option",
-						render: (text, row, index, action) => {
-							return [
-								<Dropdown.Button
-									key={index}
-									type="link"
-									overlay={menu}
-								>
-									<Link
-										to={`/studio/${studioname}/channel/${row.uid}/edit`}
-									>
-										{intl.formatMessage({
-											id: "buttons.edit",
-										})}
-									</Link>
-								</Dropdown.Button>,
-							];
-						},
-					},
-				]}
-				rowSelection={{
-					// 自定义选择项参考: https://ant.design/components/table-cn/#components-table-demo-row-selection-custom
-					// 注释该行则默认不显示下拉选项
-					selections: [Table.SELECTION_ALL, Table.SELECTION_INVERT],
-				}}
-				tableAlertRender={({
-					selectedRowKeys,
-					selectedRows,
-					onCleanSelected,
-				}) => (
-					<Space size={24}>
-						<span>
-							{intl.formatMessage({ id: "buttons.selected" })}
-							{selectedRowKeys.length}
-							<Button
-								type="link"
-								style={{ marginInlineStart: 8 }}
-								onClick={onCleanSelected}
-							>
-								{intl.formatMessage({ id: "buttons.unselect" })}
-							</Button>
-						</span>
-					</Space>
-				)}
-				tableAlertOptionRender={() => {
-					return (
-						<Space size={16}>
-							<Button type="link">
-								{intl.formatMessage({
-									id: "buttons.delete.all",
-								})}
-							</Button>
-						</Space>
-					);
-				}}
-				request={async (params = {}, sorter, filter) => {
-					// TODO
-					console.log(params, sorter, filter);
+  const intl = useIntl();
+  const { studioname } = useParams();
+  const channelCreate = <ChannelCreate studio={studioname} />;
+  return (
+    <>
+      <ProTable<IItem>
+        columns={[
+          {
+            title: intl.formatMessage({
+              id: "dict.fields.sn.label",
+            }),
+            dataIndex: "id",
+            key: "id",
+            width: 50,
+            search: false,
+          },
+          {
+            title: intl.formatMessage({
+              id: "forms.fields.title.label",
+            }),
+            dataIndex: "title",
+            key: "title",
+            tip: "过长会自动收缩",
+            ellipsis: true,
+            render: (text, row, index, action) => {
+              return (
+                <Link to={`/studio/${studioname}/channel/${row.uid}/edit`}>
+                  {row.title}
+                </Link>
+              );
+            },
+          },
+          {
+            title: intl.formatMessage({
+              id: "forms.fields.summary.label",
+            }),
+            dataIndex: "summary",
+            key: "summary",
+            tip: "过长会自动收缩",
+            ellipsis: true,
+          },
+          {
+            title: intl.formatMessage({
+              id: "forms.fields.type.label",
+            }),
+            dataIndex: "type",
+            key: "type",
+            width: 100,
+            search: false,
+            filters: true,
+            onFilter: true,
+            valueEnum: {
+              all: {
+                text: intl.formatMessage({
+                  id: "channel.type.all.title",
+                }),
+                status: "Default",
+              },
+              translation: {
+                text: intl.formatMessage({
+                  id: "channel.type.translation.label",
+                }),
+                status: "Success",
+              },
+              nissaya: {
+                text: intl.formatMessage({
+                  id: "channel.type.nissaya.label",
+                }),
+                status: "Processing",
+              },
+              commentary: {
+                text: intl.formatMessage({
+                  id: "channel.type.commentary.label",
+                }),
+                status: "Default",
+              },
+              original: {
+                text: intl.formatMessage({
+                  id: "channel.type.original.label",
+                }),
+                status: "Default",
+              },
+              general: {
+                text: intl.formatMessage({
+                  id: "channel.type.general.label",
+                }),
+                status: "Default",
+              },
+            },
+          },
+          {
+            title: intl.formatMessage({
+              id: "forms.fields.publicity.label",
+            }),
+            dataIndex: "publicity",
+            key: "publicity",
+            width: 100,
+            search: false,
+            filters: true,
+            onFilter: true,
+            valueEnum: PublicityValueEnum(),
+          },
+          {
+            title: intl.formatMessage({
+              id: "forms.fields.created-at.label",
+            }),
+            key: "created-at",
+            width: 100,
+            search: false,
+            dataIndex: "createdAt",
+            valueType: "date",
+            sorter: (a, b) => a.createdAt - b.createdAt,
+          },
+          {
+            title: intl.formatMessage({ id: "buttons.option" }),
+            key: "option",
+            width: 120,
+            valueType: "option",
+            render: (text, row, index, action) => {
+              return [
+                <Dropdown.Button key={index} type="link" overlay={menu}>
+                  <Link to={`/studio/${studioname}/channel/${row.uid}/edit`}>
+                    {intl.formatMessage({
+                      id: "buttons.edit",
+                    })}
+                  </Link>
+                </Dropdown.Button>,
+              ];
+            },
+          },
+        ]}
+        rowSelection={{
+          // 自定义选择项参考: https://ant.design/components/table-cn/#components-table-demo-row-selection-custom
+          // 注释该行则默认不显示下拉选项
+          selections: [Table.SELECTION_ALL, Table.SELECTION_INVERT],
+        }}
+        tableAlertRender={({
+          selectedRowKeys,
+          selectedRows,
+          onCleanSelected,
+        }) => (
+          <Space size={24}>
+            <span>
+              {intl.formatMessage({ id: "buttons.selected" })}
+              {selectedRowKeys.length}
+              <Button
+                type="link"
+                style={{ marginInlineStart: 8 }}
+                onClick={onCleanSelected}
+              >
+                {intl.formatMessage({ id: "buttons.unselect" })}
+              </Button>
+            </span>
+          </Space>
+        )}
+        tableAlertOptionRender={() => {
+          return (
+            <Space size={16}>
+              <Button type="link">
+                {intl.formatMessage({
+                  id: "buttons.delete.all",
+                })}
+              </Button>
+            </Space>
+          );
+        }}
+        request={async (params = {}, sorter, filter) => {
+          // TODO
+          console.log(params, sorter, filter);
 
-					const res: IApiResponseChannelList = await get(
-						`/v2/channel?view=studio&name=${studioname}`
-					);
-					const items: IItem[] = res.data.rows.map((item, id) => {
-						const date = new Date(item.created_at);
-						return {
-							id: id,
-							uid: item.uid,
-							title: item.name,
-							summary: item.summary,
-							type: item.type,
-							publicity: item.status,
-							createdAt: date.getTime(),
-						};
-					});
-					return {
-						total: res.data.count,
-						succcess: true,
-						data: items,
-					};
-				}}
-				rowKey="id"
-				bordered
-				pagination={{
-					showQuickJumper: true,
-					showSizeChanger: true,
-				}}
-				search={false}
-				options={{
-					search: true,
-				}}
-				toolBarRender={() => [
-					<Popover
-						content={channelCreate}
-						title="new channel"
-						placement="bottomRight"
-					>
-						<Button
-							key="button"
-							icon={<PlusOutlined />}
-							type="primary"
-						>
-							{intl.formatMessage({ id: "buttons.create" })}
-						</Button>
-					</Popover>,
-				]}
-			/>
-		</>
-	);
+          const res: IApiResponseChannelList = await get(
+            `/v2/channel?view=studio&name=${studioname}`
+          );
+          const items: IItem[] = res.data.rows.map((item, id) => {
+            const date = new Date(item.created_at);
+            return {
+              id: id,
+              uid: item.uid,
+              title: item.name,
+              summary: item.summary,
+              type: item.type,
+              publicity: item.status,
+              createdAt: date.getTime(),
+            };
+          });
+          return {
+            total: res.data.count,
+            succcess: true,
+            data: items,
+          };
+        }}
+        rowKey="id"
+        bordered
+        pagination={{
+          showQuickJumper: true,
+          showSizeChanger: true,
+        }}
+        search={false}
+        options={{
+          search: true,
+        }}
+        toolBarRender={() => [
+          <Popover
+            content={channelCreate}
+            title="new channel"
+            placement="bottomRight"
+          >
+            <Button key="button" icon={<PlusOutlined />} type="primary">
+              {intl.formatMessage({ id: "buttons.create" })}
+            </Button>
+          </Popover>,
+        ]}
+      />
+    </>
+  );
 };
 
 export default Widget;

+ 291 - 295
dashboard/src/pages/studio/dict/list.tsx

@@ -3,325 +3,321 @@ import { ProTable } from "@ant-design/pro-components";
 import { useIntl } from "react-intl";
 
 import {
-	Button,
-	Layout,
-	Space,
-	Table,
-	Dropdown,
-	MenuProps,
-	Menu,
-	Drawer,
+  Button,
+  Layout,
+  Space,
+  Table,
+  Dropdown,
+  MenuProps,
+  Menu,
+  Drawer,
 } from "antd";
 import { PlusOutlined, SearchOutlined } from "@ant-design/icons";
 
-import DictCreate from "../../../components/studio/dict/DictCreate";
+import DictCreate from "../../../components/dict/DictCreate";
 import { IApiResponseDictList } from "../../../components/api/Dict";
 import { get } from "../../../request";
 import { useState } from "react";
-import DictEdit from "../../../components/studio/dict/DictEdit";
+import DictEdit from "../../../components/dict/DictEdit";
 
 const onMenuClick: MenuProps["onClick"] = (e) => {
-	console.log("click", e);
+  console.log("click", e);
 };
 
 const menu = (
-	<Menu
-		onClick={onMenuClick}
-		items={[
-			{
-				key: "1",
-				label: "在藏经阁中打开",
-				icon: <SearchOutlined />,
-			},
-			{
-				key: "2",
-				label: "分享",
-				icon: <SearchOutlined />,
-			},
-			{
-				key: "3",
-				label: "删除",
-				icon: <SearchOutlined />,
-			},
-		]}
-	/>
+  <Menu
+    onClick={onMenuClick}
+    items={[
+      {
+        key: "1",
+        label: "在藏经阁中打开",
+        icon: <SearchOutlined />,
+      },
+      {
+        key: "2",
+        label: "分享",
+        icon: <SearchOutlined />,
+      },
+      {
+        key: "3",
+        label: "删除",
+        icon: <SearchOutlined />,
+      },
+    ]}
+  />
 );
 
 interface IItem {
-	id: number;
-	wordId: number;
-	word: string;
-	type: string;
-	grammar: string;
-	parent: string;
-	meaning: string;
-	note: string;
-	factors: string;
-	createdAt: number;
+  id: number;
+  wordId: number;
+  word: string;
+  type: string;
+  grammar: string;
+  parent: string;
+  meaning: string;
+  note: string;
+  factors: string;
+  createdAt: number;
 }
 
 const valueEnum = {
-	0: "n",
-	1: "ti",
-	2: "v",
-	3: "ind",
+  0: "n",
+  1: "ti",
+  2: "v",
+  3: "ind",
 };
 
 const Widget = () => {
-	const intl = useIntl();
-	const { studioname } = useParams();
-	const [isEditOpen, setIsEditOpen] = useState(false);
-	const [isCreateOpen, setIsCreateOpen] = useState(false);
-	const [wordId, setWordId] = useState(0);
-	const [drawerTitle, setDrawerTitle] = useState("New Word");
+  const intl = useIntl();
+  const { studioname } = useParams();
+  const [isEditOpen, setIsEditOpen] = useState(false);
+  const [isCreateOpen, setIsCreateOpen] = useState(false);
+  const [wordId, setWordId] = useState(0);
+  const [drawerTitle, setDrawerTitle] = useState("New Word");
 
-	return (
-		<>
-			<Layout>{studioname}</Layout>
-			<ProTable<IItem>
-				columns={[
-					{
-						title: intl.formatMessage({
-							id: "dict.fields.sn.label",
-						}),
-						dataIndex: "id",
-						key: "id",
-						width: 80,
-						search: false,
-					},
-					{
-						title: intl.formatMessage({
-							id: "dict.fields.word.label",
-						}),
-						dataIndex: "word",
-						key: "word",
-						tip: "单词过长会自动收缩",
-						ellipsis: true,
-					},
-					{
-						title: intl.formatMessage({
-							id: "dict.fields.type.label",
-						}),
-						dataIndex: "type",
-						key: "type",
-						search: false,
-						filters: true,
-						onFilter: true,
-						valueEnum: {
-							all: { text: "全部", status: "Default" },
-							n: { text: "名词", status: "Default" },
-							ti: { text: "三性", status: "Processing" },
-							v: { text: "动词", status: "Success" },
-							ind: { text: "不变词", status: "Success" },
-						},
-					},
-					{
-						title: intl.formatMessage({
-							id: "dict.fields.grammar.label",
-						}),
-						dataIndex: "grammar",
-						key: "grammar",
-						search: false,
-					},
-					{
-						title: intl.formatMessage({
-							id: "dict.fields.parent.label",
-						}),
-						dataIndex: "parent",
-						key: "parent",
-					},
-					{
-						title: intl.formatMessage({
-							id: "dict.fields.meaning.label",
-						}),
-						dataIndex: "meaning",
-						key: "meaning",
-						tip: "意思过长会自动收缩",
-						ellipsis: true,
-					},
-					{
-						title: intl.formatMessage({
-							id: "dict.fields.note.label",
-						}),
-						dataIndex: "note",
-						key: "note",
-						search: false,
-						tip: "注释过长会自动收缩",
-						ellipsis: true,
-					},
-					{
-						title: intl.formatMessage({
-							id: "dict.fields.factors.label",
-						}),
-						dataIndex: "factors",
-						key: "factors",
-						search: false,
-					},
-					{
-						title: intl.formatMessage({
-							id: "forms.fields.created-at.label",
-						}),
-						key: "created-at",
-						width: 200,
+  return (
+    <>
+      <Layout>{studioname}</Layout>
+      <ProTable<IItem>
+        columns={[
+          {
+            title: intl.formatMessage({
+              id: "dict.fields.sn.label",
+            }),
+            dataIndex: "id",
+            key: "id",
+            width: 80,
+            search: false,
+          },
+          {
+            title: intl.formatMessage({
+              id: "dict.fields.word.label",
+            }),
+            dataIndex: "word",
+            key: "word",
+            tip: "单词过长会自动收缩",
+            ellipsis: true,
+          },
+          {
+            title: intl.formatMessage({
+              id: "dict.fields.type.label",
+            }),
+            dataIndex: "type",
+            key: "type",
+            search: false,
+            filters: true,
+            onFilter: true,
+            valueEnum: {
+              all: { text: "全部", status: "Default" },
+              n: { text: "名词", status: "Default" },
+              ti: { text: "三性", status: "Processing" },
+              v: { text: "动词", status: "Success" },
+              ind: { text: "不变词", status: "Success" },
+            },
+          },
+          {
+            title: intl.formatMessage({
+              id: "dict.fields.grammar.label",
+            }),
+            dataIndex: "grammar",
+            key: "grammar",
+            search: false,
+          },
+          {
+            title: intl.formatMessage({
+              id: "dict.fields.parent.label",
+            }),
+            dataIndex: "parent",
+            key: "parent",
+          },
+          {
+            title: intl.formatMessage({
+              id: "dict.fields.meaning.label",
+            }),
+            dataIndex: "meaning",
+            key: "meaning",
+            tip: "意思过长会自动收缩",
+            ellipsis: true,
+          },
+          {
+            title: intl.formatMessage({
+              id: "dict.fields.note.label",
+            }),
+            dataIndex: "note",
+            key: "note",
+            search: false,
+            tip: "注释过长会自动收缩",
+            ellipsis: true,
+          },
+          {
+            title: intl.formatMessage({
+              id: "dict.fields.factors.label",
+            }),
+            dataIndex: "factors",
+            key: "factors",
+            search: false,
+          },
+          {
+            title: intl.formatMessage({
+              id: "forms.fields.created-at.label",
+            }),
+            key: "created-at",
+            width: 200,
 
-						search: false,
-						dataIndex: "createdAt",
-						valueType: "date",
-						sorter: (a, b) => a.createdAt - b.createdAt,
-					},
-					{
-						title: intl.formatMessage({ id: "buttons.option" }),
-						key: "option",
-						width: 120,
-						valueType: "option",
-						render: (text, row, index, action) => {
-							return [
-								<Dropdown.Button
-									key={index}
-									type="link"
-									overlay={menu}
-									onClick={() => {
-										setWordId(row.wordId);
-										setDrawerTitle(row.word);
-										setIsEditOpen(true);
-									}}
-								>
-									{intl.formatMessage({
-										id: "buttons.edit",
-									})}
-								</Dropdown.Button>,
-							];
-						},
-					},
-				]}
-				rowSelection={{
-					// 自定义选择项参考: https://ant.design/components/table-cn/#components-table-demo-row-selection-custom
-					// 注释该行则默认不显示下拉选项
-					selections: [Table.SELECTION_ALL, Table.SELECTION_INVERT],
-				}}
-				tableAlertRender={({
-					selectedRowKeys,
-					selectedRows,
-					onCleanSelected,
-				}) => (
-					<Space size={24}>
-						<span>
-							{intl.formatMessage({ id: "buttons.selected" })}
-							{selectedRowKeys.length}
-							<Button
-								type="link"
-								style={{ marginInlineStart: 8 }}
-								onClick={onCleanSelected}
-							>
-								{intl.formatMessage({ id: "buttons.unselect" })}
-							</Button>
-						</span>
-					</Space>
-				)}
-				tableAlertOptionRender={() => {
-					return (
-						<Space size={16}>
-							<Button type="link">批量删除</Button>
-							<Button type="link">导出数据</Button>
-						</Space>
-					);
-				}}
-				request={async (params = {}, sorter, filter) => {
-					// TODO
-					console.log(params, sorter, filter);
-					const offset =
-						((params.current ? params.current : 1) - 1) *
-						(params.pageSize ? params.pageSize : 20);
-					let url = `/v2/userdict?view=studio&name=${studioname}&limit=${params.pageSize}&offset=${offset}`;
-					if (typeof params.keyword !== "undefined") {
-						url +=
-							"&search=" + (params.keyword ? params.keyword : "");
-					}
-					console.log(url);
-					const res: IApiResponseDictList = await get(url);
+            search: false,
+            dataIndex: "createdAt",
+            valueType: "date",
+            sorter: (a, b) => a.createdAt - b.createdAt,
+          },
+          {
+            title: intl.formatMessage({ id: "buttons.option" }),
+            key: "option",
+            width: 120,
+            valueType: "option",
+            render: (text, row, index, action) => {
+              return [
+                <Dropdown.Button
+                  key={index}
+                  type="link"
+                  overlay={menu}
+                  onClick={() => {
+                    setWordId(row.wordId);
+                    setDrawerTitle(row.word);
+                    setIsEditOpen(true);
+                  }}
+                >
+                  {intl.formatMessage({
+                    id: "buttons.edit",
+                  })}
+                </Dropdown.Button>,
+              ];
+            },
+          },
+        ]}
+        rowSelection={{
+          // 自定义选择项参考: https://ant.design/components/table-cn/#components-table-demo-row-selection-custom
+          // 注释该行则默认不显示下拉选项
+          selections: [Table.SELECTION_ALL, Table.SELECTION_INVERT],
+        }}
+        tableAlertRender={({
+          selectedRowKeys,
+          selectedRows,
+          onCleanSelected,
+        }) => (
+          <Space size={24}>
+            <span>
+              {intl.formatMessage({ id: "buttons.selected" })}
+              {selectedRowKeys.length}
+              <Button
+                type="link"
+                style={{ marginInlineStart: 8 }}
+                onClick={onCleanSelected}
+              >
+                {intl.formatMessage({ id: "buttons.unselect" })}
+              </Button>
+            </span>
+          </Space>
+        )}
+        tableAlertOptionRender={() => {
+          return (
+            <Space size={16}>
+              <Button type="link">批量删除</Button>
+              <Button type="link">导出数据</Button>
+            </Space>
+          );
+        }}
+        request={async (params = {}, sorter, filter) => {
+          // TODO
+          console.log(params, sorter, filter);
+          const offset =
+            ((params.current ? params.current : 1) - 1) *
+            (params.pageSize ? params.pageSize : 20);
+          let url = `/v2/userdict?view=studio&name=${studioname}&limit=${params.pageSize}&offset=${offset}`;
+          if (typeof params.keyword !== "undefined") {
+            url += "&search=" + (params.keyword ? params.keyword : "");
+          }
+          console.log(url);
+          const res: IApiResponseDictList = await get(url);
 
-					const items: IItem[] = res.data.rows.map((item, id) => {
-						const date = new Date(item.updated_at);
-						const id2 =
-							((params.current || 1) - 1) *
-								(params.pageSize || 20) +
-							id +
-							1;
-						return {
-							id: id2,
-							wordId: item.id,
-							word: item.word,
-							type: item.type,
-							grammar: item.grammar,
-							parent: item.parent,
-							meaning: item.mean,
-							note: item.note,
-							factors: item.factors,
-							createdAt: date.getTime(),
-						};
-					});
-					return {
-						total: res.data.count,
-						success: true,
-						data: items,
-					};
-				}}
-				rowKey="id"
-				bordered
-				pagination={{
-					showQuickJumper: true,
-					showSizeChanger: true,
-				}}
-				search={false}
-				options={{
-					search: true,
-				}}
-				headerTitle=""
-				toolBarRender={() => [
-					<Button
-						key="button"
-						icon={<PlusOutlined />}
-						type="primary"
-						onClick={() => {
-							setDrawerTitle("New word");
-							setIsCreateOpen(true);
-						}}
-					>
-						{intl.formatMessage({ id: "buttons.create" })}
-					</Button>,
-				]}
-			/>
+          const items: IItem[] = res.data.rows.map((item, id) => {
+            const date = new Date(item.updated_at);
+            const id2 =
+              ((params.current || 1) - 1) * (params.pageSize || 20) + id + 1;
+            return {
+              id: id2,
+              wordId: item.id,
+              word: item.word,
+              type: item.type,
+              grammar: item.grammar,
+              parent: item.parent,
+              meaning: item.mean,
+              note: item.note,
+              factors: item.factors,
+              createdAt: date.getTime(),
+            };
+          });
+          return {
+            total: res.data.count,
+            success: true,
+            data: items,
+          };
+        }}
+        rowKey="id"
+        bordered
+        pagination={{
+          showQuickJumper: true,
+          showSizeChanger: true,
+        }}
+        search={false}
+        options={{
+          search: true,
+        }}
+        headerTitle=""
+        toolBarRender={() => [
+          <Button
+            key="button"
+            icon={<PlusOutlined />}
+            type="primary"
+            onClick={() => {
+              setDrawerTitle("New word");
+              setIsCreateOpen(true);
+            }}
+          >
+            {intl.formatMessage({ id: "buttons.create" })}
+          </Button>,
+        ]}
+      />
 
-			<Drawer
-				title={drawerTitle}
-				placement="right"
-				open={isCreateOpen}
-				onClose={() => {
-					setIsCreateOpen(false);
-				}}
-				key="create"
-				style={{ maxWidth: "100%" }}
-				contentWrapperStyle={{ overflowY: "auto" }}
-				footer={null}
-			>
-				<DictCreate studio={studioname ? studioname : ""} />
-			</Drawer>
-			<Drawer
-				title={drawerTitle}
-				placement="right"
-				open={isEditOpen}
-				onClose={() => {
-					setIsEditOpen(false);
-				}}
-				key="edit"
-				style={{ maxWidth: "100%" }}
-				contentWrapperStyle={{ overflowY: "auto" }}
-				footer={null}
-			>
-				<DictEdit wordId={wordId} />
-			</Drawer>
-		</>
-	);
+      <Drawer
+        title={drawerTitle}
+        placement="right"
+        open={isCreateOpen}
+        onClose={() => {
+          setIsCreateOpen(false);
+        }}
+        key="create"
+        style={{ maxWidth: "100%" }}
+        contentWrapperStyle={{ overflowY: "auto" }}
+        footer={null}
+      >
+        <DictCreate studio={studioname ? studioname : ""} />
+      </Drawer>
+      <Drawer
+        title={drawerTitle}
+        placement="right"
+        open={isEditOpen}
+        onClose={() => {
+          setIsEditOpen(false);
+        }}
+        key="edit"
+        style={{ maxWidth: "100%" }}
+        contentWrapperStyle={{ overflowY: "auto" }}
+        footer={null}
+      >
+        <DictEdit wordId={wordId} />
+      </Drawer>
+    </>
+  );
 };
 
 export default Widget;

+ 149 - 162
dashboard/src/pages/studio/group/list.tsx

@@ -5,7 +5,7 @@ import { ProTable } from "@ant-design/pro-components";
 import { Button, Popover, Typography, Dropdown, Menu, MenuProps } from "antd";
 import { PlusOutlined, SearchOutlined } from "@ant-design/icons";
 
-import GroupCreate from "../../../components/studio/group/GroupCreate";
+import GroupCreate from "../../../components/group/GroupCreate";
 import { RoleValueEnum } from "../../../components/studio/table";
 import { IGroupListResponse } from "../../../components/api/Group";
 import { get } from "../../../request";
@@ -13,176 +13,163 @@ import { get } from "../../../request";
 const { Text } = Typography;
 
 const onMenuClick: MenuProps["onClick"] = (e) => {
-	console.log("click", e);
+  console.log("click", e);
 };
 const menu = (
-	<Menu
-		onClick={onMenuClick}
-		items={[
-			{
-				key: "1",
-				label: "在藏经阁中打开",
-				icon: <SearchOutlined />,
-			},
-			{
-				key: "2",
-				label: "分享",
-				icon: <SearchOutlined />,
-			},
-		]}
-	/>
+  <Menu
+    onClick={onMenuClick}
+    items={[
+      {
+        key: "1",
+        label: "在藏经阁中打开",
+        icon: <SearchOutlined />,
+      },
+      {
+        key: "2",
+        label: "分享",
+        icon: <SearchOutlined />,
+      },
+    ]}
+  />
 );
 
 interface DataItem {
-	sn: number;
-	id: string;
-	name: string;
-	description: string;
-	role: string;
-	createdAt: number;
+  sn: number;
+  id: string;
+  name: string;
+  description: string;
+  role: string;
+  createdAt: number;
 }
 
 const Widget = () => {
-	const intl = useIntl(); //i18n
-	const { studioname } = useParams(); //url 参数
-	return (
-		<>
-			<ProTable<DataItem>
-				columns={[
-					{
-						title: intl.formatMessage({
-							id: "dict.fields.sn.label",
-						}),
-						dataIndex: "sn",
-						key: "sn",
-						width: 50,
-						search: false,
-					},
-					{
-						title: intl.formatMessage({
-							id: "forms.fields.name.label",
-						}),
-						dataIndex: "name",
-						key: "name",
-						tip: "过长会自动收缩",
-						ellipsis: true,
-						render: (text, row, index, action) => {
-							return (
-								<div>
-									<div>
-										<Link
-											to={`/studio/${studioname}/group/${row.id}/show`}
-										>
-											{row.name}
-										</Link>
-									</div>
-									<Text type="secondary">
-										{row.description}
-									</Text>
-								</div>
-							);
-						},
-					},
-					{
-						title: intl.formatMessage({
-							id: "forms.fields.role.label",
-						}),
-						dataIndex: "role",
-						key: "role",
-						width: 100,
-						search: false,
-						filters: true,
-						onFilter: true,
-						valueEnum: RoleValueEnum(),
-					},
-					{
-						title: intl.formatMessage({
-							id: "forms.fields.created-at.label",
-						}),
-						key: "created-at",
-						width: 100,
-						search: false,
-						dataIndex: "createdAt",
-						valueType: "date",
-						sorter: (a, b) => a.createdAt - b.createdAt,
-					},
-					{
-						title: intl.formatMessage({ id: "buttons.option" }),
-						key: "option",
-						width: 120,
-						valueType: "option",
-						render: (text, row, index, action) => [
-							<Dropdown.Button
-								type="link"
-								key={index}
-								overlay={menu}
-							>
-								{intl.formatMessage({
-									id: "buttons.edit",
-								})}
-							</Dropdown.Button>,
-						],
-					},
-				]}
-				request={async (params = {}, sorter, filter) => {
-					// TODO
-					console.log(params, sorter, filter);
-					let url = `/v2/group?view=studio&name=${studioname}`;
-					const offset =
-						((params.current ? params.current : 1) - 1) *
-						(params.pageSize ? params.pageSize : 20);
-					url += `&limit=${params.pageSize}&offset=${offset}`;
-					if (typeof params.keyword !== "undefined") {
-						url +=
-							"&search=" + (params.keyword ? params.keyword : "");
-					}
+  const intl = useIntl(); //i18n
+  const { studioname } = useParams(); //url 参数
+  return (
+    <>
+      <ProTable<DataItem>
+        columns={[
+          {
+            title: intl.formatMessage({
+              id: "dict.fields.sn.label",
+            }),
+            dataIndex: "sn",
+            key: "sn",
+            width: 50,
+            search: false,
+          },
+          {
+            title: intl.formatMessage({
+              id: "forms.fields.name.label",
+            }),
+            dataIndex: "name",
+            key: "name",
+            tip: "过长会自动收缩",
+            ellipsis: true,
+            render: (text, row, index, action) => {
+              return (
+                <div>
+                  <div>
+                    <Link to={`/studio/${studioname}/group/${row.id}/show`}>
+                      {row.name}
+                    </Link>
+                  </div>
+                  <Text type="secondary">{row.description}</Text>
+                </div>
+              );
+            },
+          },
+          {
+            title: intl.formatMessage({
+              id: "forms.fields.role.label",
+            }),
+            dataIndex: "role",
+            key: "role",
+            width: 100,
+            search: false,
+            filters: true,
+            onFilter: true,
+            valueEnum: RoleValueEnum(),
+          },
+          {
+            title: intl.formatMessage({
+              id: "forms.fields.created-at.label",
+            }),
+            key: "created-at",
+            width: 100,
+            search: false,
+            dataIndex: "createdAt",
+            valueType: "date",
+            sorter: (a, b) => a.createdAt - b.createdAt,
+          },
+          {
+            title: intl.formatMessage({ id: "buttons.option" }),
+            key: "option",
+            width: 120,
+            valueType: "option",
+            render: (text, row, index, action) => [
+              <Dropdown.Button type="link" key={index} overlay={menu}>
+                {intl.formatMessage({
+                  id: "buttons.edit",
+                })}
+              </Dropdown.Button>,
+            ],
+          },
+        ]}
+        request={async (params = {}, sorter, filter) => {
+          // TODO
+          console.log(params, sorter, filter);
+          let url = `/v2/group?view=studio&name=${studioname}`;
+          const offset =
+            ((params.current ? params.current : 1) - 1) *
+            (params.pageSize ? params.pageSize : 20);
+          url += `&limit=${params.pageSize}&offset=${offset}`;
+          if (typeof params.keyword !== "undefined") {
+            url += "&search=" + (params.keyword ? params.keyword : "");
+          }
 
-					const res = await get<IGroupListResponse>(url);
-					const items: DataItem[] = res.data.rows.map((item, id) => {
-						const date = new Date(item.created_at);
-						return {
-							sn: id + 1,
-							id: item.uid,
-							name: item.name,
-							description: item.description,
-							role: item.role,
-							createdAt: date.getTime(),
-						};
-					});
-					console.log(items);
-					return {
-						total: res.data.count,
-						succcess: true,
-						data: items,
-					};
-				}}
-				rowKey="id"
-				bordered
-				pagination={{
-					showQuickJumper: true,
-					showSizeChanger: true,
-				}}
-				search={false}
-				options={{
-					search: true,
-				}}
-				toolBarRender={() => [
-					<Popover
-						content={<GroupCreate studio={studioname} />}
-						placement="bottomRight"
-					>
-						<Button
-							key="button"
-							icon={<PlusOutlined />}
-							type="primary"
-						>
-							{intl.formatMessage({ id: "buttons.create" })}
-						</Button>
-					</Popover>,
-				]}
-			/>
-		</>
-	);
+          const res = await get<IGroupListResponse>(url);
+          const items: DataItem[] = res.data.rows.map((item, id) => {
+            const date = new Date(item.created_at);
+            return {
+              sn: id + 1,
+              id: item.uid,
+              name: item.name,
+              description: item.description,
+              role: item.role,
+              createdAt: date.getTime(),
+            };
+          });
+          console.log(items);
+          return {
+            total: res.data.count,
+            succcess: true,
+            data: items,
+          };
+        }}
+        rowKey="id"
+        bordered
+        pagination={{
+          showQuickJumper: true,
+          showSizeChanger: true,
+        }}
+        search={false}
+        options={{
+          search: true,
+        }}
+        toolBarRender={() => [
+          <Popover
+            content={<GroupCreate studio={studioname} />}
+            placement="bottomRight"
+          >
+            <Button key="button" icon={<PlusOutlined />} type="primary">
+              {intl.formatMessage({ id: "buttons.create" })}
+            </Button>
+          </Popover>,
+        ]}
+      />
+    </>
+  );
 };
 
 export default Widget;

+ 29 - 31
dashboard/src/pages/studio/group/show.tsx

@@ -2,43 +2,41 @@ import { useParams } from "react-router-dom";
 import { useIntl } from "react-intl";
 import { Button, Card } from "antd";
 import { Col, Row } from "antd";
-import GroupFile from "../../../components/studio/group/GroupFile";
-import GroupMember from "../../../components/studio/group/GroupMember";
+import GroupFile from "../../../components/group/GroupFile";
+import GroupMember from "../../../components/group/GroupMember";
 import { useEffect, useState } from "react";
 import { get } from "../../../request";
 import { IGroupResponse } from "../../../components/api/Group";
 import GoBack from "../../../components/studio/GoBack";
 
 const Widget = () => {
-	const intl = useIntl();
-	const { studioname, groupid } = useParams(); //url 参数
-	const [title, setTitle] = useState("loading");
-	useEffect(() => {
-		get<IGroupResponse>(`/v2/group/${groupid}`).then((json) => {
-			setTitle(json.data.name);
-		});
-	}, [groupid]);
-	return (
-		<Card
-			title={
-				<GoBack to={`/studio/${studioname}/group/list`} title={title} />
-			}
-			extra={
-				<Button type="link" danger>
-					退群
-				</Button>
-			}
-		>
-			<Row>
-				<Col flex="auto">
-					<GroupFile groupid={groupid} />
-				</Col>
-				<Col flex="400px">
-					<GroupMember groupid={groupid} />
-				</Col>
-			</Row>
-		</Card>
-	);
+  const intl = useIntl();
+  const { studioname, groupid } = useParams(); //url 参数
+  const [title, setTitle] = useState("loading");
+  useEffect(() => {
+    get<IGroupResponse>(`/v2/group/${groupid}`).then((json) => {
+      setTitle(json.data.name);
+    });
+  }, [groupid]);
+  return (
+    <Card
+      title={<GoBack to={`/studio/${studioname}/group/list`} title={title} />}
+      extra={
+        <Button type="link" danger>
+          退群
+        </Button>
+      }
+    >
+      <Row>
+        <Col flex="auto">
+          <GroupFile groupid={groupid} />
+        </Col>
+        <Col flex="400px">
+          <GroupMember groupid={groupid} />
+        </Col>
+      </Row>
+    </Card>
+  );
 };
 
 export default Widget;

+ 1 - 1
dashboard/src/pages/studio/term/list.tsx

@@ -8,7 +8,7 @@ import {
   ShareAltOutlined,
 } from "@ant-design/icons";
 
-import TermCreate from "../../../components/studio/term/TermCreate";
+import TermCreate from "../../../components/term/TermCreate";
 import { ITermListResponse } from "../../../components/api/Term";
 import { get } from "../../../request";
 

+ 1 - 1
dashboard/src/reducers/command.ts

@@ -3,7 +3,7 @@
  */
 import { createSlice, PayloadAction } from "@reduxjs/toolkit";
 import { IWidgetDict } from "../components/dict/DictComponent";
-import { IWidgetDictCreate } from "../components/studio/term/TermCreate";
+import { IWidgetDictCreate } from "../components/term/TermCreate";
 
 import type { RootState } from "../store";