2
0
visuddhinanda 2 жил өмнө
parent
commit
2bedcf5124

+ 105 - 0
dashboard/src/components/attachment/AttachmentImport.tsx

@@ -0,0 +1,105 @@
+import { Modal, Upload, UploadProps, message } from "antd";
+import { useEffect, useState } from "react";
+import { InboxOutlined } from "@ant-design/icons";
+import { API_HOST, delete_ } from "../../request";
+
+import { get as getToken } from "../../reducers/current-user";
+import { UploadFile } from "antd/es/upload/interface";
+import { IDeleteResponse } from "../api/Group";
+
+const { Dragger } = Upload;
+
+interface IWidget {
+  replaceId?: string;
+  open?: boolean;
+  onOpenChange?: Function;
+}
+const AttachmentImportWidget = ({
+  replaceId,
+  open = false,
+  onOpenChange,
+}: IWidget) => {
+  const [isOpen, setIsOpen] = useState(open);
+
+  useEffect(() => setIsOpen(open), [open]);
+
+  const props: UploadProps = {
+    name: "file",
+    listType: "picture",
+    multiple: replaceId ? false : true,
+    action: `${API_HOST}/api/v2/attachment`,
+    headers: {
+      Authorization: `Bearer ${getToken()}`,
+    },
+    onChange(info) {
+      const { status } = info.file;
+      if (status !== "uploading") {
+        console.log(info.file, info.fileList);
+      }
+      if (status === "done") {
+        message.success(`${info.file.name} file uploaded successfully.`);
+      } else if (status === "error") {
+        message.error(`${info.file.name} file upload failed.`);
+      }
+    },
+    onDrop(e) {
+      console.log("Dropped files", e.dataTransfer.files);
+    },
+    onRemove: (file: UploadFile<any>): boolean => {
+      console.log("remove", file);
+      const url = `/v2/attachment/${file.response.data.id}`;
+      console.info("avatar delete url", url);
+      delete_<IDeleteResponse>(url)
+        .then((json) => {
+          if (json.ok) {
+            message.success("删除成功");
+          } else {
+            message.error(json.message);
+          }
+        })
+        .catch((e) => console.log("Oops errors!", e));
+      return true;
+    },
+  };
+
+  const handleOk = () => {
+    setIsOpen(false);
+    if (typeof onOpenChange !== "undefined") {
+      onOpenChange(false);
+    }
+  };
+
+  const handleCancel = () => {
+    setIsOpen(false);
+    if (typeof onOpenChange !== "undefined") {
+      onOpenChange(false);
+    }
+  };
+
+  return (
+    <Modal
+      destroyOnClose={true}
+      width={700}
+      title="Upload"
+      footer={false}
+      open={isOpen}
+      onOk={handleOk}
+      onCancel={handleCancel}
+    >
+      <Dragger {...props}>
+        <p className="ant-upload-drag-icon">
+          <InboxOutlined />
+        </p>
+        <p className="ant-upload-text">
+          Click or drag file to this area to upload
+        </p>
+        <p className="ant-upload-hint">
+          Support for a single {replaceId ? "" : "or bulk"} upload. Strictly
+          prohibit from uploading company data or other band files
+        </p>
+      </Dragger>
+    </Modal>
+  );
+};
+
+export default AttachmentImportWidget;

+ 399 - 0
dashboard/src/components/attachment/AttachmentList.tsx

