ソースを参照

Merge pull request #1945 from visuddhinanda/agile

使用attachment 组件获取附件
visuddhinanda 2 年 前
コミット
99888de3ae

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

@@ -6,6 +6,7 @@ export interface IAttachmentRequest {
   size: number;
   content_type: string;
   url: string;
+  thumbnail?: { small: string; middle: string };
   created_at?: string;
   updated_at?: string;
 }

+ 0 - 1
dashboard/src/components/article/RightPanel.tsx

@@ -4,7 +4,6 @@ import { CloseOutlined } from "@ant-design/icons";
 import { FullscreenOutlined, FullscreenExitOutlined } from "@ant-design/icons";
 
 import { IChannel } from "../channel/Channel";
-import ChannelPickerTable from "../channel/ChannelPickerTable";
 import DictComponent from "../dict/DictComponent";
 import { ArticleType } from "./Article";
 import { useAppSelector } from "../../hooks";

+ 68 - 0
dashboard/src/components/attachment/AttachmentDialog.tsx

@@ -0,0 +1,68 @@
+import { Modal } from "antd";
+import { useEffect, useState } from "react";
+import AttachmentList from "./AttachmentList";
+import { IAttachmentRequest } from "../api/Attachments";
+
+interface IWidget {
+  open?: boolean;
+  trigger?: React.ReactNode;
+  studioName?: string;
+  onOpenChange?: Function;
+  onSelect?: Function;
+}
+const AttachmentDialog = ({
+  open,
+  trigger,
+  studioName,
+  onOpenChange,
+  onSelect,
+}: IWidget) => {
+  const [isModalOpen, setIsModalOpen] = useState(open);
+
+  useEffect(() => setIsModalOpen(open), [open]);
+  const showModal = () => {
+    setIsModalOpen(true);
+    if (typeof onOpenChange !== "undefined") {
+      onOpenChange(true);
+    }
+  };
+
+  const handleOk = () => {
+    setIsModalOpen(false);
+    if (typeof onOpenChange !== "undefined") {
+      onOpenChange(false);
+    }
+  };
+
+  const handleCancel = () => {
+    setIsModalOpen(false);
+    if (typeof onOpenChange !== "undefined") {
+      onOpenChange(false);
+    }
+  };
+  return (
+    <>
+      <span onClick={showModal}>{trigger}</span>
+      <Modal
+        width={700}
+        title="加入文集"
+        open={isModalOpen}
+        onOk={handleOk}
+        onCancel={handleCancel}
+        maskClosable={false}
+      >
+        <AttachmentList
+          studioName={studioName}
+          onClick={(value: IAttachmentRequest) => {
+            if (typeof onSelect !== "undefined") {
+              onSelect(value);
+            }
+            handleOk();
+          }}
+        />
+      </Modal>
+    </>
+  );
+};
+
+export default AttachmentDialog;

+ 32 - 17
dashboard/src/components/attachment/AttachmentImport.tsx

@@ -1,14 +1,29 @@
 import { Modal, Upload, UploadProps, message } from "antd";
 import { useEffect, useState } from "react";
-import { InboxOutlined } from "@ant-design/icons";
+import { InboxOutlined, ExclamationCircleOutlined } 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";
+import modal from "antd/lib/modal";
 
 const { Dragger } = Upload;
 
