visuddhinanda 1 year ago
parent
commit
874aedddf4

+ 76 - 0
dashboard/src/components/tag/TagCreate.tsx

@@ -0,0 +1,76 @@
+import { useIntl } from "react-intl";
+import {
+  ProForm,
+  ProFormInstance,
+  ProFormText,
+} from "@ant-design/pro-components";
+import { message } from "antd";
+
+import { post } from "../../request";
+import { useRef } from "react";
+import { ITagRequest, ITagResponse } from "../api/Tag";
+
+interface IWidgetCourseCreate {
+  studio?: string;
+  onCreate?: Function;
+}
+const TagCreateWidget = ({ studio = "", onCreate }: IWidgetCourseCreate) => {
+  const intl = useIntl();
+  const formRef = useRef<ProFormInstance>();
+
+  return (
+    <ProForm<ITagRequest>
+      formRef={formRef}
+      onFinish={async (values: ITagRequest) => {
+        console.log(values);
+        if (studio) {
+          values.studio = studio;
+          const url = `/v2/tag`;
+          console.info("CourseCreateWidget api request", url, values);
+          const res = await post<ITagRequest, ITagResponse>(url, values);
+          console.info("CourseCreateWidget api response", res);
+          if (res.ok) {
+            message.success(intl.formatMessage({ id: "flashes.success" }));
+            formRef.current?.resetFields(["title"]);
+            if (typeof onCreate !== "undefined") {
+              onCreate();
+            }
+          } else {
+            message.error(res.message);
+          }
+        } else {
+          console.error("no studio");
+        }
+      }}
+    >
+      <ProForm.Group>
+        <ProFormText
+          width="md"
+          name="name"
+          required
+          label={intl.formatMessage({ id: "forms.fields.name.label" })}
+          rules={[
+            {
+              max: 32,
+              min: 1,
+            },
+          ]}
+        />
+      </ProForm.Group>
+      <ProForm.Group>
+        <ProFormText
+          width="md"
+          name="description"
+          label={intl.formatMessage({ id: "forms.fields.description.label" })}
+          rules={[
+            {
+              max: 256,
+            },
+          ]}
+        />
+      </ProForm.Group>
+    </ProForm>
+  );
+};
+
+export default TagCreateWidget;

+ 138 - 0
dashboard/src/components/tag/TagList.tsx

@@ -0,0 +1,138 @@
+import { ActionType, ProList } from "@ant-design/pro-components";
+import { Button, Popover, Tag } from "antd";
+import { PlusOutlined } from "@ant-design/icons";
+
+import { ITagData, ITagResponseList } from "../../components/api/Tag";
+import { getSorterUrl } from "../../utils";
+import { get } from "../../request";
+import { useRef, useState } from "react";
+import TagCreate from "./TagCreate";
+import { useIntl } from "react-intl";
+
+interface IWidget {
+  studioName?: string;
+  onSelect?: Function;
+}
+
+const TagsList = ({ studioName, onSelect }: IWidget) => {
+  const intl = useIntl(); //i18n
+  const ref = useRef<ActionType>();
+  const [openCreate, setOpenCreate] = useState(false);
+  return (
+    <ProList<ITagData>
+      actionRef={ref}
+      toolBarRender={() => {
+        return [
+          <Popover
+            content={
+              <TagCreate
+                studio={studioName}
+                onCreate={() => {
+                  //新建课程成功后刷新
+
+                  ref.current?.reload();
+                  setOpenCreate(false);
+                }}
+              />
+            }
+            title="Create"
+            placement="bottomRight"
+            trigger="click"
+            open={openCreate}
+            onOpenChange={(newOpen: boolean) => {
+              setOpenCreate(newOpen);
+            }}
+          >
+            <Button key="button" icon={<PlusOutlined />} type="primary">
+              {intl.formatMessage({ id: "buttons.create" })}
+            </Button>
+          </Popover>,
+        ];
+      }}
+      search={{
+        filterType: "light",
+      }}
+      rowKey="name"
+      request={async (params = {}, sorter, filter) => {
+        console.log(params, sorter, filter);
+        let url = `/v2/tag?view=studio&name=${studioName}`;
+        const offset =
+          ((params.current ? params.current : 1) - 1) *
+          (params.pageSize ? params.pageSize : 20);
+        url += `&limit=${params.pageSize}&offset=${offset}`;
+        url += params.keyword ? "&search=" + params.keyword : "";
+
+        url += getSorterUrl(sorter);
+
+        console.info("api request", url);
+        const res = await get<ITagResponseList>(url);
+        console.info("api response", res);
+        return {
+          total: res.data.count,
+          succcess: true,
+          data: res.data.rows,
+        };
+      }}
+      pagination={{
+        pageSize: 10,
+      }}
+      options={{
+        search: true,
+      }}
+      metas={{
+        title: {
+          dataIndex: "name",
+          title: "用户",
+          search: false,
+          render(dom, entity, index, action, schema) {
+            return (
+              <Tag
+                color={"#" + entity.color.toString(16)}
+                onClick={() => {
+                  if (typeof onSelect !== "undefined") {
+                    onSelect(entity);
+                  }
+                }}
+              >
+                {entity.name}
+              </Tag>
+            );
+          },
+        },
+        subTitle: {
+          dataIndex: "description",
+          search: false,
+        },
+        actions: {
+          render: (text, row) => [
+            <Button>{"edit"}</Button>,
+            <Button danger>{"delete"}</Button>,
+          ],
+          search: false,
+        },
+        status: {
+          // 自己扩展的字段,主要用于筛选,不在列表中显示
+          title: "排序",
+          valueType: "select",
+          valueEnum: {
+            all: { text: "全部", status: "Default" },
+            open: {
+              text: "未解决",
+              status: "Error",
+            },
+            closed: {
+              text: "已解决",
+              status: "Success",
+            },
+            processing: {
+              text: "解决中",
+              status: "Processing",
+            },
+          },
+        },
+      }}
+    />
+  );
+};
+
+export default TagsList;

