Przeglądaj źródła

Merge pull request #1306 from visuddhinanda/agile

添加 article edit 按钮
visuddhinanda 2 lat temu
rodzic
commit
33631059f7

+ 1 - 0
dashboard/src/components/api/Corpus.ts

@@ -151,6 +151,7 @@ export interface ISentenceRequest {
   prEditor?: string;
   prEditor?: string;
   prId?: string;
   prId?: string;
   prEditAt?: string;
   prEditAt?: string;
+  channels?: string;
 }
 }
 
 
 export interface ISentenceData {
 export interface ISentenceData {

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

@@ -13,6 +13,7 @@ export interface ITermDataRequest {
   studioName?: string;
   studioName?: string;
   studioId?: string;
   studioId?: string;
   language?: string;
   language?: string;
+  copy?: string;
 }
 }
 export interface ITermDataResponse {
 export interface ITermDataResponse {
   id: number;
   id: number;

+ 47 - 9
dashboard/src/components/channel/ChannelSelect.tsx

@@ -1,13 +1,17 @@
 import { ProFormCascader } from "@ant-design/pro-components";
 import { ProFormCascader } from "@ant-design/pro-components";
 import { message } from "antd";
 import { message } from "antd";
+import { useAppSelector } from "../../hooks";
+import { currentUser } from "../../reducers/current-user";
 
 
 import { get } from "../../request";
 import { get } from "../../request";
 import { IApiResponseChannelList } from "../api/Channel";
 import { IApiResponseChannelList } from "../api/Channel";
+import { IStudio } from "../auth/StudioName";
 
 
 interface IOption {
 interface IOption {
   value: string;
   value: string;
-  label: string;
+  label?: string;
   lang?: string;
   lang?: string;
+  children?: IOption[];
 }
 }
 
 
 interface IWidget {
 interface IWidget {
@@ -32,6 +36,7 @@ const ChannelSelectWidget = ({
   placeholder,
   placeholder,
   onSelect,
   onSelect,
 }: IWidget) => {
 }: IWidget) => {
+  const user = useAppSelector(currentUser);
   return (
   return (
     <ProFormCascader
     <ProFormCascader
       width={width}
       width={width}
@@ -53,23 +58,56 @@ const ChannelSelectWidget = ({
               iterator.studio.nickName ? iterator.studio.nickName : ""
               iterator.studio.nickName ? iterator.studio.nickName : ""
             );
             );
           }
           }
-          let channels: IOption[] = [{ value: "", label: "通用于此Studio" }];
+          let channels: IOption[] = [];
+          console.log("parentStudioId", parentStudioId);
+          if (user && user.id === parentStudioId) {
+            channels.push({ value: "", label: "通用于此Studio" });
+          }
           if (typeof parentChannelId === "string") {
           if (typeof parentChannelId === "string") {
             channels.push({ value: parentChannelId, label: "仅此版本" });
             channels.push({ value: parentChannelId, label: "仅此版本" });
           }
           }
-          studio.forEach((value, key, map) => {
-            const node = {
-              value: key,
-              label: value,
+
+          if (user) {
+            //自己的 studio
+            channels.push({
+              value: user.id,
+              label: user.realName,
               children: json.data.rows
               children: json.data.rows
-                .filter((value) => value.studio.id === key)
+                .filter((value) => value.studio.id === user.id)
                 .map((item) => {
                 .map((item) => {
                   return { value: item.uid, label: item.name, lang: item.lang };
                   return { value: item.uid, label: item.name, lang: item.lang };
                 }),
                 }),
-            };
-            channels.push(node);
+            });
+          }
+
+          let arrStudio: IStudio[] = [];
+          studio.forEach((value, key, map) => {
+            arrStudio.push({ id: key, nickName: value });
           });
           });
 
 
+          const others: IOption[] = arrStudio
+            .sort((a, b) =>
+              a.nickName && b.nickName ? (a.nickName > b.nickName ? 1 : -1) : 0
+            )
+            .filter((value) => value.id !== user?.id)
+            .map((item) => {
+              const node = {
+                value: item.id,
+                label: item.nickName,
+                children: json.data.rows
+                  .filter((value) => value.studio.id === item.id)
+                  .map((item) => {
+                    return {
+                      value: item.uid,
+                      label: item.name,
+                      lang: item.lang,
+                    };
+                  }),
+              };
+              return node;
+            });
+          channels = [...channels, ...others];
+
           console.log("json", channels);
           console.log("json", channels);
           return channels;
           return channels;
         } else {
         } else {

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

@@ -23,7 +23,7 @@ interface IWidgetTocPath {
 }
 }
 const TocPathWidget = ({
 const TocPathWidget = ({
   data = [],
   data = [],
-  trigger,
+  trigger = "toc",
   link = "self",
   link = "self",
   channel,
   channel,
   onChange,
   onChange,

+ 31 - 13
dashboard/src/components/dict/UserDictList.tsx

@@ -1,6 +1,4 @@
-import { useParams } from "react-router-dom";
 import { useIntl } from "react-intl";
 import { useIntl } from "react-intl";
-
 import {
 import {
   Button,
   Button,
   Space,
   Space,
@@ -28,6 +26,8 @@ import { delete_2, get } from "../../request";
 import { useRef, useState } from "react";
 import { useRef, useState } from "react";
 import DictEdit from "../../components/dict/DictEdit";
 import DictEdit from "../../components/dict/DictEdit";
 import { IDeleteResponse } from "../../components/api/Article";
 import { IDeleteResponse } from "../../components/api/Article";
+import { getSorterUrl } from "../../pages/admin/relation/list";
+import TimeShow from "../general/TimeShow";
 
 
 const { Link } = Typography;
 const { Link } = Typography;
 
 
@@ -42,9 +42,14 @@ export interface IWord {
   note: string;
   note: string;
   factors: string;
   factors: string;
   dict?: IDictInfo;
   dict?: IDictInfo;
-  createdAt: number;
+  updated_at?: string;
+  created_at?: string;
+}
+interface IParams {
+  word?: string;
+  parent?: string;
+  dict?: string;
 }
 }
-
 interface IWidget {
 interface IWidget {
   studioName?: string;
   studioName?: string;
   view?: "studio" | "all";
   view?: "studio" | "all";
@@ -100,7 +105,7 @@ const UserDictListWidget = ({ studioName, view = "studio" }: IWidget) => {
 
 
   return (
   return (
     <>
     <>
-      <ProTable<IWord>
+      <ProTable<IWord, IParams>
         actionRef={ref}
         actionRef={ref}
         columns={[
         columns={[
           {
           {
@@ -161,6 +166,7 @@ const UserDictListWidget = ({ studioName, view = "studio" }: IWidget) => {
             key: "meaning",
             key: "meaning",
             tip: "意思过长会自动收缩",
             tip: "意思过长会自动收缩",
             ellipsis: true,
             ellipsis: true,
+            search: false,
           },
           },
           {
           {
             title: intl.formatMessage({
             title: intl.formatMessage({
@@ -187,21 +193,24 @@ const UserDictListWidget = ({ studioName, view = "studio" }: IWidget) => {
             dataIndex: "dict",
             dataIndex: "dict",
             key: "dict",
             key: "dict",
             hideInTable: view !== "all",
             hideInTable: view !== "all",
-            search: false,
+            search: view !== "all" ? false : undefined,
             render: (text, row, index, action) => {
             render: (text, row, index, action) => {
               return row.dict?.shortname;
               return row.dict?.shortname;
             },
             },
           },
           },
           {
           {
             title: intl.formatMessage({
             title: intl.formatMessage({
-              id: "forms.fields.created-at.label",
+              id: "forms.fields.updated-at.label",
             }),
             }),
-            key: "created-at",
+            key: "updated_at",
             width: 200,
             width: 200,
             search: false,
             search: false,
-            dataIndex: "createdAt",
+            dataIndex: "updated_at",
             valueType: "date",
             valueType: "date",
-            sorter: (a, b) => a.createdAt - b.createdAt,
+            sorter: true,
+            render: (text, row, index, action) => {
+              return <TimeShow time={row.updated_at} showIcon={false} />;
+            },
           },
           },
           {
           {
             title: intl.formatMessage({ id: "buttons.option" }),
             title: intl.formatMessage({ id: "buttons.option" }),
@@ -323,11 +332,18 @@ const UserDictListWidget = ({ studioName, view = "studio" }: IWidget) => {
               break;
               break;
           }
           }
           url += `&limit=${params.pageSize}&offset=${offset}`;
           url += `&limit=${params.pageSize}&offset=${offset}`;
+
           url += params.keyword ? "&search=" + params.keyword : "";
           url += params.keyword ? "&search=" + params.keyword : "";
+
+          url += params.word ? `&word=${params.word}` : "";
+          url += params.parent ? `&parent=${params.parent}` : "";
+          url += params.dict ? `&dict=${params.dict}` : "";
+
+          url += getSorterUrl(sorter);
+
           console.log(url);
           console.log(url);
           const res = await get<IApiResponseDictList>(url);
           const res = await get<IApiResponseDictList>(url);
           const items: IWord[] = res.data.rows.map((item, id) => {
           const items: IWord[] = res.data.rows.map((item, id) => {
-            const date = new Date(item.updated_at);
             const id2 =
             const id2 =
               ((params.current || 1) - 1) * (params.pageSize || 20) + id + 1;
               ((params.current || 1) - 1) * (params.pageSize || 20) + id + 1;
             return {
             return {
@@ -341,7 +357,7 @@ const UserDictListWidget = ({ studioName, view = "studio" }: IWidget) => {
               note: item.note,
               note: item.note,
               factors: item.factors,
               factors: item.factors,
               dict: item.dict,
               dict: item.dict,
-              createdAt: date.getTime(),
+              updated_at: item.updated_at,
             };
             };
           });
           });
           return {
           return {
@@ -356,7 +372,9 @@ const UserDictListWidget = ({ studioName, view = "studio" }: IWidget) => {
           showQuickJumper: true,
           showQuickJumper: true,
           showSizeChanger: true,
           showSizeChanger: true,
         }}
         }}
-        search={false}
+        search={{
+          filterType: "light",
+        }}
         options={{
         options={{
           search: true,
           search: true,
         }}
         }}

+ 8 - 2
dashboard/src/components/general/LangSelect.tsx

@@ -27,8 +27,14 @@ interface IWidget {
   width?: number | "md" | "sm" | "xl" | "xs" | "lg";
   width?: number | "md" | "sm" | "xl" | "xs" | "lg";
   label?: string;
   label?: string;
   disabled?: boolean;
   disabled?: boolean;
+  required?: boolean;
 }
 }
-const LangSelectWidget = ({ width, label, disabled = false }: IWidget) => {
+const LangSelectWidget = ({
+  width,
+  label,
+  disabled = false,
+  required = true,
+}: IWidget) => {
   const intl = useIntl();
   const intl = useIntl();
 
 
   const langOptions = [
   const langOptions = [
@@ -63,7 +69,7 @@ const LangSelectWidget = ({ width, label, disabled = false }: IWidget) => {
       }
       }
       rules={[
       rules={[
         {
         {
-          required: true,
+          required: required,
           message: intl.formatMessage({
           message: intl.formatMessage({
             id: "forms.message.lang.required",
             id: "forms.message.lang.required",
           }),
           }),

+ 2 - 1
dashboard/src/components/general/NissayaCard.tsx

@@ -1,5 +1,5 @@
 import { useEffect, useState } from "react";
 import { useEffect, useState } from "react";
-import { Modal, Popover, Skeleton, Space, Typography } from "antd";
+import { Modal, Popover, Skeleton, Typography } from "antd";
 
 
 import { get } from "../../request";
 import { get } from "../../request";
 import { get as getLang } from "../../locales";
 import { get as getLang } from "../../locales";
@@ -17,6 +17,7 @@ interface INissayaCardModal {
 export const NissayaCardPop = ({ text, trigger }: INissayaCardModal) => {
 export const NissayaCardPop = ({ text, trigger }: INissayaCardModal) => {
   return (
   return (
     <Popover
     <Popover
+      style={{ width: 600 }}
       content={<NissayaCardWidget text={text} cache={true} />}
       content={<NissayaCardWidget text={text} cache={true} />}
       placement="bottom"
       placement="bottom"
     >
     >

+ 2 - 1
dashboard/src/components/general/NissayaCardTable.tsx

@@ -2,6 +2,7 @@ import { Button, Space, Table, Tag } from "antd";
 import lodash from "lodash";
 import lodash from "lodash";
 import { useEffect, useState } from "react";
 import { useEffect, useState } from "react";
 import { ArrowRightOutlined } from "@ant-design/icons";
 import { ArrowRightOutlined } from "@ant-design/icons";
+import Marked from "./Marked";
 
 
 const randomString = () =>
 const randomString = () =>
   lodash.times(20, () => lodash.random(35).toString(36)).join("");
   lodash.times(20, () => lodash.random(35).toString(36)).join("");
@@ -186,7 +187,7 @@ const NissayaCardTableWidget = ({ data }: IWidget) => {
             if (record.isChildren) {
             if (record.isChildren) {
               return undefined;
               return undefined;
             } else {
             } else {
-              return <>{record.category?.note}</>;
+              return <Marked text={record.category?.note} />;
             }
             }
           },
           },
         },
         },

+ 3 - 3
dashboard/src/components/general/style.css

@@ -13,7 +13,7 @@
   z-index: 1;
   z-index: 1;
   resize: vertical;
   resize: vertical;
   color: var(--main-color);
   color: var(--main-color);
-  background-color: var(--drop-bg-color);
+  background-color: #e4e45c52;
   border: unset;
   border: unset;
   border-radius: 8px;
   border-radius: 8px;
   padding: 5px;
   padding: 5px;
@@ -29,14 +29,14 @@
   border-left: 1px solid #000;
   border-left: 1px solid #000;
 }
 }
 .text_input > .menu {
 .text_input > .menu {
-  background: linear-gradient(45deg, black, transparent);
+  background-color: wheat;
   width: 200px;
   width: 200px;
   height: 300px;
   height: 300px;
   box-shadow: #000;
   box-shadow: #000;
   position: absolute;
   position: absolute;
   display: none;
   display: none;
   z-index: 100;
   z-index: 100;
-  box-shadow: 0 5px 7px rgb(0 0 0 / 15%);
+  box-shadow: 0 5px 7px rgb(0 0 0 / 25%);
 }
 }
 .text_input > .menu ul {
 .text_input > .menu ul {
   list-style-type: none;
   list-style-type: none;

+ 1 - 0
dashboard/src/components/share/ShareModal.tsx

@@ -31,6 +31,7 @@ const ShareModalWidget = ({ resId, resType, trigger }: IWidget) => {
         open={isModalOpen}
         open={isModalOpen}
         onOk={handleOk}
         onOk={handleOk}
         onCancel={handleCancel}
         onCancel={handleCancel}
+        footer={false}
       >
       >
         <Share resId={resId} resType={resType} />
         <Share resId={resId} resType={resType} />
       </Modal>
       </Modal>

+ 1 - 2
dashboard/src/components/template/Nissaya/NissayaMeaning.tsx

@@ -1,5 +1,4 @@
-import { Space } from "antd";
-import React, { useEffect, useState } from "react";
+import { useEffect, useState } from "react";
 import { useAppSelector } from "../../../hooks";
 import { useAppSelector } from "../../../hooks";
 import { getEnding } from "../../../reducers/nissaya-ending-vocabulary";
 import { getEnding } from "../../../reducers/nissaya-ending-vocabulary";
 import Lookup from "../../dict/Lookup";
 import Lookup from "../../dict/Lookup";

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

@@ -31,6 +31,7 @@ export interface ISentence {
   updateAt: string;
   updateAt: string;
   suggestionCount?: ISuggestionCount;
   suggestionCount?: ISuggestionCount;
   openInEditMode?: boolean;
   openInEditMode?: boolean;
+  translationChannels?: string[];
 }
 }
 export interface ISentenceId {
 export interface ISentenceId {
   book: number;
   book: number;

+ 6 - 4
dashboard/src/components/template/SentEdit/SentCanRead.tsx

@@ -35,12 +35,11 @@ const SentCanReadWidget = ({
 }: IWidget) => {
 }: IWidget) => {
   const [sentData, setSentData] = useState<ISentence[]>([]);
   const [sentData, setSentData] = useState<ISentence[]>([]);
   const [channels, setChannels] = useState<string[]>();
   const [channels, setChannels] = useState<string[]>();
-
   const user = useAppSelector(_currentUser);
   const user = useAppSelector(_currentUser);
 
 
   const load = () => {
   const load = () => {
     const sentId = `${book}-${para}-${wordStart}-${wordEnd}`;
     const sentId = `${book}-${para}-${wordStart}-${wordEnd}`;
-    let url = `/v2/sentence?view=sent-can-read&sentence=${sentId}&type=${type}&mode=edit`;
+    let url = `/v2/sentence?view=sent-can-read&sentence=${sentId}&type=${type}&mode=edit&html=true`;
     url += channelsId ? `&channels=${channelsId.join()}` : "";
     url += channelsId ? `&channels=${channelsId.join()}` : "";
     console.log("url", url);
     console.log("url", url);
     get<ISentenceListResponse>(url)
     get<ISentenceListResponse>(url)
@@ -64,9 +63,11 @@ const SentCanReadWidget = ({
               studio: item.studio,
               studio: item.studio,
               channel: item.channel,
               channel: item.channel,
               suggestionCount: item.suggestionCount,
               suggestionCount: item.suggestionCount,
+              translationChannels: channelsId,
               updateAt: item.updated_at,
               updateAt: item.updated_at,
             };
             };
           });
           });
+          console.log("new data", newData);
           setSentData(newData);
           setSentData(newData);
         } else {
         } else {
           message.error(json.message);
           message.error(json.message);
@@ -90,7 +91,7 @@ const SentCanReadWidget = ({
   }, [reload]);
   }, [reload]);
 
 
   return (
   return (
-    <>
+    <div style={{ backgroundColor: "#8080801f" }}>
       <div style={{ display: "flex", justifyContent: "space-between" }}>
       <div style={{ display: "flex", justifyContent: "space-between" }}>
         <span></span>
         <span></span>
         <Button
         <Button
@@ -120,6 +121,7 @@ const SentCanReadWidget = ({
               userName: user.realName,
               userName: user.realName,
             },
             },
             channel: channel,
             channel: channel,
+            translationChannels: channelsId,
             updateAt: "",
             updateAt: "",
             openInEditMode: true,
             openInEditMode: true,
           };
           };
@@ -139,7 +141,7 @@ const SentCanReadWidget = ({
           />
           />
         );
         );
       })}
       })}
-    </>
+    </div>
   );
   );
 };
 };
 
 

+ 15 - 12
dashboard/src/components/template/SentEdit/SentCellEditable.tsx

@@ -35,7 +35,6 @@ const SentCellEditableWidget = ({
   const [value, setValue] = useState(data.content);
   const [value, setValue] = useState(data.content);
   const [saving, setSaving] = useState<boolean>(false);
   const [saving, setSaving] = useState<boolean>(false);
   const [termList, setTermList] = useState<string[]>();
   const [termList, setTermList] = useState<string[]>();
-
   const sentWords = useAppSelector(wordList);
   const sentWords = useAppSelector(wordList);
 
 
   useEffect(() => {
   useEffect(() => {
@@ -77,17 +76,21 @@ const SentCellEditableWidget = ({
 
 
   const save = () => {
   const save = () => {
     setSaving(true);
     setSaving(true);
-    put<ISentenceRequest, ISentenceResponse>(
-      `/v2/sentence/${data.book}_${data.para}_${data.wordStart}_${data.wordEnd}_${data.channel.id}`,
-      {
-        book: data.book,
-        para: data.para,
-        wordStart: data.wordStart,
-        wordEnd: data.wordEnd,
-        channel: data.channel.id,
-        content: value,
-      }
-    )
+    console.log("on save", data);
+    let url = `/v2/sentence/${data.book}_${data.para}_${data.wordStart}_${data.wordEnd}_${data.channel.id}`;
+    url += "?mode=edit&html=true";
+    console.log("url", url);
+    const body = {
+      book: data.book,
+      para: data.para,
+      wordStart: data.wordStart,
+      wordEnd: data.wordEnd,
+      channel: data.channel.id,
+      content: value,
+      channels: data.translationChannels?.join(),
+    };
+    console.log("body", body);
+    put<ISentenceRequest, ISentenceResponse>(url, body)
       .then((json) => {
       .then((json) => {
         console.log(json);
         console.log(json);
         setSaving(false);
         setSaving(false);

+ 2 - 4
dashboard/src/components/template/SentEdit/SentTab.tsx

@@ -78,9 +78,7 @@ const SentTabWidget = ({
             <TocPath
             <TocPath
               link="none"
               link="none"
               data={mPath}
               data={mPath}
-              trigger={
-                path ? path.length > 0 ? path[0].paliTitle : <></> : <></>
-              }
+              trigger={path ? path.length > 0 ? path[0].title : <></> : <></>}
               onChange={(para: IChapter) => {
               onChange={(para: IChapter) => {
                 //点击章节目录
                 //点击章节目录
                 const type = para.level
                 const type = para.level
@@ -93,7 +91,7 @@ const SentTabWidget = ({
                 );
                 );
               }}
               }}
             />
             />
-            <Text copyable={{ text: sentId[0] }}>{sentId[0]}</Text>
+            <Text copyable={{ text: `{{${sentId[0]}}}` }}>{sentId[0]}</Text>
             <SentMenu
             <SentMenu
               book={book}
               book={book}
               para={para}
               para={para}

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

@@ -112,7 +112,7 @@ const TermCtl = ({
               style={{ maxWidth: 500, minWidth: 300 }}
               style={{ maxWidth: 500, minWidth: 300 }}
               actions={[
               actions={[
                 <Button type="link" size="small" icon={<SearchOutlined />}>
                 <Button type="link" size="small" icon={<SearchOutlined />}>
-                  <Link to={`/term/list/āraññika`} target="_blank">
+                  <Link to={`/term/list/${termData.word}`} target="_blank">
                     详情
                     详情
                   </Link>
                   </Link>
                 </Button>,
                 </Button>,

+ 44 - 6
dashboard/src/components/term/TermEdit.tsx

@@ -37,6 +37,7 @@ export interface ITerm {
   channel?: string[];
   channel?: string[];
   channelId?: string;
   channelId?: string;
   lang?: string;
   lang?: string;
+  copy?: string;
 }
 }
 
 
 interface IWidget {
 interface IWidget {
@@ -116,7 +117,7 @@ const TermEditWidget = ({
           meaning: values.meaning,
           meaning: values.meaning,
           other_meaning: values.meaning2?.join(),
           other_meaning: values.meaning2?.join(),
           note: values.note,
           note: values.note,
-          channal: values.channel
+          channel: values.channel
             ? values.channel[values.channel.length - 1]
             ? values.channel[values.channel.length - 1]
               ? values.channel[values.channel.length - 1]
               ? values.channel[values.channel.length - 1]
               : undefined
               : undefined
@@ -124,6 +125,7 @@ const TermEditWidget = ({
           studioName: studioName,
           studioName: studioName,
           studioId: parentStudioId,
           studioId: parentStudioId,
           language: values.lang,
           language: values.lang,
+          copy: values.copy,
         };
         };
         console.log("value", newValue);
         console.log("value", newValue);
         let res: ITermResponse;
         let res: ITermResponse;
@@ -283,6 +285,7 @@ const TermEditWidget = ({
         <ChannelSelect
         <ChannelSelect
           channelId={channelId}
           channelId={channelId}
           parentChannelId={parentChannelId}
           parentChannelId={parentChannelId}
+          parentStudioId={parentStudioId}
           width="md"
           width="md"
           name="channel"
           name="channel"
           placeholder="通用于此Studio"
           placeholder="通用于此Studio"
@@ -295,12 +298,47 @@ const TermEditWidget = ({
         />
         />
         <ProFormDependency name={["channel"]}>
         <ProFormDependency name={["channel"]}>
           {({ channel }) => {
           {({ channel }) => {
-            console.log("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;
+              } else {
+                noChange = false;
+              }
+            }
             return (
             return (
-              <LangSelect
-                disabled={channel ? (channel[0] === "" ? false : true) : false}
-              />
+              <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>
         </ProFormDependency>

+ 15 - 13
dashboard/src/load.ts

@@ -123,20 +123,22 @@ const init = () => {
   );
   );
 
 
   //获取 relation 表
   //获取 relation 表
-  get<IRelationListResponse>(`/v2/relation?limit=1000`).then((json) => {
-    if (json.ok) {
-      const items: IRelation[] = json.data.rows.map((item, id) => {
-        return {
-          id: item.id,
-          name: item.name,
-          case: item.case,
-          from: item.from,
-          to: item.to,
-        };
-      });
-      store.dispatch(pushRelation(items));
+  get<IRelationListResponse>(`/v2/relation?vocabulary=true&limit=1000`).then(
+    (json) => {
+      if (json.ok) {
+        const items: IRelation[] = json.data.rows.map((item, id) => {
+          return {
+            id: item.id,
+            name: item.name,
+            case: item.case,
+            from: item.from,
+            to: item.to,
+          };
+        });
+        store.dispatch(pushRelation(items));
+      }
     }
     }
-  });
+  );
 
 
   //获取用户选择的主题
   //获取用户选择的主题
   const theme = localStorage.getItem("theme");
   const theme = localStorage.getItem("theme");

+ 3 - 3
dashboard/src/pages/admin/relation/list.tsx

@@ -144,9 +144,6 @@ const Widget = () => {
     <>
     <>
       <ProTable<IRelation, IParams>
       <ProTable<IRelation, IParams>
         actionRef={ref}
         actionRef={ref}
-        search={{
-          filterType: "light",
-        }}
         columns={[
         columns={[
           {
           {
             title: intl.formatMessage({
             title: intl.formatMessage({
@@ -443,6 +440,9 @@ const Widget = () => {
         options={{
         options={{
           search: true,
           search: true,
         }}
         }}
+        search={{
+          filterType: "light",
+        }}
         toolBarRender={() => [
         toolBarRender={() => [
           <DataImport
           <DataImport
             url="/v2/relation-import"
             url="/v2/relation-import"

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

@@ -56,6 +56,8 @@ const Widget = () => {
   const [searchParams, setSearchParams] = useSearchParams();
   const [searchParams, setSearchParams] = useSearchParams();
   const [anchorNavOpen, setAnchorNavOpen] = useState(false);
   const [anchorNavOpen, setAnchorNavOpen] = useState(false);
   const [recentModalOpen, setRecentModalOpen] = useState(false);
   const [recentModalOpen, setRecentModalOpen] = useState(false);
+  const [loadedArticleData, setLoadedArticleData] =
+    useState<IArticleDataResponse>();
 
 
   const paraChange = useAppSelector(paraParam);
   const paraChange = useAppSelector(paraParam);
 
 
@@ -125,7 +127,19 @@ const Widget = () => {
             <NetStatus style={{ color: "white" }} />
             <NetStatus style={{ color: "white" }} />
           </Space>
           </Space>
           <div></div>
           <div></div>
-          <div key="right" style={{ display: "flex" }}>
+          <Space key="right">
+            {type === "article" && loadedArticleData ? (
+              <Button
+                ghost
+                onClick={() => {
+                  navigate(
+                    `/studio/${loadedArticleData.studio?.realName}/article/${loadedArticleData.uid}/edit`
+                  );
+                }}
+              >
+                Edit
+              </Button>
+            ) : undefined}
             <Avatar placement="bottom" />
             <Avatar placement="bottom" />
             <Divider type="vertical" />
             <Divider type="vertical" />
             <ModeSwitch
             <ModeSwitch
@@ -170,7 +184,7 @@ const Widget = () => {
                 setRightPanel((value) => (value === "close" ? "open" : "close"))
                 setRightPanel((value) => (value === "close" ? "open" : "close"))
               }
               }
             />
             />
-          </div>
+          </Space>
         </Header>
         </Header>
       </Affix>
       </Affix>
       <div style={{ width: "100%", display: "flex" }}>
       <div style={{ width: "100%", display: "flex" }}>
@@ -265,7 +279,9 @@ const Widget = () => {
                 });
                 });
                 navigate(url);
                 navigate(url);
               }}
               }}
-              onLoad={(article: IArticleDataResponse) => {}}
+              onLoad={(article: IArticleDataResponse) => {
+                setLoadedArticleData(article);
+              }}
             />
             />
             <Navigate
             <Navigate
               type={type as ArticleType}
               type={type as ArticleType}

+ 2 - 372
dashboard/src/pages/studio/dict/list.tsx

@@ -1,380 +1,10 @@
 import { useParams } from "react-router-dom";
 import { useParams } from "react-router-dom";
-import { useIntl } from "react-intl";
 
 
-import {
-  Button,
-  Space,
-  Table,
-  Dropdown,
-  Drawer,
-  message,
-  Modal,
-  Typography,
-} from "antd";
-import {
-  PlusOutlined,
-  ExclamationCircleOutlined,
-  DeleteOutlined,
-} from "@ant-design/icons";
-import { ActionType, ProTable } from "@ant-design/pro-components";
-
-import DictCreate from "../../../components/dict/DictCreate";
-import {
-  IApiResponseDictList,
-  IUserDictDeleteRequest,
-} from "../../../components/api/Dict";
-import { delete_2, get } from "../../../request";
-import { useRef, useState } from "react";
-import DictEdit from "../../../components/dict/DictEdit";
-import { IDeleteResponse } from "../../../components/api/Article";
-
-const { Link } = Typography;
-
-export interface IWord {
-  sn: number;
-  wordId: string;
-  word: string;
-  type: string;
-  grammar: string;
-  parent: string;
-  meaning: string;
-  note: string;
-  factors: string;
-  createdAt: number;
-}
+import UserDictList from "../../../components/dict/UserDictList";
 
 
 const Widget = () => {
 const Widget = () => {
-  const intl = useIntl();
   const { studioname } = useParams();
   const { studioname } = useParams();
-  const [isEditOpen, setIsEditOpen] = useState(false);
-  const [isCreateOpen, setIsCreateOpen] = useState(false);
-  const [wordId, setWordId] = useState<string>();
-  const [drawerTitle, setDrawerTitle] = useState("New Word");
-
-  const showDeleteConfirm = (id: string[], title: string) => {
-    Modal.confirm({
-      icon: <ExclamationCircleOutlined />,
-      title:
-        intl.formatMessage({
-          id: "message.delete.sure",
-        }) +
-        intl.formatMessage({
-          id: "message.irrevocable",
-        }),
-
-      content: title,
-      okText: intl.formatMessage({
-        id: "buttons.delete",
-      }),
-      okType: "danger",
-      cancelText: intl.formatMessage({
-        id: "buttons.no",
-      }),
-      onOk() {
-        console.log("delete", id);
-        return delete_2<IUserDictDeleteRequest, IDeleteResponse>(
-          `/v2/userdict/${id}`,
-          {
-            id: JSON.stringify(id),
-          }
-        )
-          .then((json) => {
-            if (json.ok) {
-              message.success("删除成功");
-              ref.current?.reload();
-            } else {
-              message.error(json.message);
-            }
-          })
-          .catch((e) => console.log("Oops errors!", e));
-      },
-    });
-  };
-
-  const ref = useRef<ActionType>();
-
-  return (
-    <>
-      <ProTable<IWord>
-        actionRef={ref}
-        columns={[
-          {
-            title: intl.formatMessage({
-              id: "dict.fields.sn.label",
-            }),
-            dataIndex: "sn",
-            key: "sn",
-            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"
-                  menu={{
-                    items: [
-                      {
-                        key: "remove",
-                        label: intl.formatMessage({
-                          id: "buttons.delete",
-                        }),
-                        icon: <DeleteOutlined />,
-                        danger: true,
-                      },
-                    ],
-                    onClick: (e) => {
-                      switch (e.key) {
-                        case "share":
-                          break;
-                        case "remove":
-                          showDeleteConfirm([row.wordId], row.word);
-                          break;
-                        default:
-                          break;
-                      }
-                    },
-                  }}
-                >
-                  <Link
-                    onClick={() => {
-                      setWordId(row.wordId);
-                      setDrawerTitle(row.word);
-                      setIsEditOpen(true);
-                    }}
-                  >
-                    {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={({
-          intl,
-          selectedRowKeys,
-          selectedRows,
-          onCleanSelected,
-        }) => {
-          return (
-            <Space size={16}>
-              <Button
-                type="link"
-                onClick={() => {
-                  console.log(selectedRowKeys);
-                  showDeleteConfirm(
-                    selectedRowKeys.map((item) => item.toString()),
-                    selectedRowKeys.length + "个单词"
-                  );
-                  onCleanSelected();
-                }}
-              >
-                批量删除
-              </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}`;
-          url += params.keyword ? "&search=" + params.keyword : "";
-          console.log(url);
-          const res = await get<IApiResponseDictList>(url);
-          const items: IWord[] = 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 {
-              sn: 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="wordId"
-        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);
-            }}
-            disabled={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>
-    </>
-  );
+  return <UserDictList studioName={studioname} />;
 };
 };
 
 
 export default Widget;
 export default Widget;