فهرست منبع

Merge pull request #1328 from visuddhinanda/agile

支持mermaid
visuddhinanda 2 سال پیش
والد
کامیت
08fc94c10b

+ 2 - 0
dashboard/src/components/api/Term.ts

@@ -1,6 +1,7 @@
 import { IStudio } from "../auth/StudioName";
 import { IUser } from "../auth/User";
 import { IChannel } from "../channel/Channel";
+import { TRole } from "./Auth";
 
 export interface ITermDataRequest {
   id?: string;
@@ -28,6 +29,7 @@ export interface ITermDataResponse {
   channel?: IChannel;
   studio: IStudio;
   editor: IUser;
+  role?: TRole;
   language: string;
   created_at: string;
   updated_at: string;

+ 66 - 80
dashboard/src/components/article/ArticleEdit.tsx

@@ -7,7 +7,7 @@ import {
   ProFormTextArea,
 } from "@ant-design/pro-components";
 
-import { Alert, Button, Form, message, Result, Space, Tabs } from "antd";
+import { Alert, Button, Form, message, Result, Space } from "antd";
 
 import { get, put } from "../../request";
 import {
@@ -60,7 +60,7 @@ const ArticleEditWidget = ({
     <>
       {readonly ? (
         <Alert
-          message="文章为只读,如果需要修改,请联络拥有者分配权限。"
+          message="该资源为只读,如果需要修改,请联络拥有者分配权限。"
           type="warning"
           closable
           action={
@@ -133,84 +133,70 @@ const ArticleEditWidget = ({
           };
         }}
       >
-        <Tabs
-          items={[
-            {
-              key: "info",
-              label: intl.formatMessage({ id: "course.basic.info.label" }),
-              children: (
-                <>
-                  <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",
-                          }),
-                        },
-                      ]}
-                    />
-                    <ProFormText
-                      width="md"
-                      name="subtitle"
-                      label={intl.formatMessage({
-                        id: "forms.fields.subtitle.label",
-                      })}
-                    />
-                  </ProForm.Group>
-                  <ProForm.Group>
-                    <LangSelect width="md" />
-                    <PublicitySelect width="md" />
-                  </ProForm.Group>
-                  <ProForm.Group>
-                    <ProFormTextArea
-                      name="summary"
-                      width="lg"
-                      label={intl.formatMessage({
-                        id: "forms.fields.summary.label",
-                      })}
-                    />
-                  </ProForm.Group>
-                </>
-              ),
-            },
-            {
-              key: "content",
-              label: intl.formatMessage({ id: "forms.fields.content.label" }),
-              forceRender: true,
-              children: (
-                <ProForm.Group>
-                  <Form.Item
-                    name="content"
-                    label={
-                      <Space>
-                        {intl.formatMessage({
-                          id: "forms.fields.content.label",
-                        })}
-                        {articleId ? (
-                          <ArticlePrevDrawer
-                            trigger={<Button>预览</Button>}
-                            articleId={articleId}
-                            content={content}
-                          />
-                        ) : undefined}
-                      </Space>
-                    }
-                  >
-                    <MDEditor onChange={(value) => setContent(value)} />
-                  </Form.Item>
-                </ProForm.Group>
-              ),
-            },
-          ]}
-        />
+        <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",
+                }),
+              },
+            ]}
+          />
+          <ProFormText
+            width="md"
+            name="subtitle"
+            label={intl.formatMessage({
+              id: "forms.fields.subtitle.label",
+            })}
+          />
+        </ProForm.Group>
+        <ProForm.Group>
+          <LangSelect width="md" />
+          <PublicitySelect width="md" />
+        </ProForm.Group>
+        <ProForm.Group>
+          <ProFormTextArea
+            name="summary"
+            width="lg"
+            label={intl.formatMessage({
+              id: "forms.fields.summary.label",
+            })}
+          />
+        </ProForm.Group>
+        <ProForm.Group>
+          <Form.Item
+            name="content"
+            style={{ width: "100%" }}
+            label={
+              <Space>
+                {intl.formatMessage({
+                  id: "forms.fields.content.label",
+                })}
+                {articleId ? (
+                  <ArticlePrevDrawer
+                    trigger={<Button>预览</Button>}
+                    articleId={articleId}
+                    content={content}
+                  />
+                ) : undefined}
+              </Space>
+            }
+          >
+            <MDEditor
+              onChange={(value) => setContent(value)}
+              height={550}
+              style={{ width: "100%" }}
+            />
+          </Form.Item>
+        </ProForm.Group>
       </ProForm>
     </>
   );

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