+export const deleteRes = (id: string) => {
+  const url = `/v2/attachment/${id}`;
+  console.info("attachment 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));
+};
+
 interface IWidget {
   replaceId?: string;
   open?: boolean;
@@ -27,7 +42,7 @@ const AttachmentImportWidget = ({
     name: "file",
     listType: "picture",
     multiple: replaceId ? false : true,
-    action: `${API_HOST}/api/v2/attachment`,
+    action: `${API_HOST}/api/v2/attachment?id=${replaceId}`,
     headers: {
       Authorization: `Bearer ${getToken()}`,
     },
@@ -47,17 +62,7 @@ const AttachmentImportWidget = ({
     },
     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));
+      deleteRes(file.response.data.id);
       return true;
     },
   };
@@ -70,10 +75,19 @@ const AttachmentImportWidget = ({
   };
 
   const handleCancel = () => {
-    setIsOpen(false);
-    if (typeof onOpenChange !== "undefined") {
-      onOpenChange(false);
-    }
+    modal.confirm({
+      title: "关闭上传窗口",
+      icon: <ExclamationCircleOutlined />,
+      content: "所有正在上传文件将取消上传。",
+      okText: "确认",
+      cancelText: "取消",
+      onOk: () => {
+        setIsOpen(false);
+        if (typeof onOpenChange !== "undefined") {
+          onOpenChange(false);
+        }
+      },
+    });
   };
 
   return (
@@ -82,6 +96,7 @@ const AttachmentImportWidget = ({
       width={700}
       title="Upload"
       footer={false}
+      maskClosable={false}
       open={isOpen}
       onOk={handleOk}
       onCancel={handleCancel}

+ 83 - 4
dashboard/src/components/attachment/AttachmentList.tsx

@@ -8,6 +8,7 @@ import {
   Modal,
   Typography,
   Image,
+  Segmented,
 } from "antd";
 import {
   PlusOutlined,
@@ -16,6 +17,8 @@ import {
   AudioOutlined,
   FileImageOutlined,
   MoreOutlined,
+  BarsOutlined,
+  AppstoreOutlined,
 } from "@ant-design/icons";
 
 import { ActionType, ProList } from "@ant-design/pro-components";
@@ -33,9 +36,10 @@ import {
   IAttachmentUpdate,
 } from "../api/Attachments";
 import { VideoIcon } from "../../assets/icon";
-import AttachmentImport from "./AttachmentImport";
+import AttachmentImport, { deleteRes } from "./AttachmentImport";
 import VideoModal from "../general/VideoModal";
 import FileSize from "../general/FileSize";
+import modal from "antd/lib/modal";
 
 const { Text } = Typography;
 
@@ -54,13 +58,21 @@ interface IParams {
 interface IWidget {
   studioName?: string;
   view?: "studio" | "all";
+  multiSelect?: boolean;
+  onClick?: Function;
 }
-const AttachmentWidget = ({ studioName, view = "studio" }: IWidget) => {
+const AttachmentWidget = ({
+  studioName,
+  view = "studio",
+  multiSelect = false,
+  onClick,
+}: 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 [list, setList] = useState("list");
 
   const [videoVisible, setVideoVisible] = useState(false);
   const [videoUrl, setVideoUrl] = useState<string>();
@@ -121,6 +133,17 @@ const AttachmentWidget = ({ studioName, view = "studio" }: IWidget) => {
             return res.ok;
           },
         }}
+        ghost={list === "list" ? false : true}
+        onItem={(record: IAttachmentRequest, index: number) => {
+          return {
+            onClick: (event) => {
+              // 点击行
+              if (typeof onClick !== "undefined") {
+                onClick(record);
+              }
+            },
+          };
+        }}
         metas={{
           title: {
             dataIndex: "title",
@@ -169,6 +192,27 @@ const AttachmentWidget = ({ studioName, view = "studio" }: IWidget) => {
             editable: false,
             search: false,
           },
+          content:
+            list === "list"
+              ? undefined
+              : {
+                  editable: false,
+                  search: false,
+                  render: (dom, entity, index, action, schema) => {
+                    const thumbnail = entity.thumbnail
+                      ? entity.thumbnail.middle
+                      : entity.url;
+
+                    return (
+                      <Image
+                        src={thumbnail}
+                        preview={{
+                          src: entity.url,
+                        }}
+                      />
+                    );
+                  },
+                },
           avatar: {
             editable: false,
             search: false,
@@ -207,6 +251,7 @@ const AttachmentWidget = ({ studioName, view = "studio" }: IWidget) => {
                     items: [
                       { label: "替换", key: "replace" },
                       { label: "引用模版", key: "tpl" },
+                      { label: "删除", key: "delete", danger: true },
                     ],
                     onClick: (e) => {
                       console.log("click ", e.key);
@@ -215,7 +260,24 @@ const AttachmentWidget = ({ studioName, view = "studio" }: IWidget) => {
                           setReplaceId(row.id);
                           setImportOpen(true);
                           break;
-
+                        case "delete":
+                          modal.confirm({
+                            title: intl.formatMessage({
+                              id: "message.delete.confirm",
+                            }),
+                            icon: <ExclamationCircleOutlined />,
+                            content: intl.formatMessage({
+                              id: "message.irrevocable",
+                            }),
+                            okText: "确认",
+                            cancelText: "取消",
+                            okType: "danger",
+                            onOk: () => {
+                              deleteRes(row.id);
+                              ref.current?.reload();
+                            },
+                          });
+                          break;
                         default:
                           break;
                       }
@@ -257,11 +319,13 @@ const AttachmentWidget = ({ studioName, view = "studio" }: IWidget) => {
         rowSelection={
           view === "all"
             ? undefined
-            : {
+            : multiSelect
+            ? {
                 // 自定义选择项参考: https://ant.design/components/table-cn/#components-table-demo-row-selection-custom
                 // 注释该行则默认不显示下拉选项
                 selections: [Table.SELECTION_ALL, Table.SELECTION_INVERT],
               }
+            : undefined
         }
         tableAlertRender={
           view === "all"
@@ -351,8 +415,23 @@ const AttachmentWidget = ({ studioName, view = "studio" }: IWidget) => {
         options={{
           search: true,
         }}
+        grid={list === "list" ? undefined : { gutter: 16, column: 3 }}
         headerTitle=""
         toolBarRender={() => [
+          <Segmented
+            options={[
+              { label: "List", value: "list", icon: <BarsOutlined /> },
+              {
+                label: "Thumbnail",
+                value: "thumbnail",
+                icon: <AppstoreOutlined />,
+              },
+            ]}
+            onChange={(value) => {
+              console.log(value); // string
+              setList(value.toString());
+            }}
+          />,
           <Button
             key="button"
             icon={<PlusOutlined />}

+ 1 - 1
dashboard/src/components/general/VideoModal.tsx

@@ -46,7 +46,7 @@ export const VideoModalWidget = ({
         onOk={handleClose}
         onCancel={handleClose}
         width={800}
-        destroyOnClose
+        destroyOnClose={false}
         maskClosable={false}
         mask={false}
       >

+ 7 - 1
dashboard/src/components/template/Article.tsx

@@ -8,7 +8,13 @@ import { useIntl } from "react-intl";
 
 const { Text } = Typography;
 
-export type TDisplayStyle = "modal" | "card" | "toggle" | "link" | "window";
+export type TDisplayStyle =
+  | "modal"
+  | "card"
+  | "toggle"
+  | "link"
+  | "window"
+  | "popover";
 interface IWidgetChapterCtl {
   type?: ArticleType;
   id?: string;

+ 160 - 78
dashboard/src/components/template/Video.tsx

@@ -1,24 +1,50 @@
-import { Card, Collapse, Modal, Space } from "antd";
+import { Button, Card, Collapse, Modal, Popover, Space } from "antd";
 import { Typography } from "antd";
-import { useState } from "react";
+import { useEffect, useState } from "react";
+import { CloseOutlined } from "@ant-design/icons";
 
 import { Link } from "react-router-dom";
 import { TDisplayStyle } from "./Article";
 import Video from "../general/Video";
 import { VideoIcon } from "../../assets/icon";
 import { useIntl } from "react-intl";
+import { IAttachmentResponse } from "../api/Attachments";
+import { get } from "../../request";
 
 const { Text } = Typography;
 
 interface IVideoCtl {
   url?: string;
+  id?: string;
   title?: React.ReactNode;
   style?: TDisplayStyle;
+  _style?: TDisplayStyle;
 }
 
-export const VideoCtl = ({ url, title, style = "modal" }: IVideoCtl) => {
+export const VideoCtl = ({
+  url,
+  id,
+  title,
+  style = "modal",
+  _style,
+}: IVideoCtl) => {
   const intl = useIntl();
   const [isModalOpen, setIsModalOpen] = useState(false);
+  const [fetchUrl, setFetchUrl] = useState<string>();
+  style = _style ? _style : style;
+  useEffect(() => {
+    if (id) {
+      const url = `/v2/attachment/${id}`;
+      console.info("url", url);
+      get<IAttachmentResponse>(url).then((json) => {
+        console.log(json);
+        if (json.ok) {
+          setFetchUrl(json.data.url);
+        }
+      });
+    }
+  }, [id]);
+
   const showModal = () => {
     setIsModalOpen(true);
   };
@@ -32,92 +58,148 @@ export const VideoCtl = ({ url, title, style = "modal" }: IVideoCtl) => {
   };
 
   let output = <></>;
-  let articleLink = url ? url : "";
+  let articleLink = url ? url : fetchUrl ? fetchUrl : "";
 
-  switch (style) {
-    case "modal":
-      output = (
-        <>
-          <Typography.Link
-            onClick={(event: React.MouseEvent<HTMLElement, MouseEvent>) => {
-              if (event.ctrlKey || event.metaKey) {
-                window.open(articleLink, "_blank");
-              } else {
-                showModal();
-              }
-            }}
-          >
-            <Space>
-              <VideoIcon />
-              {title}
-            </Space>
-          </Typography.Link>
-          <Modal
-            width={"90%"}
-            destroyOnClose
-            style={{ maxWidth: 1000, top: 20, height: 700 }}
-            title={
-              <div
-                style={{
-                  display: "flex",
-                  justifyContent: "space-between",
-                  marginRight: 30,
-                }}
-              >
-                <Text>{title}</Text>
-                <Text>
-                  <Link to={articleLink} target="_blank">
-                    {intl.formatMessage({
-                      id: "buttons.open.in.new.tab",
-                    })}
-                  </Link>
-                </Text>
-              </div>
+  const VideoModal = () => {
+    return (
+      <>
+        <Typography.Link
+          onClick={(event: React.MouseEvent<HTMLElement, MouseEvent>) => {
+            if (event.ctrlKey || event.metaKey) {
+              window.open(articleLink, "_blank");
+            } else {
+              showModal();
             }
-            open={isModalOpen}
-            onOk={handleOk}
-            onCancel={handleCancel}
-            footer={[]}
-          >
-            <div style={{ height: 550 }}>
-              <Video src={url} />
+          }}
+        >
+          <Space>
+            <VideoIcon />
+            {title}
+          </Space>
+        </Typography.Link>
+        <Modal
+          width={"90%"}
+          destroyOnClose
+          style={{ maxWidth: 1000, top: 20, height: 700 }}
+          title={
+            <div
+              style={{
+                display: "flex",
+                justifyContent: "space-between",
+                marginRight: 30,
+              }}
+            >
+              <Text>{title}</Text>
+              <Text>
+                <Link to={articleLink} target="_blank">
+                  {intl.formatMessage({
+                    id: "buttons.open.in.new.tab",
+                  })}
+                </Link>
+              </Text>
             </div>
-          </Modal>
-        </>
-      );
+          }
+          open={isModalOpen}
+          onOk={handleOk}
+          onCancel={handleCancel}
+          footer={[]}
+        >
+          <div style={{ height: 550 }}>
+            <Video src={url} />
+          </div>
+        </Modal>
+      </>
+    );
+  };
+
+  const VideoCard = () => {
+    return (
+      <Card title={title} bodyStyle={{ width: 550, height: 400 }}>
+        <Video src={url} />
+      </Card>
+    );
+  };
+
+  const VideoWindow = () => {
+    return (
+      <div style={{ width: 550, height: 320 }}>
+        <Video src={url} />
+      </div>
+    );
+  };
+
+  const VideoToggle = () => {
+    return (
+      <Collapse bordered={false}>
+        <Collapse.Panel header={title} key="parent2">
+          <Video src={url} />
+        </Collapse.Panel>
+      </Collapse>
+    );
+  };
+
+  const VideoLink = () => {
+    return (
+      <Link to={articleLink} target="_blank">
+        <Space>
+          <VideoIcon />
+          {title}
+        </Space>
+      </Link>
+    );
+  };
+
+  const VideoPopover = () => {
+    const [popOpen, setPopOpen] = useState(false);
+
+    return (
+      <Popover
+        title={
+          <div>
+            {title}
+            <Button
+              type="link"
+              icon={<CloseOutlined />}
+              onClick={() => {
+                setPopOpen(false);
+              }}
+            />
+          </div>
+        }
+        content={
+          <div style={{ width: 600, height: 350 }}>
+            <Video src={url} />
+          </div>
+        }
+        trigger={"click"}
+        placement="bottom"
+        open={popOpen}
+      >
+        <span onClick={() => setPopOpen(true)}>
+          <VideoIcon />
+          {title}
+        </span>
+      </Popover>
+    );
+  };
+  switch (style) {
+    case "modal":
+      output = <VideoModal />;
       break;
     case "card":
-      output = (
-        <Card title={title} bodyStyle={{ width: 550, height: 400 }}>
-          <Video src={url} />
-        </Card>
-      );
+      output = <VideoCard />;
       break;
     case "window":
-      output = (
-        <div style={{ width: 550, height: 320 }}>
-          <Video src={url} />
-        </div>
-      );
+      output = <VideoWindow />;
       break;
     case "toggle":
-      output = (
-        <Collapse bordered={false}>
-          <Collapse.Panel header={title} key="parent2">
-            <Video src={url} />
-          </Collapse.Panel>
-        </Collapse>
-      );
+      output = <VideoToggle />;
       break;
     case "link":
-      output = (
-        <Link to={articleLink} target="_blank">
-          <Space>
-            <VideoIcon />
-            {title}
-          </Space>
-        </Link>
-      );
+      output = <VideoLink />;
+      break;
+    case "popover":
+      output = <VideoPopover />;
       break;
     default:
       break;

+ 19 - 11
dashboard/src/components/template/Wbw/WbwDetail.tsx

@@ -3,7 +3,7 @@ import { useIntl } from "react-intl";
 import { Dropdown, Tabs, Divider, Button, Switch, Rate } from "antd";
 import { SaveOutlined } from "@ant-design/icons";
 
-import { IWbw, IWbwField, TFieldName } from "./WbwWord";
+import { IWbw, IWbwAttachment, IWbwField, TFieldName } from "./WbwWord";
 import WbwDetailBasic from "./WbwDetailBasic";
 import WbwDetailBookMark from "./WbwDetailBookMark";
 import WbwDetailNote from "./WbwDetailNote";
@@ -14,7 +14,7 @@ import {
   UnLockIcon,
 } from "../../../assets/icon";
 import { UploadFile } from "antd/es/upload/interface";
-import { IAttachmentResponse } from "../../api/Attachments";
+import { IAttachmentRequest, IAttachmentResponse } from "../../api/Attachments";
 import WbwDetailAttachment from "./WbwDetailAttachment";
 import CommentBox from "../../discussion/DiscussionDrawer";
 
@@ -23,12 +23,14 @@ interface IWidget {
   onClose?: Function;
   onSave?: Function;
   onCommentCountChange?: Function;
+  onAttachmentSelectOpen?: Function;
 }
 const WbwDetailWidget = ({
   data,
   onClose,
   onSave,
   onCommentCountChange,
+  onAttachmentSelectOpen,
 }: IWidget) => {
   const intl = useIntl();
   const [currWbwData, setCurrWbwData] = useState<IWbw>(
@@ -193,23 +195,29 @@ const WbwDetailWidget = ({
               <div style={{ minHeight: 270 }}>
                 <WbwDetailAttachment
                   data={currWbwData}
-                  onChange={(e: IWbwField) => {
-                    fieldChanged(e.field, e.value);
-                  }}
-                  onUpload={(fileList: UploadFile<IAttachmentResponse>[]) => {
+                  onUpload={(fileList: IAttachmentRequest[]) => {
                     let mData = JSON.parse(JSON.stringify(currWbwData));
                     mData.attachments = fileList.map((item) => {
                       return {
-                        id: item.response ? item.response?.data.id : item.uid,
-                        title: item.name,
+                        id: item.id,
+                        title: item.title,
                         size: item.size ? item.size : 0,
-                        content_type: item.response
-                          ? item.response?.data.content_type
-                          : "",
+                        content_type: item.content_type,
                       };
                     });
                     setCurrWbwData(mData);
                   }}
+                  onDialogOpen={(open: boolean) => {
+                    if (typeof onAttachmentSelectOpen !== "undefined") {
+                      onAttachmentSelectOpen(open);
+                    }
+                  }}
+                  onChange={(value: IWbwAttachment[]) => {
+                    let mData = JSON.parse(JSON.stringify(currWbwData));
+                    mData.attachments = value;
+                    setCurrWbwData(mData);
+                    //fieldChanged(e.field, e.value);
+                  }}
                 />
               </div>
             ),

+ 20 - 4
dashboard/src/components/template/Wbw/WbwDetailAttachment.tsx

@@ -1,24 +1,40 @@
 import { UploadFile } from "antd/es/upload/interface";
-import { IAttachmentResponse } from "../../api/Attachments";
+import { IAttachmentRequest, IAttachmentResponse } from "../../api/Attachments";
 import WbwDetailUpload from "./WbwDetailUpload";
 
-import { IWbw } from "./WbwWord";
+import { IWbw, IWbwAttachment } from "./WbwWord";
 
 interface IWidget {
   data: IWbw;
   onChange?: Function;
   onUpload?: Function;
+  onDialogOpen?: Function;
 }
-const WbwDetailAttachmentWidget = ({ data, onChange, onUpload }: IWidget) => {
+const WbwDetailAttachmentWidget = ({
+  data,
+  onChange,
+  onUpload,
+  onDialogOpen,
+}: IWidget) => {
   return (
     <div>
       <WbwDetailUpload
         data={data}
-        onUpload={(fileList: UploadFile<IAttachmentResponse>[]) => {
+        onUpload={(fileList: IAttachmentRequest[]) => {
           if (typeof onUpload !== "undefined") {
             onUpload(fileList);
           }
         }}
+        onDialogOpen={(open: boolean) => {
+          if (typeof onDialogOpen !== "undefined") {
+            onDialogOpen(open);
+          }
+        }}
+        onChange={(value: IWbwAttachment[]) => {
+          if (typeof onChange !== "undefined") {
+            onChange(value);
+          }
+        }}
       />
     </div>
   );

+ 72 - 10
dashboard/src/components/template/Wbw/WbwDetailUpload.tsx

@@ -1,18 +1,31 @@
 import { useIntl } from "react-intl";
-import { UploadOutlined } from "@ant-design/icons";
+import { DeleteOutlined } from "@ant-design/icons";
 import type { UploadProps } from "antd";
-import { Button, message, Upload } from "antd";
+import { Button, List, message, Upload } from "antd";
 
 import { API_HOST } from "../../../request";
-import { get as getToken } from "../../../reducers/current-user";
-import { IWbw } from "./WbwWord";
+import { currentUser, get as getToken } from "../../../reducers/current-user";
+import { IWbw, IWbwAttachment } from "./WbwWord";
+import AttachmentDialog from "../../attachment/AttachmentDialog";
+import { useAppSelector } from "../../../hooks";
+import { useState } from "react";
+import { IAttachmentRequest } from "../../api/Attachments";
 
 interface IWidget {
   data: IWbw;
   onUpload?: Function;
+  onChange?: Function;
+  onDialogOpen?: Function;
 }
-const WbwDetailUploadWidget = ({ data, onUpload }: IWidget) => {
+const WbwDetailUploadWidget = ({
+  data,
+  onUpload,
+  onChange,
+  onDialogOpen,
+}: IWidget) => {
   const intl = useIntl();
+  const user = useAppSelector(currentUser);
+  const attachments = data.attachments;
 
   const props: UploadProps = {
     name: "file",
@@ -46,12 +59,61 @@ const WbwDetailUploadWidget = ({ data, onUpload }: IWidget) => {
     },
   };
 
+  /**
+   *       <Upload {...props}>
+        <Button icon={<UploadOutlined />}>
+          {intl.formatMessage({ id: "buttons.click.upload" })}
+        </Button>
+      </Upload>
+   */
   return (
-    <Upload {...props}>
-      <Button icon={<UploadOutlined />}>
-        {intl.formatMessage({ id: "buttons.click.upload" })}
-      </Button>
-    </Upload>
+    <>
+      <List
+        itemLayout="vertical"
+        size="small"
+        header={
+          <AttachmentDialog
+            trigger={<Button>上传</Button>}
+            studioName={user?.realName}
+            onOpenChange={(open: boolean) => {
+              if (typeof onDialogOpen !== "undefined") {
+                onDialogOpen(open);
+              }
+            }}
+            onSelect={(value: IAttachmentRequest) => {
+              if (typeof onUpload !== "undefined") {
+                onUpload([value]);
+              }
+            }}
+          />
+        }
+        dataSource={attachments}
+        renderItem={(item, id) => (
+          <List.Item>
+            <div style={{ display: "flex", justifyContent: "space-between" }}>
+              <div style={{ maxWidth: 400, overflowX: "hidden" }}>
+                {item.title}
+              </div>
+              <div style={{ marginLeft: 20 }}>
+                <Button
+                  type="link"
+                  size="small"
+                  icon={<DeleteOutlined />}
+                  onClick={() => {
+                    const output = data.attachments?.filter(
+                      (value: IWbwAttachment, index: number) => index !== id
+                    );
+                    if (typeof onChange !== "undefined") {
+                      onChange(output);
+                    }
+                  }}
+                />
+              </div>
+            </div>
+          </List.Item>
+        )}
+      />
+    </>
   );
 };
 

+ 43 - 23
dashboard/src/components/template/Wbw/WbwPali.tsx

@@ -1,5 +1,5 @@
 import { useEffect, useRef, useState } from "react";
-import { Button, Popover, Space, Typography } from "antd";
+import { Popover, Space, Tooltip, Typography } from "antd";
 import {
   TagTwoTone,
   InfoCircleOutlined,
@@ -164,28 +164,36 @@ const WbwPaliWidget = ({ data, channelId, mode, display, onSave }: IWidget) => {
           setHasComment(false);
         }
       }}
+      onAttachmentSelectOpen={(open: boolean) => {
+        setPopOpen(!open);
+      }}
     />
   );
 
-  const noteIcon = () =>
-    data.note?.value ? (
+  const NoteIcon = () => {
+    return data.note?.value ? (
       data.note.value.trim() !== "" ? (
         <Popover content={data.note?.value} placement="bottom">
           <InfoCircleOutlined style={{ color: "blue" }} />
         </Popover>
-      ) : undefined
-    ) : undefined;
+      ) : (
+        <></>
+      )
+    ) : (
+      <></>
+    );
+  };
 
   const color = data.bookMarkColor?.value
     ? bookMarkColor[data.bookMarkColor.value]
     : "white";
 
   //生成视频播放按钮
-  const videoList = data.attachments?.filter((item) =>
-    item.content_type?.includes("video")
-  );
-  const videoIcon = () =>
-    videoList ? (
+  const VideoIcon = () => {
+    const videoList = data.attachments?.filter((item) =>
+      item.content_type?.includes("video")
+    );
+    return videoList ? (
       <WbwVideoButton
         video={videoList?.map((item) => {
           return {
@@ -198,19 +206,28 @@ const WbwPaliWidget = ({ data, channelId, mode, display, onSave }: IWidget) => {
     ) : (
       <></>
     );
+  };
 
-  const relationIcon = () =>
-    data.relation ? <ApartmentOutlined style={{ color: "blue" }} /> : undefined;
+  const RelationIcon = () => {
+    return data.relation ? (
+      <ApartmentOutlined style={{ color: "blue" }} />
+    ) : (
+      <></>
+    );
+  };
 
-  const bookMarkIcon = () =>
-    data.bookMarkText?.value && data.bookMarkText.value.trim() !== "" ? (
+  const BookMarkIcon = () => {
+    return data.bookMarkText?.value && data.bookMarkText.value.trim() !== "" ? (
       <Popover
         content={<Paragraph copyable>{data.bookMarkText.value}</Paragraph>}
         placement="bottom"
       >
         <TagTwoTone twoToneColor={color} />
       </Popover>
-    ) : undefined;
+    ) : (
+      <></>
+    );
+  };
 
   let classPali: string = "pali";
   switch (data.style?.value) {
@@ -278,14 +295,16 @@ const WbwPaliWidget = ({ data, channelId, mode, display, onSave }: IWidget) => {
     display: "inline-block",
   };
 
-  const discussionIcon = () =>
-    hasComment ? (
+  const DiscussionIcon = () => {
+    return hasComment ? (
       <div style={commentShellStyle}>
         <CommentBox
           resId={data.uid}
           resType="wbw"
           trigger={
-            <Button icon={<CommentOutlinedIcon />} type="text" title="讨论" />
+            <Tooltip title="讨论">
+              <CommentOutlinedIcon style={{ cursor: "pointer" }} />
+            </Tooltip>
           }
           onCommentCountChange={(count: number) => {
             if (count > 0) {
@@ -299,6 +318,7 @@ const WbwPaliWidget = ({ data, channelId, mode, display, onSave }: IWidget) => {
     ) : (
       <></>
     );
+  };
 
   if (typeof data.real !== "undefined" && data.real.value !== "") {
     //非标点符号
@@ -354,11 +374,11 @@ const WbwPaliWidget = ({ data, channelId, mode, display, onSave }: IWidget) => {
           </Popover>
         </span>
         <Space>
-          {videoIcon()}
-          {noteIcon()}
-          {bookMarkIcon()}
-          {relationIcon()}
-          {discussionIcon()}
+          <VideoIcon />
+          <NoteIcon />
+          <BookMarkIcon />
+          <RelationIcon />
+          <DiscussionIcon />
         </Space>
       </div>
     );

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

@@ -19,6 +19,9 @@ const WbwVideoButtonWidget = ({ video }: IWidget) => {
   const [curr, setCurr] = useState(0);
 
   useEffect(() => {
+    if (!video || video.length === 0) {
+      return;
+    }
     const url = `/v2/attachment/${video[curr].videoId}`;
     console.info("url", url);
     get<IAttachmentResponse>(url).then((json) => {
@@ -29,7 +32,7 @@ const WbwVideoButtonWidget = ({ video }: IWidget) => {
     });
   }, [curr, video]);
 
-  return video ? (
+  return video && video.length > 0 ? (
     <VideoModal src={url} type={video[0].type} trigger={<VideoIcon />} />
   ) : (
     <></>