Browse Source

Merge pull request #1276 from visuddhinanda/agile

后台管理添加dict
visuddhinanda 2 years ago
parent
commit
524a3827bd

+ 5 - 0
dashboard/src/Router.tsx

@@ -22,6 +22,8 @@ import AdminRelation from "./pages/admin/relation";
 import AdminRelationList from "./pages/admin/relation/list";
 import AdminNissayaEnding from "./pages/admin/nissaya-ending";
 import AdminNissayaEndingList from "./pages/admin/nissaya-ending/list";
+import AdminDictionary from "./pages/admin/dictionary";
+import AdminDictionaryList from "./pages/admin/dictionary/list";
 import AdminApi from "./pages/admin/api";
 import AdminApiDashboard from "./pages/admin/api/dashboard";
 
@@ -136,6 +138,9 @@ const Widget = () => {
             <Route path="list" element={<AdminNissayaEndingList />} />
             <Route path="list/:relation" element={<AdminNissayaEndingList />} />
           </Route>
+          <Route path="dictionary" element={<AdminDictionary />}>
+            <Route path="list" element={<AdminDictionaryList />} />
+          </Route>
         </Route>
         <Route path="anonymous" element={<Anonymous />}>
           <Route path="users">

+ 5 - 1
dashboard/src/components/admin/LeftSider.tsx

@@ -36,9 +36,13 @@ const LeftSiderWidget = ({ selectedKeys = "" }: IWidgetHeadBar) => {
           key: "relation",
         },
         {
-          label: <Link to="/admin/nissaya-ending/list">nissaya-ending</Link>,
+          label: <Link to="/admin/nissaya-ending/list">Nissaya Ending</Link>,
           key: "nissaya-ending",
         },
+        {
+          label: <Link to="/admin/dictionary/list">Dictionary</Link>,
+          key: "dict",
+        },
       ],
     },
     {

+ 21 - 3
dashboard/src/components/article/Navigate.tsx

@@ -5,7 +5,21 @@ import { DoubleRightOutlined, DoubleLeftOutlined } from "@ant-design/icons";
 import { get } from "../../request";
 import { ArticleType } from "./Article";
 
-const { Paragraph } = Typography;
+const { Paragraph, Text } = Typography;
+
+const EllipsisMiddle: React.FC<{
+  suffixCount: number;
+  maxWidth: number;
+  children?: string;
+}> = ({ suffixCount, maxWidth = 500, children = "" }) => {
+  const start = children.slice(0, children.length - suffixCount).trim();
+  const suffix = children.slice(-suffixCount).trim();
+  return (
+    <Text style={{ maxWidth: maxWidth }} ellipsis={{ suffix }}>
+      {start}
+    </Text>
+  );
+};
 
 interface INavButton {
   title: string;
@@ -55,7 +69,9 @@ const NavigateWidget = ({ type, articleId, onChange }: IWidget) => {
       >
         <Space>
           <DoubleLeftOutlined />
-          {prev?.title}
+          <EllipsisMiddle maxWidth={300} suffixCount={7}>
+            {prev?.title}
+          </EllipsisMiddle>
         </Space>
       </Button>
       <Button
@@ -68,7 +84,9 @@ const NavigateWidget = ({ type, articleId, onChange }: IWidget) => {
         }}
       >
         <Space>
-          {next?.title}
+          <EllipsisMiddle maxWidth={300} suffixCount={7}>
+            {next?.title}
+          </EllipsisMiddle>
           <DoubleRightOutlined />
         </Space>
       </Button>

+ 8 - 9
dashboard/src/components/dict/Compound.tsx

@@ -58,16 +58,15 @@ const CompoundWidget = ({ word, add, split, onSearch }: IWidget) => {
     if (typeof word === "undefined") {
       return;
     }
-    get<IApiResponseDictList>(`/v2/userdict?view=compound&word=${word}`).then(
-      (json) => {
-        if (json.ok) {
-          const data = json.data.rows.map((item) => {
-            return { value: item.factors, label: item.factors };
-          });
-          setCompound(data);
-        }
+    const url = `/v2/userdict?view=compound&word=${word}&order=confidence`;
+    get<IApiResponseDictList>(url).then((json) => {
+      if (json.ok) {
+        const data = json.data.rows.map((item) => {
+          return { value: item.factors, label: item.factors };
+        });
+        setCompound(data);
       }
-    );
+    });
   }, [word]);
   return (
     <div

+ 404 - 0
dashboard/src/components/dict/UserDictList.tsx

@@ -0,0 +1,404 @@
+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;
+}
+
+interface IWidget {
+  studioName?: string;
+  view?: "studio" | "all";
+}
+const UserDictListWidget = ({ studioName, view = "studio" }: IWidget) => {
+  const intl = useIntl();
+  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 view === "all"
+                ? undefined
+                : [
+                    <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={
+          view === "all"
+            ? undefined
+            : {
+                // 自定义选择项参考: https://ant.design/components/table-cn/#components-table-demo-row-selection-custom
+                // 注释该行则默认不显示下拉选项
+                selections: [Table.SELECTION_ALL, Table.SELECTION_INVERT],
+              }
+        }
+        tableAlertRender={
+          view === "all"
+            ? undefined
+            : ({ 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={
+          view === "all"
+            ? undefined
+            : ({ 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?";
+          switch (view) {
+            case "studio":
+              url += `view=studio&name=${studioName}`;
+              break;
+            case "all":
+              url += `view=all`;
+              break;
+            default:
+              break;
+          }
+          url += `&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={
+          view === "all"
+            ? undefined
+            : () => [
+                <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>
+    </>
+  );
+};
+
+export default UserDictListWidget;

+ 3 - 0
dashboard/src/components/template/Wbw/WbwCase.tsx

@@ -37,6 +37,9 @@ const WbwCaseWidget = ({ data, display, onSplit, onChange }: IWidget) => {
 
   const inlineDict = useAppSelector(_inlineDict);
   useEffect(() => {
+    if (!data.real.value) {
+      return;
+    }
     if (inlineDict.wordIndex.includes(data.real.value)) {
       const result = inlineDict.wordList.filter(
         (word) => word.word === data.real.value

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

@@ -36,7 +36,7 @@ const WbwDetailAdvanceWidget = ({ data, onChange }: IWidget) => {
       <Input
         showCount
         maxLength={512}
-        defaultValue={data.real?.value}
+        defaultValue={data.real?.value ? data.real?.value : ""}
         onChange={onRealChange}
       />
     </>

+ 4 - 1
dashboard/src/components/template/Wbw/WbwDetailFactor.tsx

@@ -55,6 +55,9 @@ const WbwDetailFactorWidget = ({ data, onChange }: IWidget) => {
   }, [data.factors]);
 
   useEffect(() => {
+    if (!data.real.value) {
+      return;
+    }
     const factors = getFactorsInDict(
       data.real.value,
       inlineDict.wordIndex,
@@ -72,7 +75,7 @@ const WbwDetailFactorWidget = ({ data, onChange }: IWidget) => {
   return (
     <AutoComplete
       options={factorOptions}
-      defaultValue={data.factors?.value}
+      value={data.factors?.value}
       onChange={(value: string) => {
         if (typeof onChange !== "undefined") {
           onChange(value);

+ 3 - 0
dashboard/src/components/template/Wbw/WbwFactors.tsx

@@ -57,6 +57,9 @@ const WbwFactorsWidget = ({ data, display, onChange }: IWidget) => {
   const inlineDict = useAppSelector(_inlineDict);
 
   useEffect(() => {
+    if (!data.real.value) {
+      return;
+    }
     if (inlineDict.wordIndex.includes(data.real.value)) {
       const result = inlineDict.wordList.filter(
         (word) => word.word === data.real.value

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

@@ -54,7 +54,7 @@ const WbwMeaningSelectWidget = ({ data, onSelect }: IWidget) => {
 
   useEffect(() => {
     //判断单词列表里面是否有这个词
-    if (typeof data.real === "undefined") {
+    if (typeof data.real === "undefined" || data.real.value === null) {
       return;
     }
     if (inlineDict.wordIndex.includes(data.real.value)) {

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

@@ -1,5 +1,4 @@
 import { useState, useEffect, useRef } from "react";
-import type { UploadFile } from "antd/es/upload/interface";
 
 import { useAppSelector } from "../../../hooks";
 import { add, updateIndex, wordIndex } from "../../../reducers/inline-dict";
@@ -70,7 +69,7 @@ export interface IWbw {
   para: number;
   sn: number[];
   word: WbwElement<string>;
-  real: WbwElement<string>;
+  real: WbwElement<string | null>;
   meaning?: WbwElement<string | null>;
   type?: WbwElement<string | null>;
   grammar?: WbwElement<string | null>;
@@ -192,7 +191,7 @@ const WbwWordWidget = ({
   };
 
   if (wordData.type?.value === ".ctl.") {
-    if (wordData.word.value.includes("para")) {
+    if (wordData.word.value?.includes("para")) {
       return <WbwPara data={wordData} />;
     } else {
       return <WbwPage data={wordData} />;
@@ -207,7 +206,8 @@ const WbwWordWidget = ({
           if (
             intervalRef.current === null &&
             wordData.real &&
-            wordData.real.value?.length > 0
+            wordData.real.value &&
+            wordData.real.value.length > 0
           ) {
             //开始计时,计时结束查字典
             let words: string[] = [wordData.real.value];

+ 5 - 3
dashboard/src/components/template/WbwSent.tsx

@@ -39,7 +39,7 @@ interface IMagicDictResponse {
 interface IWbwXml {
   id: string;
   pali: WbwElement<string>;
-  real?: WbwElement<string>;
+  real?: WbwElement<string | null>;
   type?: WbwElement<string | null>;
   gramma?: WbwElement<string | null>;
   mean?: WbwElement<string | null>;
@@ -137,10 +137,11 @@ export const WbwSentCtl = ({
         (value) =>
           value.type?.value !== null &&
           value.type?.value !== ".ctl." &&
+          value.real.value &&
           value.real.value.length > 0
       )
       .forEach((value) => {
-        words.set(value.real.value, 1);
+        words.set(value.real.value ? value.real.value : "", 1);
         if (value.parent?.value) {
           words.set(value.parent.value, 1);
         }
@@ -366,7 +367,7 @@ export const WbwSentCtl = ({
         : ["", ""];
 
       wordData.push({
-        word: data.real.value,
+        word: data.real.value ? data.real.value : "",
         type: wordType,
         grammar: wordGrammar,
         mean: data.meaning?.value,
@@ -434,6 +435,7 @@ export const WbwSentCtl = ({
                   value.sn.length === e.sn.length &&
                   e.sn.slice(0, e.sn.length - 1).join() ===
                     value.sn.slice(0, e.sn.length - 1).join() &&
+                  value.real.value &&
                   value.real.value.length > 0
                 ) {
                   return true;

+ 15 - 0
dashboard/src/pages/admin/dictionary/index.tsx

@@ -0,0 +1,15 @@
+import { Outlet } from "react-router-dom";
+import { Layout } from "antd";
+import { styleStudioContent } from "../../studio/style";
+
+const { Content } = Layout;
+
+const Widget = () => {
+  return (
+    <Content style={styleStudioContent}>
+      <Outlet />
+    </Content>
+  );
+};
+
+export default Widget;

+ 7 - 0
dashboard/src/pages/admin/dictionary/list.tsx

@@ -0,0 +1,7 @@
+import UserDictList from "../../../components/dict/UserDictList";
+
+const Widget = () => {
+  return <UserDictList view="all" />;
+};
+
+export default Widget;