@@ -1,7 +1,9 @@
 import Article from "./Article";
 import Exercise from "./Exercise";
+import Mermaid from "./Mermaid";
 import Nissaya from "./Nissaya";
 import Note from "./Note";
+import ParaHandle from "./ParaHandle";
 import Quote from "./Quote";
 import SentEdit from "./SentEdit";
 import SentRead from "./SentRead";
@@ -39,6 +41,10 @@ const Widget = ({ tpl, props, children }: IWidgetMdTpl) => {
       return <Nissaya props={props ? props : ""} />;
     case "toggle":
       return <Toggle props={props ? props : undefined}>{children}</Toggle>;
+    case "para":
+      return <ParaHandle props={props ? props : ""} />;
+    case "mermaid":
+      return <Mermaid props={props ? props : ""} />;
     default:
       return <>未定义模版({tpl})</>;
   }

+ 22 - 0
dashboard/src/components/template/Mermaid.tsx

@@ -0,0 +1,22 @@
+import Mermaid from "../general/Mermaid";
+
+interface IWidgetMermaidCtl {
+  text?: string;
+}
+const MermaidCtl = ({ text }: IWidgetMermaidCtl) => {
+  return <Mermaid text={text} />;
+};
+
+interface IWidget {
+  props: string;
+}
+const Widget = ({ props }: IWidget) => {
+  const prop = JSON.parse(atob(props)) as IWidgetMermaidCtl;
+  return (
+    <>
+      <MermaidCtl {...prop} />
+    </>
+  );
+};
+
+export default Widget;

+ 79 - 0
dashboard/src/components/template/ParaHandle.tsx

@@ -0,0 +1,79 @@
+import { Button, Divider, Dropdown, MenuProps, message, Popover } from "antd";
+import { useNavigate } from "react-router-dom";
+import { fullUrl } from "../../utils";
+
+interface IWidgetParaHandleCtl {
+  book: number;
+  para: number;
+  channels?: string[];
+  sentences: string[];
+}
+const ParaHandleCtl = ({
+  book,
+  para,
+  channels,
+  sentences,
+}: IWidgetParaHandleCtl) => {
+  const navigate = useNavigate();
+  const items: MenuProps["items"] = [
+    {
+      key: "solo",
+      label: "仅显示此段",
+    },
+    {
+      key: "solo-in-tab",
+      label: "在标签页打开此段",
+    },
+    {
+      key: "copy-sent",
+      label: "复制句子链接",
+    },
+  ];
+  const onClick: MenuProps["onClick"] = (e) => {
+    const channelQuery = channels?.join("_");
+    const url = `/article/para/${book}-${para}?mode=read&book=${book}&par=${para}&channel=${channelQuery}`;
+    switch (e.key) {
+      case "solo":
+        navigate(url);
+        break;
+      case "solo-in-tab":
+        window.open(fullUrl(url), "_blank");
+        break;
+      case "copy-sent":
+        navigator.clipboard
+          .writeText(sentences.map((item) => `{{${item}}}`).join(""))
+          .then(() => {
+            message.success("链接地址已经拷贝到剪贴板");
+          });
+        break;
+      default:
+        break;
+    }
+  };
+  return (
+    <Divider orientation="left">
+      <Dropdown
+        menu={{ items, onClick }}
+        placement="bottomLeft"
+        trigger={["click"]}
+      >
+        <Button type="text">{para}</Button>
+      </Dropdown>
+    </Divider>
+  );
+};
+
+interface IWidget {
+  props: string;
+}
+const Widget = ({ props }: IWidget) => {
+  const prop = JSON.parse(atob(props)) as IWidgetParaHandleCtl;
+  console.log(prop);
+  return (
+    <>
+      <ParaHandleCtl {...prop} />
+    </>
+  );
+};
+
+export default Widget;

