Ver Fonte

:construction: create

visuddhinanda há 3 anos atrás
pai
commit
9e2fd79881

+ 32 - 0
dashboard/src/components/admin/relation/CaseSelect.tsx

@@ -0,0 +1,32 @@
+import { ProFormSelect } from "@ant-design/pro-components";
+
+import { useIntl } from "react-intl";
+
+interface IWidget {
+  name?: string;
+  width?: number | "md" | "sm" | "xl" | "xs" | "lg";
+}
+const Widget = ({ name = "case", width = "md" }: IWidget) => {
+  const intl = useIntl();
+  const _case = ["nom", "acc", "gen", "dat", "inst", "abl", "loc"];
+  const caseOptions = _case.map((item) => {
+    return {
+      value: item,
+      label: intl.formatMessage({
+        id: `dict.fields.type.${item}.label`,
+      }),
+    };
+  });
+
+  return (
+    <ProFormSelect
+      options={caseOptions}
+      width={width}
+      name={name}
+      allowClear={false}
+      label={intl.formatMessage({ id: "forms.fields.case.label" })}
+    />
+  );
+};
+
+export default Widget;

+ 234 - 0
dashboard/src/components/channel/ChapterInChannelList.tsx

@@ -0,0 +1,234 @@
+import { useIntl } from "react-intl";
+import { Progress, Typography } from "antd";
+import { ProTable } from "@ant-design/pro-components";
+import { Link } from "react-router-dom";
+import { Space, Table } from "antd";
+import type { MenuProps } from "antd";
+import { Button, Dropdown, Menu } from "antd";
+import { SearchOutlined } from "@ant-design/icons";
+
+import { get } from "../../request";
+
+import { IChapterListResponse } from "../../components/api/Corpus";
+
+const { Text } = Typography;
+
+const onMenuClick: MenuProps["onClick"] = (e) => {
+  console.log("click", e);
+};
+
+const menu = (
+  <Menu
+    onClick={onMenuClick}
+    items={[
+      {
+        key: "share",
+        label: "分享",
+        icon: <SearchOutlined />,
+      },
+      {
+        key: "delete",
+        label: "删除",
+        icon: <SearchOutlined />,
+      },
+    ]}
+  />
+);
+
+interface IItem {
+  sn: number;
+  title: string;
+  subTitle: string;
+  summary: string;
+  book: number;
+  paragraph: number;
+  path: string;
+  progress: number;
+  view: number;
+  createdAt: number;
+  updatedAt: number;
+}
+interface IWidget {
+  channelId?: string;
+}
+const Widget = ({ channelId }: IWidget) => {
+  const intl = useIntl();
+
+  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={`/article/chapter/${row.book}-${row.paragraph}_${channelId}`}
+                  >
+                    {row.title ? row.title : row.subTitle}
+                  </Link>
+                </div>
+                <Text type="secondary">{row.subTitle}</Text>
+              </div>
+            );
+          },
+        },
+        {
+          title: intl.formatMessage({
+            id: "forms.fields.summary.label",
+          }),
+          dataIndex: "summary",
+          key: "summary",
+          tip: "过长会自动收缩",
+          ellipsis: true,
+        },
+        {
+          title: intl.formatMessage({
+            id: "forms.fields.publicity.label",
+          }),
+          dataIndex: "progress",
+          key: "progress",
+          width: 100,
+          search: false,
+          render: (text, row, index, action) => {
+            const per = Math.round(row.progress * 100);
+            return <Progress percent={per} size="small" />;
+          },
+        },
+        {
+          title: intl.formatMessage({
+            id: "forms.fields.publicity.label",
+          }),
+          dataIndex: "view",
+          key: "view",
+          width: 100,
+          search: false,
+        },
+        {
+          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={`/article/chapter/${row.book}-${row.paragraph}_${channelId}/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 offset = (params.current || 1 - 1) * (params.pageSize || 20);
+        const res = await get<IChapterListResponse>(
+          `/v2/progress?view=chapter&channel=${channelId}&progress=0.01&offset=${offset}`
+        );
+        console.log(res.data.rows);
+        const items: IItem[] = res.data.rows.map((item, id) => {
+          const createdAt = new Date(item.created_at);
+          const updatedAt = new Date(item.updated_at);
+          return {
+            sn: id + offset + 1,
+            book: item.book,
+            paragraph: item.para,
+            view: item.view,
+            title: item.title,
+            subTitle: item.toc,
+            summary: item.summary,
+            path: item.path,
+            progress: item.progress,
+            createdAt: createdAt.getTime(),
+            updatedAt: updatedAt.getTime(),
+          };
+        });
+        return {
+          total: res.data.count,
+          succcess: true,
+          data: items,
+        };
+      }}
+      rowKey="id"
+      bordered
+      pagination={{
+        showQuickJumper: true,
+        showSizeChanger: true,
+      }}
+      search={false}
+      options={{
+        search: true,
+      }}
+    />
+  );
+};
+
+export default Widget;

+ 67 - 0
dashboard/src/components/term/TermExport.tsx

@@ -0,0 +1,67 @@
+import { Button, message } from "antd";
+import { ExportOutlined } from "@ant-design/icons";
+import { API_HOST, get } from "../../request";
+import modal from "antd/lib/modal";
+import { useState } from "react";
+
+interface IExportResponse {
+  ok: boolean;
+  message: string;
+  data: {
+    uuid: string;
+    filename: string;
+    type: string;
+  };
+}
+interface IWidget {
+  channelId?: string;
+  studioName?: string;
+}
+const Widget = ({ channelId, studioName }: IWidget) => {
+  const [loading, setLoading] = useState(false);
+  return (
+    <Button
+      loading={loading}
+      icon={<ExportOutlined />}
+      onClick={() => {
+        let url = `/v2/terms-export?view=`;
+        if (typeof channelId !== "undefined") {
+          url += `channel&id=${channelId}`;
+        }
+        setLoading(true);
+        get<IExportResponse>(url)
+          .then((json) => {
+            if (json.ok) {
+              console.log("download", json);
+              const link = `${API_HOST}/api/v2/download/${json.data.type}/${json.data.uuid}/${json.data.filename}`;
+              modal.info({
+                title: "download",
+                content: (
+                  <>
+                    {"link: "}
+                    <a
+                      href={link}
+                      target="_blank"
+                      key="export"
+                      rel="noreferrer"
+                    >
+                      Download
+                    </a>
+                  </>
+                ),
+              });
+            } else {
+              message.error(json.message);
+            }
+          })
+          .finally(() => {
+            setLoading(false);
+          });
+      }}
+    >
+      Export
+    </Button>
+  );
+};
+
+export default Widget;

+ 349 - 0
dashboard/src/components/term/TermList.tsx

@@ -0,0 +1,349 @@
+import { ActionType, ProTable } from "@ant-design/pro-components";
+import { useIntl } from "react-intl";
+import {
+  Button,
+  Space,
+  Table,
+  Dropdown,
+  Typography,
+  Modal,
+  message,
+} from "antd";
+import {
+  ExclamationCircleOutlined,
+  DeleteOutlined,
+  ImportOutlined,
+} from "@ant-design/icons";
+
+import {
+  ITermDeleteRequest,
+  ITermListResponse,
+} from "../../components/api/Term";
+import { delete_2, get } from "../../request";
+import TermCreate from "../../components/term/TermCreate";
+import { IDeleteResponse } from "../../components/api/Article";
+import { useRef } from "react";
+import { IChannel } from "../channel/Channel";
+import TermExport from "./TermExport";
+import DataImport from "../admin/relation/DataImport";
+
+const { Text } = Typography;
+
+interface IItem {
+  sn: number;
+  id: string;
+  word: string;
+  tag: string;
+  channel?: IChannel;
+  meaning: string;
+  meaning2: string;
+  note: string;
+  createdAt: number;
+}
+
+interface IWidget {
+  studioName?: string;
+  channelId?: string;
+}
+const Widget = ({ studioName, channelId }: IWidget) => {
+  const intl = useIntl();
+
+  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<ITermDeleteRequest, IDeleteResponse>(
+          `/v2/terms/${id}`,
+          {
+            uuid: true,
+            id: 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<IItem>
+        actionRef={ref}
+        columns={[
+          {
+            title: intl.formatMessage({
+              id: "term.fields.sn.label",
+            }),
+            dataIndex: "sn",
+            key: "sn",
+            width: 80,
+            search: false,
+          },
+          {
+            title: intl.formatMessage({
+              id: "term.fields.word.label",
+            }),
+            dataIndex: "word",
+            key: "word",
+            tip: "单词过长会自动收缩",
+            ellipsis: true,
+          },
+          {
+            title: intl.formatMessage({
+              id: "term.fields.description.label",
+            }),
+            dataIndex: "tag",
+            key: "tag",
+            search: false,
+          },
+          {
+            title: intl.formatMessage({
+              id: "term.fields.channel.label",
+            }),
+            dataIndex: "channel",
+            key: "channel",
+            render(dom, entity, index, action, schema) {
+              return entity.channel?.name;
+            },
+          },
+          {
+            title: intl.formatMessage({
+              id: "term.fields.meaning.label",
+            }),
+            dataIndex: "meaning",
+            key: "meaning",
+          },
+          {
+            title: intl.formatMessage({
+              id: "term.fields.meaning2.label",
+            }),
+            dataIndex: "meaning2",
+            key: "meaning2",
+            tip: "意思过长会自动收缩",
+            ellipsis: true,
+          },
+          {
+            title: intl.formatMessage({
+              id: "term.fields.note.label",
+            }),
+            dataIndex: "note",
+            key: "note",
+            search: false,
+            tip: "注释过长会自动收缩",
+            ellipsis: true,
+          },
+          {
+            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: (
+                          <Text type="danger">
+                            {intl.formatMessage({
+                              id: "buttons.delete",
+                            })}
+                          </Text>
+                        ),
+                        icon: (
+                          <Text type="danger">
+                            <DeleteOutlined />
+                          </Text>
+                        ),
+                      },
+                    ],
+                    onClick: (e) => {
+                      switch (e.key) {
+                        case "remove":
+                          showDeleteConfirm([row.id], row.word);
+                          break;
+                        default:
+                          break;
+                      }
+                    },
+                  }}
+                >
+                  <TermCreate
+                    studio={studioName}
+                    channel={channelId}
+                    isCreate={false}
+                    wordId={row.id}
+                  />
+                </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/terms?`;
+          if (typeof channelId === "string") {
+            url += `view=channel&id=${channelId}`;
+          } else {
+            url += `view=studio&name=${studioName}`;
+          }
+
+          url += `&limit=${params.pageSize}&offset=${offset}`;
+          if (typeof params.keyword !== "undefined") {
+            url += "&search=" + (params.keyword ? params.keyword : "");
+          }
+
+          const res = await get<ITermListResponse>(url);
+          console.log(res);
+          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 {
+              sn: id2,
+              id: item.guid,
+              word: item.word,
+              tag: item.tag,
+              channel: item.channel,
+              meaning: item.meaning,
+              meaning2: item.other_meaning,
+              note: item.note,
+              createdAt: date.getTime(),
+            };
+          });
+          return {
+            total: res.data.count,
+            success: true,
+            data: items,
+          };
+        }}
+        rowKey="id"
+        //bordered
+        pagination={{
+          showQuickJumper: true,
+          showSizeChanger: true,
+        }}
+        toolBarRender={() => [
+          <DataImport
+            url="/v2/terms-import"
+            urlExtra={
+              channelId
+                ? `view=channel&id=${channelId}`
+                : `view=studio&name=${studioName}`
+            }
+            trigger={<Button icon={<ImportOutlined />}>Import</Button>}
+            onSuccess={() => {
+              ref.current?.reload();
+            }}
+          />,
+          <TermExport channelId={channelId} />,
+          <TermCreate
+            isCreate={true}
+            studio={studioName}
+            channel={channelId}
+            onUpdate={() => ref.current?.reload()}
+          />,
+        ]}
+        search={false}
+        options={{
+          search: true,
+        }}
+        dateFormatter="string"
+      />
+    </>
+  );
+};
+
+export default Widget;