+ 49 - 0
dashboard/src/components/tag/TagSelect.tsx

@@ -0,0 +1,49 @@
+import { useState } from "react";
+import { Modal } from "antd";
+import TagList from "./TagList";
+import { ITagData } from "../api/Tag";
+
+interface IWidget {
+  studioName?: string;
+  trigger?: React.ReactNode;
+  onSelect?: Function;
+}
+const TagSelectWidget = ({ studioName, trigger, onSelect }: IWidget) => {
+  const [isModalOpen, setIsModalOpen] = useState(false);
+
+  const showModal = () => {
+    setIsModalOpen(true);
+  };
+
+  const handleOk = () => {
+    setIsModalOpen(false);
+  };
+
+  const handleCancel = () => {
+    setIsModalOpen(false);
+  };
+
+  return (
+    <>
+      <span onClick={showModal}>{trigger}</span>
+      <Modal
+        width={"70%"}
+        title="标签列表"
+        open={isModalOpen}
+        onOk={handleOk}
+        onCancel={handleCancel}
+      >
+        <TagList
+          studioName={studioName}
+          onSelect={(tag: ITagData) => {
+            if (typeof onSelect !== "undefined") {
+              onSelect(tag);
+            }
+          }}
+        />
+      </Modal>
+    </>
+  );
+};
+
+export default TagSelectWidget;

+ 79 - 0
dashboard/src/components/tag/TagSelectButton.tsx

@@ -0,0 +1,79 @@
+import { Button, message } from "antd";
+import { TagOutlined } from "@ant-design/icons";
+
+import TagSelect from "./TagSelect";
+import { ITagData, ITagMapRequest, ITagResponseList } from "../api/Tag";
+import { useAppSelector } from "../../hooks";
+import { courseInfo } from "../../reducers/current-course";
+import { currentUser } from "../../reducers/current-user";
+import { post } from "../../request";
+import { useIntl } from "react-intl";
+
+interface IWidget {
+  resId?: string;
+  resType?: string;
+  disabled?: boolean;
+  onSelect?: Function;
+  onCreate?: Function;
+}
+
+const TagSelectButtonWidget = ({
+  resId,
+  resType,
+  disabled = false,
+  onSelect,
+  onCreate,
+}: IWidget) => {
+  const intl = useIntl();
+  const course = useAppSelector(courseInfo);
+  const user = useAppSelector(currentUser);
+
+  const studioName =
+    course?.course?.studio?.realName ?? user?.nickName ?? undefined;
+
+  return (
+    <TagSelect
+      studioName={studioName}
+      trigger={
+        <Button disabled={disabled} type="text" icon={<TagOutlined />} />
+      }
+      onSelect={(tag: ITagData) => {
+        if (typeof onSelect !== "undefined") {
+          onSelect(tag);
+        } else {
+          if (studioName || course) {
+            const data: ITagMapRequest = {
+              table_name: resType,
+              anchor_id: resId,
+              tag_id: tag.id,
+              course: course ? course.courseId : undefined,
+              studio: studioName,
+            };
+
+            const url = `/v2/tag-map`;
+            console.info("tag-map  api request", url, data);
+            post<ITagMapRequest, ITagResponseList>(url, data)
+              .then((json) => {
+                console.info("tag-map api response", json);
+                if (json.ok) {
+                  message.success(
+                    intl.formatMessage({ id: "flashes.success" })
+                  );
+                  if (typeof onCreate !== "undefined") {
+                    onCreate(json.data.rows);
+                  }
+                } else {
+                  message.error(json.message);
+                }
+              })
+              .catch((e) => console.error(e));
+          } else {
+            console.error("no studio");
+          }
+        }
+      }}
+    />
+  );
+};
+
+export default TagSelectButtonWidget;

+ 98 - 0
dashboard/src/components/tag/TagShow.tsx