+ 3 - 5
dashboard/src/components/template/SentEdit/SentTab.tsx

@@ -1,9 +1,9 @@
+import { useEffect, useState } from "react";
 import { Badge, Space, Tabs, Typography } from "antd";
 import {
   TranslationOutlined,
   CloseOutlined,
   BlockOutlined,
-  BarsOutlined,
 } from "@ant-design/icons";
 
 import SentTabButton from "./SentTabButton";
@@ -14,10 +14,6 @@ import TocPath, { ITocPathNode } from "../../corpus/TocPath";
 import { IWbw } from "../Wbw/WbwWord";
 import RelaGraphic from "../Wbw/RelaGraphic";
 import SentMenu from "./SentMenu";
-import { IChapter } from "../../corpus/BookViewer";
-import store from "../../../store";
-import { change } from "../../../reducers/para-change";
-import { useEffect, useState } from "react";
 
 const { Text } = Typography;
 
@@ -159,6 +155,7 @@ const SentTabWidget = ({
                 wordStart={parseInt(sId[2])}
                 wordEnd={parseInt(sId[3])}
                 type="translation"
+                channelsId={channelsId}
               />
             ),
           },
@@ -182,6 +179,7 @@ const SentTabWidget = ({
                 wordStart={parseInt(sId[2])}
                 wordEnd={parseInt(sId[3])}
                 type="nissaya"
+                channelsId={channelsId}
               />
             ),
           },

+ 267 - 239
dashboard/src/components/term/TermEdit.tsx