@@ -0,0 +1,399 @@
+import { useIntl } from "react-intl";
+import {
+  Button,
+  Space,
+  Table,
+  Dropdown,
+  message,
+  Modal,
+  Typography,
+  Image,
+} from "antd";
+import {
+  PlusOutlined,
+  ExclamationCircleOutlined,
+  FileOutlined,
+  AudioOutlined,
+  FileImageOutlined,
+  MoreOutlined,
+} from "@ant-design/icons";
+
+import { ActionType, ProList } from "@ant-design/pro-components";
+
+import { IUserDictDeleteRequest } from "../api/Dict";
+import { delete_2, get, put } from "../../request";
+import { useRef, useState } from "react";
+import { IDeleteResponse } from "../api/Article";
+import TimeShow from "../general/TimeShow";
+import { getSorterUrl } from "../../utils";
+import {
+  IAttachmentListResponse,
+  IAttachmentRequest,
+  IAttachmentResponse,
+  IAttachmentUpdate,
+} from "../api/Attachments";
+import { VideoIcon } from "../../assets/icon";
+import AttachmentImport from "./AttachmentImport";
+import VideoModal from "../general/VideoModal";
+import FileSize from "../general/FileSize";
+
+const { Text } = Typography;
+
+export interface IAttachment {
+  id: string;
+  name: string;
+  filename: string;
+  title: string;
+  size: number;
+  content_type: string;
+  url: string;
+}
+interface IParams {
+  content_type?: string;
+}
+interface IWidget {
+  studioName?: string;
+  view?: "studio" | "all";
+}
+const AttachmentWidget = ({ studioName, view = "studio" }: IWidget) => {
+  const intl = useIntl();
+  const [replaceId, setReplaceId] = useState<string>();
+  const [importOpen, setImportOpen] = useState(false);
+  const [imgVisible, setImgVisible] = useState(false);
+  const [imgPrev, setImgPrev] = useState<string>();
+
+  const [videoVisible, setVideoVisible] = useState(false);
+  const [videoUrl, setVideoUrl] = useState<string>();
+
+  const showDeleteConfirm = (id: string[], title: string) => {
+    Modal.confirm({
+      icon: <ExclamationCircleOutlined />,
+      title:
+        intl.formatMessage({
+          id: "message.delete.confirm",
+        }) +
+        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 (
+    <>
+      <ProList<IAttachmentRequest, IParams>
+        actionRef={ref}
+        editable={{
+          onSave: async (key, record, originRow) => {
+            console.log(key, record, originRow);
+            const url = `/v2/attachment/${key}`;
+            const res = await put<IAttachmentUpdate, IAttachmentResponse>(url, {
+              title: record.title,
+            });
+            return res.ok;
+          },
+        }}
+        metas={{
+          title: {
+            dataIndex: "title",
+            search: false,
+            render: (dom, entity, index, action, schema) => {
+              return (
+                <Button
+                  type="link"
+                  onClick={() => {
+                    const ct = entity.content_type.split("/");
+                    switch (ct[0]) {
+                      case "image":
+                        setImgPrev(entity.url);
+                        setImgVisible(true);
+                        break;
+                      case "video":
+                        setVideoUrl(entity.url);
+                        setVideoVisible(true);
+                        break;
+                      default:
+                        break;
+                    }
+                  }}
+                >
+                  {entity.title}
+                </Button>
+              );
+            },
+          },
+          description: {
+            render: (dom, entity, index, action, schema) => {
+              return (
+                <Text type="secondary">
+                  <Space>
+                    {entity.content_type}
+                    <FileSize size={entity.size} />
+                    <TimeShow
+                      type="secondary"
+                      createdAt={entity.created_at}
+                      updatedAt={entity.updated_at}
+                    />
+                  </Space>
+                </Text>
+              );
+            },
+            editable: false,
+            search: false,
+          },
+          avatar: {
+            editable: false,
+            search: false,
+            render: (dom, entity, index, action, schema) => {
+              const ct = entity.content_type.split("/");
+              let icon = <FileOutlined />;
+              switch (ct[0]) {
+                case "video":
+                  icon = <VideoIcon />;
+                  break;
+                case "audio":
+                  icon = <AudioOutlined />;
+                  break;
+                case "image":
+                  icon = <FileImageOutlined />;
+
+                  break;
+              }
+              return icon;
+            },
+          },
+          actions: {
+            render: (text, row, index, action) => {
+              return [
+                <Button
+                  type="link"
+                  size="small"
+                  onClick={() => {
+                    action?.startEditable(row.id);
+                  }}
+                >
+                  编辑
+                </Button>,
+                <Dropdown
+                  menu={{
+                    items: [
+                      { label: "替换", key: "replace" },
+                      { label: "引用模版", key: "tpl" },
+                    ],
+                    onClick: (e) => {
+                      console.log("click ", e.key);
+                      switch (e.key) {
+                        case "replace":
+                          setReplaceId(row.id);
+                          setImportOpen(true);
+                          break;
+
+                        default:
+                          break;
+                      }
+                    },
+                  }}
+                  placement="bottomRight"
+                >
+                  <Button
+                    type="link"
+                    size="small"
+                    icon={<MoreOutlined />}
+                    onClick={(e) => e.preventDefault()}
+                  />
+                </Dropdown>,
+              ];
+            },
+          },
+          content_type: {
+            // 自己扩展的字段,主要用于筛选,不在列表中显示
+            title: "类型",
+            valueType: "select",
+            valueEnum: {
+              all: { text: "全部", status: "Default" },
+              image: {
+                text: "图片",
+                status: "Error",
+              },
+              video: {
+                text: "视频",
+                status: "Success",
+              },
+              audio: {
+                text: "音频",
+                status: "Processing",
+              },
+            },
+          },
+        }}
+        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) => {
+          console.log(params, sorter, filter);
+          const offset =
+            ((params.current ? params.current : 1) - 1) *
+            (params.pageSize ? params.pageSize : 20);
+
+          let url = "/v2/attachment?";
+          switch (view) {
+            case "studio":
+              url += `view=studio&studio=${studioName}`;
+              break;
+            case "all":
+              url += `view=all`;
+              break;
+            default:
+              break;
+          }
+          url += `&limit=${params.pageSize}&offset=${offset}`;
+
+          url += params.keyword ? "&search=" + params.keyword : "";
+          if (params.content_type && params.content_type !== "all") {
+            url += "&content_type=" + params.content_type;
+          }
+
+          url += getSorterUrl(sorter);
+
+          console.log(url);
+          const res = await get<IAttachmentListResponse>(url);
+          return {
+            total: res.data.count,
+            success: res.ok,
+            data: res.data.rows,
+          };
+        }}
+        rowKey="id"
+        bordered
+        pagination={{
+          showQuickJumper: true,
+          showSizeChanger: true,
+        }}
+        search={{
+          filterType: "light",
+        }}
+        options={{
+          search: true,
+        }}
+        headerTitle=""
+        toolBarRender={() => [
+          <Button
+            key="button"
+            icon={<PlusOutlined />}
+            type="primary"
+            onClick={() => {
+              setReplaceId(undefined);
+              setImportOpen(true);
+            }}
+            disabled={view === "all"}
+          >
+            {intl.formatMessage({ id: "buttons.import" })}
+          </Button>,
+        ]}
+      />
+      <AttachmentImport
+        replaceId={replaceId}
+        open={importOpen}
+        onOpenChange={(open: boolean) => {
+          setImportOpen(open);
+          ref.current?.reload();
+        }}
+      />
+
+      <Image
+        width={200}
+        style={{ display: "none" }}
+        preview={{
+          visible: imgVisible,
+          src: imgPrev,
+          onVisibleChange: (value) => {
+            setImgVisible(value);
+          },
+        }}
+      />
+      <VideoModal
+        src={videoUrl}
+        open={videoVisible}
+        onOpenChange={(open: boolean) => setVideoVisible(open)}
+      />
+    </>
+  );
+};
+
+export default AttachmentWidget;

+ 24 - 0
dashboard/src/components/general/FileSize.tsx

@@ -0,0 +1,24 @@
+interface IWidget {
+  size?: number;
+}
+const FileSizeWidget = ({ size = 0 }: IWidget) => {
+  let strSize = 0;
+  let end = "";
+  if (size > Math.pow(1024, 3)) {
+    strSize = size / Math.pow(1024, 3);
+    end = "GB";
+  } else if (size > Math.pow(1024, 2)) {
+    strSize = size / Math.pow(1024, 2);
+    end = "MB";
+  } else if (size > Math.pow(1024, 1)) {
+    strSize = size / Math.pow(1024, 1);
+    end = "KB";
+  } else {
+    strSize = size;
+    end = "B";
+  }
+  const output = strSize.toString().substring(0, 4) + end;
+  return <>{output}</>;
+};
+
+export default FileSizeWidget;

+ 20 - 0
dashboard/src/pages/studio/attachment/index.tsx

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

+ 10 - 0
dashboard/src/pages/studio/attachment/list.tsx

@@ -0,0 +1,10 @@
+import { useParams } from "react-router-dom";
+
+import AttachmentList from "../../../components/attachment/AttachmentList";
+
+const Widget = () => {
+  const { studioname } = useParams();
+  return <AttachmentList studioName={studioname} />;
+};
+
+export default Widget;