@@ -0,0 +1,98 @@
+import { ActionType, ProList } from "@ant-design/pro-components";
+import { Button } from "antd";
+
+import { ITagMapData, ITagMapResponseList } from "../api/Tag";
+import { getSorterUrl } from "../../utils";
+import { get } from "../../request";
+import { useRef } from "react";
+import { useIntl } from "react-intl";
+
+interface IWidget {
+  tagId?: string;
+  onSelect?: Function;
+}
+
+const TagsList = ({ tagId, onSelect }: IWidget) => {
+  const intl = useIntl(); //i18n
+  const ref = useRef<ActionType>();
+  const pageSize = 10;
+
+  return (
+    <ProList<ITagMapData>
+      actionRef={ref}
+      search={{
+        filterType: "light",
+      }}
+      rowKey="name"
+      request={async (params = {}, sorter, filter) => {
+        console.log(params, sorter, filter);
+        let url = `/v2/tag-map?view=items&tag_id=${tagId}`;
+        const offset =
+          ((params.current ? params.current : 1) - 1) *
+          (params.pageSize ? params.pageSize : pageSize);
+        url += `&limit=${params.pageSize}&offset=${offset}`;
+        url += params.keyword ? "&search=" + params.keyword : "";
+
+        url += getSorterUrl(sorter);
+
+        console.info("api request", url);
+        const res = await get<ITagMapResponseList>(url);
+        console.info("api response", res);
+        return {
+          total: res.data.count,
+          succcess: true,
+          data: res.data.rows,
+        };
+      }}
+      pagination={{
+        pageSize: pageSize,
+      }}
+      options={{
+        search: true,
+      }}
+      metas={{
+        title: {
+          dataIndex: "title",
+          title: "title",
+          search: false,
+          render(dom, entity, index, action, schema) {
+            return <>{entity.title}</>;
+          },
+        },
+        subTitle: {
+          dataIndex: "description",
+          search: false,
+        },
+        actions: {
+          render: (text, row) => [
+            <Button>{"edit"}</Button>,
+            <Button danger>{"delete"}</Button>,
+          ],
+          search: false,
+        },
+        status: {
+          // 自己扩展的字段,主要用于筛选,不在列表中显示
+          title: "排序",
+          valueType: "select",
+          valueEnum: {
+            all: { text: "全部", status: "Default" },
+            open: {
+              text: "未解决",
+              status: "Error",
+            },
+            closed: {
+              text: "已解决",
+              status: "Success",
+            },
+            processing: {
+              text: "解决中",
+              status: "Processing",
+            },
+          },
+        },
+      }}
+    />
+  );
+};
+
+export default TagsList;

+ 21 - 0
dashboard/src/components/tag/TagsArea.tsx

@@ -0,0 +1,21 @@
+import { Tag } from "antd";
+import { ITagData } from "../api/Tag";
+
+interface IWidget {
+  data?: ITagData[];
+  max?: number;
+  onTagClose?: Function;
+  onTagClick?: Function;
+}
+const TagsAreaWidget = ({ data, max = 5, onTagClose, onTagClick }: IWidget) => {
+  const tags = data?.map((item, id) => {
+    return id < max ? (
+      <Tag key={id} closable onClose={() => {}}>
+        {item.name}
+      </Tag>
+    ) : undefined;
+  });
+  return <div style={{ width: "100%", lineHeight: "2em" }}>{tags}</div>;
+};
+
+export default TagsAreaWidget;

+ 22 - 0
dashboard/src/pages/studio/tags/index.tsx

@@ -0,0 +1,22 @@
+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>
+      <Layout>
+        <LeftSider selectedKeys="tag" />
+        <Content style={styleStudioContent}>
+          <Outlet />
+        </Content>
+      </Layout>
+    </Layout>
+  );
+};
+
+export default Widget;

+ 20 - 0
dashboard/src/pages/studio/tags/list.tsx

@@ -0,0 +1,20 @@
+import { useNavigate, useParams } from "react-router-dom";
+
+import TagList from "../../../components/tag/TagList";
+import { ITagData } from "../../../components/api/Tag";
+
+const Widget = () => {
+  const { studioname } = useParams();
+  const navigate = useNavigate();
+  return (
+    <TagList
+      studioName={studioname}
+      onSelect={(tag: ITagData) => {
+        const url = `/studio/${studioname}/tags/${tag.id}/list`;
+        navigate(url);
+      }}
+    />
+  );
+};
+
+export default Widget;

+ 21 - 0
dashboard/src/pages/studio/tags/show.tsx

@@ -0,0 +1,21 @@
+import { useNavigate, useParams } from "react-router-dom";
+
+import TagList from "../../../components/tag/TagList";
+import { ITagData } from "../../../components/api/Tag";
+import TagShow from "../../../components/tag/TagShow";
+
+const Widget = () => {
+  const { studioname, id } = useParams();
+  const navigate = useNavigate();
+  return (
+    <TagShow
+      tagId={id}
+      onSelect={(tag: ITagData) => {
+        const url = `/studio/${studioname}/tags/${tag.id}/list`;
+        navigate(url);
+      }}
+    />
+  );
+};
+
+export default Widget;