@@ -9,7 +9,16 @@ import {
 
 import LangSelect from "../general/LangSelect";
 import ChannelSelect from "../channel/ChannelSelect";
-import { AutoComplete, Form, Input, message, Space, Tag } from "antd";
+import {
+  Alert,
+  AutoComplete,
+  Button,
+  Form,
+  Input,
+  message,
+  Space,
+  Tag,
+} from "antd";
 import { useEffect, useRef, useState } from "react";
 import {
   ITermCreateResponse,
@@ -60,6 +69,7 @@ const TermEditWidget = ({
 }: IWidget) => {
   const intl = useIntl();
   const [meaningOptions, setMeaningOptions] = useState<ValueType[]>([]);
+  const [readonly, setReadonly] = useState(false);
   //console.log("word", id, word, channelId, studioName);
 
   const [form] = Form.useForm<ITerm>();
@@ -98,261 +108,279 @@ const TermEditWidget = ({
     }
   }, [word]);
   return (
-    <ProForm<ITerm>
-      form={form}
-      formRef={formRef}
-      autoFocusFirstInput={true}
-      onFinish={async (values: ITerm) => {
-        console.log("term submit", values);
-        if (
-          typeof values.word === "undefined" ||
-          typeof values.meaning === "undefined"
-        ) {
-          return;
-        }
-        const newValue = {
-          id: values.id,
-          word: values.word,
-          tag: values.tag,
-          meaning: values.meaning,
-          other_meaning: values.meaning2?.join(),
-          note: values.note,
-          channel: values.channel
-            ? values.channel[values.channel.length - 1]
+    <>
+      {readonly ? (
+        <Alert
+          message="该资源为只读,如果需要修改,请联络拥有者分配权限。"
+          type="warning"
+          closable
+          action={
+            <Button disabled size="small" type="text">
+              详情
+            </Button>
+          }
+        />
+      ) : undefined}
+      <ProForm<ITerm>
+        form={form}
+        formRef={formRef}
+        autoFocusFirstInput={true}
+        onFinish={async (values: ITerm) => {
+          console.log("term submit", values);
+          if (
+            typeof values.word === "undefined" ||
+            typeof values.meaning === "undefined"
+          ) {
+            return;
+          }
+          const newValue = {
+            id: values.id,
+            word: values.word,
+            tag: values.tag,
+            meaning: values.meaning,
+            other_meaning: values.meaning2?.join(),
+            note: values.note,
+            channel: values.channel
               ? values.channel[values.channel.length - 1]
-              : undefined
-            : undefined,
-          studioName: studioName,
-          studioId: parentStudioId,
-          language: values.lang,
-          copy: values.copy,
-        };
-        console.log("value", newValue);
-        let res: ITermResponse;
-        if (typeof values.id === "undefined") {
-          res = await post<ITermDataRequest, ITermResponse>(
-            `/v2/terms`,
-            newValue
-          );
-        } else {
-          res = await put<ITermDataRequest, ITermResponse>(
-            `/v2/terms/${values.id}`,
-            newValue
-          );
-        }
-
-        if (res.ok) {
-          message.success("提交成功");
-          if (typeof onUpdate !== "undefined") {
-            onUpdate(res.data);
+                ? values.channel[values.channel.length - 1]
+                : undefined
+              : undefined,
+            studioName: studioName,
+            studioId: parentStudioId,
+            language: values.lang,
+            copy: values.copy,
+          };
+          console.log("value", newValue);
+          let res: ITermResponse;
+          if (typeof values.id === "undefined") {
+            res = await post<ITermDataRequest, ITermResponse>(
+              `/v2/terms`,
+              newValue
+            );
+          } else {
+            res = await put<ITermDataRequest, ITermResponse>(
+              `/v2/terms/${values.id}`,
+              newValue
+            );
           }
-        } else {
-          message.error(res.message);
-        }
 
-        return true;
-      }}
-      request={async () => {
-        let url: string;
-        let data: ITerm = {
-          word: word ? word : "",
-          tag: "",
-          meaning: "",
-          meaning2: [],
-          note: "",
-          lang: "",
-          channel: [],
-        };
-        if (typeof id !== "undefined") {
-          // 如果是编辑,就从服务器拉取数据。
-          url = "/v2/terms/" + id;
-          console.log("有id", url);
-          const res = await get<ITermResponse>(url);
-          console.log("request", res);
-          let meaning2: string[] = [];
-          if (res.data.other_meaning) {
-            meaning2 = res.data.other_meaning.split(",");
+          if (res.ok) {
+            message.success("提交成功");
+            if (typeof onUpdate !== "undefined") {
+              onUpdate(res.data);
+            }
+          } else {
+            message.error(res.message);
           }
 
-          data = {
-            id: res.data.guid,
-            word: res.data.word,
-            tag: res.data.tag,
-            meaning: res.data.meaning,
-            meaning2: meaning2,
-            note: res.data.note ? res.data.note : "",
-            lang: res.data.language,
-            channel: res.data.channel
-              ? [res.data.studio.id, res.data.channel?.id]
-              : undefined,
-          };
-        } else if (typeof channelId !== "undefined") {
-          //在channel新建
-          url = `/v2/terms?view=create-by-channel&channel=${channelId}&word=${word}`;
-          console.log("在channel新建", url);
-          const res = await get<ITermCreateResponse>(url);
-          console.log(res);
-          data = {
+          return true;
+        }}
+        request={async () => {
+          let url: string;
+          let data: ITerm = {
             word: word ? word : "",
             tag: "",
             meaning: "",
             meaning2: [],
             note: "",
-            lang: res.data.language,
-            channel: [res.data.studio.id, channelId],
+            lang: "",
+            channel: [],
           };
-        } else if (typeof studioName !== "undefined") {
-          //在studio新建
-          url = `/v2/terms?view=create-by-studio&studio=${studioName}&word=${word}`;
-          console.log("在 studio 新建", url);
-        }
+          if (typeof id !== "undefined") {
+            // 如果是编辑,就从服务器拉取数据。
+            url = "/v2/terms/" + id;
+            console.log("有id", url);
+            const res = await get<ITermResponse>(url);
+            if (res.ok) {
+              let meaning2: string[] = [];
+              if (res.data.other_meaning) {
+                meaning2 = res.data.other_meaning.split(",");
+              }
+
+              data = {
+                id: res.data.guid,
+                word: res.data.word,
+                tag: res.data.tag,
+                meaning: res.data.meaning,
+                meaning2: meaning2,
+                note: res.data.note ? res.data.note : "",
+                lang: res.data.language,
+                channel: res.data.channel
+                  ? [res.data.studio.id, res.data.channel?.id]
+                  : undefined,
+              };
+              if (res.data.role === "reader" || res.data.role === "unknown") {
+                setReadonly(true);
+              }
+            }
+          } else if (typeof channelId !== "undefined") {
+            //在channel新建
+            url = `/v2/terms?view=create-by-channel&channel=${channelId}&word=${word}`;
+            console.log("在channel新建", url);
+            const res = await get<ITermCreateResponse>(url);
+            console.log(res);
+            data = {
+              word: word ? word : "",
+              tag: "",
+              meaning: "",
+              meaning2: [],
+              note: "",
+              lang: res.data.language,
+              channel: [res.data.studio.id, channelId],
+            };
+          } else if (typeof studioName !== "undefined") {
+            //在studio新建
+            url = `/v2/terms?view=create-by-studio&studio=${studioName}&word=${word}`;
+            console.log("在 studio 新建", url);
+          }
 
-        return data;
-      }}
-    >
-      <ProForm.Group>
-        <ProFormText width="md" name="id" hidden />
+          return data;
+        }}
+      >
+        <ProForm.Group>
+          <ProFormText width="md" name="id" hidden />
 
-        <ProFormText
-          width="md"
-          name="word"
-          initialValue={word}
-          required
-          label={intl.formatMessage({
-            id: "term.fields.word.label",
-          })}
-          rules={[
-            {
-              required: true,
-            },
-          ]}
-          fieldProps={{
-            showCount: true,
-            maxLength: 128,
-          }}
-        />
-        <ProFormText
-          width="md"
-          name="tag"
-          tooltip={intl.formatMessage({
-            id: "term.fields.description.tooltip",
-          })}
-          label={intl.formatMessage({
-            id: "term.fields.description.label",
-          })}
-        />
-      </ProForm.Group>
-      <ProForm.Group>
-        <Form.Item
-          name="meaning"
-          label={intl.formatMessage({
-            id: "term.fields.meaning.label",
-          })}
-          rules={[
-            {
-              required: true,
-            },
-          ]}
-        >
-          <AutoComplete
-            options={meaningOptions}
-            onChange={(value: any) => {}}
-            maxLength={128}
+          <ProFormText
+            width="md"
+            name="word"
+            initialValue={word}
+            required
+            label={intl.formatMessage({
+              id: "term.fields.word.label",
+            })}
+            rules={[
+              {
+                required: true,
+              },
+            ]}
+            fieldProps={{
+              showCount: true,
+              maxLength: 128,
+            }}
+          />
+          <ProFormText
+            width="md"
+            name="tag"
+            tooltip={intl.formatMessage({
+              id: "term.fields.description.tooltip",
+            })}
+            label={intl.formatMessage({
+              id: "term.fields.description.label",
+            })}
+          />
+        </ProForm.Group>
+        <ProForm.Group>
+          <Form.Item
+            name="meaning"
+            label={intl.formatMessage({
+              id: "term.fields.meaning.label",
+            })}
+            rules={[
+              {
+                required: true,
+              },
+            ]}
           >
-            <Input allowClear showCount={true} />
-          </AutoComplete>
-        </Form.Item>
+            <AutoComplete
+              options={meaningOptions}
+              onChange={(value: any) => {}}
+              maxLength={128}
+            >
+              <Input allowClear showCount={true} />
+            </AutoComplete>
+          </Form.Item>
 
-        <ProFormSelect
-          width="md"
-          name="meaning2"
-          label={intl.formatMessage({
-            id: "term.fields.meaning2.label",
-          })}
-          fieldProps={{
-            mode: "tags",
-            tokenSeparators: [",", ","],
-          }}
-          placeholder="Please select other meanings"
-          rules={[
-            {
-              type: "array",
-            },
-          ]}
-        />
-      </ProForm.Group>
-      <ProForm.Group>
-        <ChannelSelect
-          channelId={channelId}
-          parentChannelId={parentChannelId}
-          parentStudioId={parentStudioId}
-          width="md"
-          name="channel"
-          placeholder="通用于此Studio"
-          tooltip={intl.formatMessage({
-            id: "term.fields.channel.tooltip",
-          })}
-          label={intl.formatMessage({
-            id: "term.fields.channel.label",
-          })}
-        />
-        <ProFormDependency name={["channel"]}>
-          {({ channel }) => {
-            const hasChannel = channel
-              ? channel.length === 0 || channel[0] === ""
-                ? false
-                : true
-              : false;
-            let noChange = true;
-            if (!channel || channel.length === 0 || channel[0] === "") {
-              if (!channelId || channelId === null || channelId === "") {
-                noChange = true;
-              } else {
-                noChange = false;
-              }
-            } else {
-              if (channel[0] === channelId) {
-                noChange = true;
+          <ProFormSelect
+            width="md"
+            name="meaning2"
+            label={intl.formatMessage({
+              id: "term.fields.meaning2.label",
+            })}
+            fieldProps={{
+              mode: "tags",
+              tokenSeparators: [",", ","],
+            }}
+            placeholder="Please select other meanings"
+            rules={[
+              {
+                type: "array",
+              },
+            ]}
+          />
+        </ProForm.Group>
+        <ProForm.Group>
+          <ChannelSelect
+            channelId={channelId}
+            parentChannelId={parentChannelId}
+            parentStudioId={parentStudioId}
+            width="md"
+            name="channel"
+            placeholder="通用于此Studio"
+            tooltip={intl.formatMessage({
+              id: "term.fields.channel.tooltip",
+            })}
+            label={intl.formatMessage({
+              id: "term.fields.channel.label",
+            })}
+          />
+          <ProFormDependency name={["channel"]}>
+            {({ channel }) => {
+              const hasChannel = channel
+                ? channel.length === 0 || channel[0] === ""
+                  ? false
+                  : true
+                : false;
+              let noChange = true;
+              if (!channel || channel.length === 0 || channel[0] === "") {
+                if (!channelId || channelId === null || channelId === "") {
+                  noChange = true;
+                } else {
+                  noChange = false;
+                }
               } else {
-                noChange = false;
+                if (channel[0] === channelId) {
+                  noChange = true;
+                } else {
+                  noChange = false;
+                }
               }
-            }
-            return (
-              <Space>
-                <LangSelect disabled={hasChannel} required={!hasChannel} />
-                <ProFormSelect
-                  initialValue={"move"}
-                  name="copy"
-                  allowClear={false}
-                  label=" "
-                  hidden={!id || noChange}
-                  placeholder="Please select other meanings"
-                  options={[
-                    {
-                      value: "move",
-                      label: "move",
-                    },
-                    {
-                      value: "copy",
-                      label: "copy",
-                    },
-                  ]}
-                />
-              </Space>
-            );
-          }}
-        </ProFormDependency>
-      </ProForm.Group>
-      <ProForm.Group>
-        <Form.Item
-          style={{ width: "100%" }}
-          name="note"
-          label={intl.formatMessage({ id: "forms.fields.note.label" })}
-        >
-          <MDEditor />
-        </Form.Item>
-      </ProForm.Group>
-    </ProForm>
+              return (
+                <Space>
+                  <LangSelect disabled={hasChannel} required={!hasChannel} />
+                  <ProFormSelect
+                    initialValue={"move"}
+                    name="copy"
+                    allowClear={false}
+                    label=" "
+                    hidden={!id || noChange}
+                    placeholder="Please select other meanings"
+                    options={[
+                      {
+                        value: "move",
+                        label: "move",
+                      },
+                      {
+                        value: "copy",
+                        label: "copy",
+                      },
+                    ]}
+                  />
+                </Space>
+              );
+            }}
+          </ProFormDependency>
+        </ProForm.Group>
+        <ProForm.Group>
+          <Form.Item
+            style={{ width: "100%" }}
+            name="note"
+            label={intl.formatMessage({ id: "forms.fields.note.label" })}
+          >
+            <MDEditor />
+          </Form.Item>
+        </ProForm.Group>
+      </ProForm>
+    </>
   );
 };