Просмотр исходного кода

Merge pull request #2109 from visuddhinanda/agile

添加标签支持
visuddhinanda 1 год назад
Родитель
Сommit
0e277a0a76

+ 2 - 0
dashboard/src/Router.tsx

@@ -142,6 +142,7 @@ import StudioInviteList from "./pages/studio/invite/list";
 import StudioTag from "./pages/studio/tags";
 import StudioTagList from "./pages/studio/tags/list";
 import StudioTagShow from "./pages/studio/tags/show";
+import StudioTagEdit from "./pages/studio/tags/edit";
 
 import { ConfigProvider } from "antd";
 import { useAppSelector } from "./hooks";
@@ -364,6 +365,7 @@ const Widget = () => {
           <Route path="tags" element={<StudioTag />}>
             <Route path="list" element={<StudioTagList />} />
             <Route path=":id/list" element={<StudioTagShow />} />
+            <Route path=":tagId/edit" element={<StudioTagEdit />} />
           </Route>
 
           <Route path="transfer" element={<StudioTransfer />}>

+ 2 - 1
dashboard/src/components/api/Comment.ts

@@ -2,6 +2,7 @@ import { IUser } from "../auth/User";
 import { TDiscussionType } from "../discussion/Discussion";
 import { TContentType } from "../discussion/DiscussionCreate";
 import { TResType } from "../discussion/DiscussionListCard";
+import { ITagMapData } from "./Tag";
 
 export interface ICommentRequest {
   id?: string;
@@ -83,5 +84,5 @@ export interface IDiscussionCountData {
 export interface IDiscussionCountResponse {
   ok: boolean;
   message: string;
-  data: IDiscussionCountData[];
+  data: { discussions: IDiscussionCountData[]; tags: ITagMapData[] };
 }

+ 7 - 5
dashboard/src/components/api/Tag.ts

@@ -22,7 +22,7 @@ export interface ITagData {
   name: string;
   description?: string | null;
   color: number;
-  owner: IStudio;
+  owner?: IStudio;
   created_at: string;
   updated_at: string;
 }
@@ -53,11 +53,13 @@ export interface ITagMapData {
   table_name: string;
   anchor_id: string;
   tag_id: string;
+  name?: string;
+  color?: number;
   title?: string;
-  editor: IUser;
-  owner: IStudio;
-  created_at: string;
-  updated_at: string;
+  editor?: IUser;
+  owner?: IStudio;
+  created_at?: string;
+  updated_at?: string;
 }
 
 export interface ITagMapResponse {

+ 1 - 1
dashboard/src/components/course/RolePower.ts

@@ -373,7 +373,7 @@ const managerData: IAction[] = [
     mode: ["manual", "invite"],
     status: "applied",
     before: ["accept", "reject"],
-    duration: [],
+    duration: ["accept", "reject"],
     after: [],
   },
   {

+ 4 - 3
dashboard/src/components/discussion/DiscussionCount.tsx

@@ -1,6 +1,6 @@
 import { useEffect } from "react";
 import { useAppSelector } from "../../hooks";
-import { publish, upgrade } from "../../reducers/discussion-count";
+import { discussions, tags, upgrade } from "../../reducers/discussion-count";
 import { sentenceList } from "../../reducers/sentence";
 import {
   IDiscussionCountRequest,
@@ -18,7 +18,7 @@ export const discussionCountUpgrade = (resId?: string) => {
   get<IDiscussionCountResponse>(url).then((json) => {
     console.debug("discussion-count api response", json);
     if (json.ok) {
-      store.dispatch(upgrade({ resId: resId, data: json.data }));
+      store.dispatch(upgrade({ resId: resId, data: json.data.discussions }));
     } else {
       console.error(json.message);
     }
@@ -51,7 +51,8 @@ const DiscussionCount = ({ courseId }: IWidget) => {
       (json) => {
         console.debug("discussion-count api response", json);
         if (json.ok) {
-          store.dispatch(publish(json.data));
+          store.dispatch(discussions(json.data.discussions));
+          store.dispatch(tags(json.data.tags));
         }
       }
     );

+ 60 - 5
dashboard/src/components/tag/TagCreate.tsx

@@ -2,22 +2,49 @@ import { useIntl } from "react-intl";
 import {
   ProForm,
   ProFormInstance,
+  ProFormSelect,
   ProFormText,
 } from "@ant-design/pro-components";
-import { message } from "antd";
+import { Tag, message } from "antd";
 
-import { post } from "../../request";
+import { get, post, put } from "../../request";
 import { useRef } from "react";
 import { ITagRequest, ITagResponse } from "../api/Tag";
 
 interface IWidgetCourseCreate {
   studio?: string;
+  tagId?: string;
   onCreate?: Function;
 }
-const TagCreateWidget = ({ studio = "", onCreate }: IWidgetCourseCreate) => {
+const TagCreateWidget = ({ studio, tagId, onCreate }: IWidgetCourseCreate) => {
   const intl = useIntl();
   const formRef = useRef<ProFormInstance>();
 
+  const _color = [
+    "b60205",
+    "d93f0b",
+    "fbca04",
+    "0e8a16",
+    "006b75",
+    "1d76db",
+    "0052cc",
+    "5319e7",
+    "e99695",
+    "f9d0c4",
+    "fef2c0",
+    "c2e0c6",
+    "bfdadc",
+    "c5def5",
+    "bfd4f2",
+    "d4c5f9",
+  ];
+  const colorOptions = _color.map((item) => {
+    return {
+      value: parseInt(item, 16),
+      label: <Tag color={`#${item}`}>{item}</Tag>,
+    };
+  });
+
   return (
     <ProForm<ITagRequest>
       formRef={formRef}
@@ -25,9 +52,18 @@ const TagCreateWidget = ({ studio = "", onCreate }: IWidgetCourseCreate) => {
         console.log(values);
         if (studio) {
           values.studio = studio;
-          const url = `/v2/tag`;
+          let url = `/v2/tag`;
+          if (tagId) {
+            url += `/${tagId}`;
+          }
           console.info("CourseCreateWidget api request", url, values);
-          const res = await post<ITagRequest, ITagResponse>(url, values);
+          let res: any;
+          if (tagId) {
+            res = await put<ITagRequest, ITagResponse>(url, values);
+          } else {
+            res = await post<ITagRequest, ITagResponse>(url, values);
+          }
+
           console.info("CourseCreateWidget api response", res);
           if (res.ok) {
             message.success(intl.formatMessage({ id: "flashes.success" }));
@@ -42,6 +78,17 @@ const TagCreateWidget = ({ studio = "", onCreate }: IWidgetCourseCreate) => {
           console.error("no studio");
         }
       }}
+      request={
+        tagId
+          ? async () => {
+              const url = `/v2/tag/${tagId}`;
+              console.info("api request", url);
+              const res = await get<ITagResponse>(url);
+              console.info("api response", res);
+              return res.data;
+            }
+          : undefined
+      }
     >
       <ProForm.Group>
         <ProFormText
@@ -69,6 +116,14 @@ const TagCreateWidget = ({ studio = "", onCreate }: IWidgetCourseCreate) => {
           ]}
         />
       </ProForm.Group>
+      <ProForm.Group>
+        <ProFormSelect
+          width="md"
+          name="color"
+          label={intl.formatMessage({ id: "forms.fields.color.label" })}
+          options={colorOptions}
+        />
+      </ProForm.Group>
     </ProForm>
   );
 };

+ 65 - 33
dashboard/src/components/tag/TagList.tsx

@@ -8,13 +8,35 @@ import { get } from "../../request";
 import { useRef, useState } from "react";
 import TagCreate from "./TagCreate";
 import { useIntl } from "react-intl";
+import { Link } from "react-router-dom";
+
+/**
+ * 10进制数字转为16进制字符串
+ * @param {number} arg
+ * @returns
+ */
+/*
+作者:sq800
+链接:https://juejin.cn/post/7250029395024281656
+来源:稀土掘金
+著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
+*/
+export const numToHex = (arg: number) => {
+  try {
+    let a = arg.toString(16).toUpperCase();
+    return a.length % 2 === 1 ? "0" + a : a;
+  } catch (e) {
+    console.warn("数字转16进制出错:", e);
+  }
+};
 
 interface IWidget {
   studioName?: string;
+  readonly?: boolean;
   onSelect?: Function;
 }
 
-const TagsList = ({ studioName, onSelect }: IWidget) => {
+const TagsList = ({ studioName, readonly = false, onSelect }: IWidget) => {
   const intl = useIntl(); //i18n
   const ref = useRef<ActionType>();
   const [openCreate, setOpenCreate] = useState(false);
@@ -22,32 +44,38 @@ const TagsList = ({ studioName, onSelect }: IWidget) => {
     <ProList<ITagData>
       actionRef={ref}
       toolBarRender={() => {
-        return [
-          <Popover
-            content={
-              <TagCreate
-                studio={studioName}
-                onCreate={() => {
-                  //新建课程成功后刷新
+        return readonly
+          ? [
+              <Link to={`/studio/${studioName}/tags/list`} target="_blank">
+                {intl.formatMessage({ id: "buttons.manage" })}
+              </Link>,
+            ]
+          : [
+              <Popover
+                content={
+                  <TagCreate
+                    studio={studioName}
+                    onCreate={() => {
+                      //新建课程成功后刷新
 
-                  ref.current?.reload();
-                  setOpenCreate(false);
+                      ref.current?.reload();
+                      setOpenCreate(false);
+                    }}
+                  />
+                }
+                title="Create"
+                placement="bottomRight"
+                trigger="click"
+                open={openCreate}
+                onOpenChange={(newOpen: boolean) => {
+                  setOpenCreate(newOpen);
                 }}
-              />
-            }
-            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>,
-        ];
+              >
+                <Button key="button" icon={<PlusOutlined />} type="primary">
+                  {intl.formatMessage({ id: "buttons.create" })}
+                </Button>
+              </Popover>,
+            ];
       }}
       search={{
         filterType: "light",
@@ -87,7 +115,7 @@ const TagsList = ({ studioName, onSelect }: IWidget) => {
           render(dom, entity, index, action, schema) {
             return (
               <Tag
-                color={"#" + entity.color.toString(16)}
+                color={"#" + numToHex(entity.color ?? 13684944)}
                 onClick={() => {
                   if (typeof onSelect !== "undefined") {
                     onSelect(entity);
@@ -103,13 +131,17 @@ const TagsList = ({ studioName, onSelect }: IWidget) => {
           dataIndex: "description",
           search: false,
         },
-        actions: {
-          render: (text, row) => [
-            <Button>{"edit"}</Button>,
-            <Button danger>{"delete"}</Button>,
-          ],
-          search: false,
-        },
+        actions: readonly
+          ? undefined
+          : {
+              render: (dom, entity, index, action, schema) => [
+                <Link to={`/studio/${studioName}/tags/${entity.id}/edit`}>
+                  {"edit"}
+                </Link>,
+                <Button danger>{"delete"}</Button>,
+              ],
+              search: false,
+            },
         status: {
           // 自己扩展的字段,主要用于筛选,不在列表中显示
           title: "排序",

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

@@ -34,6 +34,7 @@ const TagSelectWidget = ({ studioName, trigger, onSelect }: IWidget) => {
         onCancel={handleCancel}
       >
         <TagList
+          readonly
           studioName={studioName}
           onSelect={(tag: ITagData) => {
             if (typeof onSelect !== "undefined") {

+ 17 - 3
dashboard/src/components/tag/TagSelectButton.tsx

@@ -2,7 +2,7 @@ import { Button, message } from "antd";
 import { TagOutlined } from "@ant-design/icons";
 
 import TagSelect from "./TagSelect";
-import { ITagData, ITagMapRequest, ITagResponseList } from "../api/Tag";
+import { ITagData, ITagMapRequest, ITagMapResponseList } from "../api/Tag";
 import { useAppSelector } from "../../hooks";
 import { courseInfo } from "../../reducers/current-course";
 import { currentUser } from "../../reducers/current-user";
@@ -15,6 +15,7 @@ interface IWidget {
   disabled?: boolean;
   onSelect?: Function;
   onCreate?: Function;
+  onOpen?: Function;
 }
 
 const TagSelectButtonWidget = ({
@@ -23,6 +24,7 @@ const TagSelectButtonWidget = ({
   disabled = false,
   onSelect,
   onCreate,
+  onOpen,
 }: IWidget) => {
   const intl = useIntl();
   const course = useAppSelector(courseInfo);
@@ -35,7 +37,19 @@ const TagSelectButtonWidget = ({
     <TagSelect
       studioName={studioName}
       trigger={
-        <Button disabled={disabled} type="text" icon={<TagOutlined />} />
+        <Button
+          disabled={disabled}
+          type="text"
+          icon={
+            <TagOutlined
+              onClick={() => {
+                if (typeof onOpen !== "undefined") {
+                  onOpen();
+                }
+              }}
+            />
+          }
+        />
       }
       onSelect={(tag: ITagData) => {
         if (typeof onSelect !== "undefined") {
@@ -52,7 +66,7 @@ const TagSelectButtonWidget = ({
 
             const url = `/v2/tag-map`;
             console.info("tag-map  api request", url, data);
-            post<ITagMapRequest, ITagResponseList>(url, data)
+            post<ITagMapRequest, ITagMapResponseList>(url, data)
               .then((json) => {
                 console.info("tag-map api response", json);
                 if (json.ok) {

+ 30 - 6
dashboard/src/components/tag/TagsArea.tsx

@@ -1,21 +1,45 @@
 import { Tag } from "antd";
-import { ITagData } from "../api/Tag";
+import { ITagMapData } from "../api/Tag";
+import { useEffect, useState } from "react";
+import { useAppSelector } from "../../hooks";
+import { tagList } from "../../reducers/discussion-count";
+import { numToHex } from "./TagList";
 
 interface IWidget {
-  data?: ITagData[];
+  data?: ITagMapData[];
   max?: number;
+  resId?: string;
   onTagClose?: Function;
   onTagClick?: Function;
 }
-const TagsAreaWidget = ({ data, max = 5, onTagClose, onTagClick }: IWidget) => {
-  const tags = data?.map((item, id) => {
+const TagsAreaWidget = ({
+  data = [],
+  max = 5,
+  resId,
+  onTagClose,
+  onTagClick,
+}: IWidget) => {
+  const [tags, setTags] = useState<ITagMapData[]>();
+
+  const tagMapList = useAppSelector(tagList);
+
+  useEffect(() => {
+    if (tagMapList) {
+      const currTags = tagMapList.filter((value) => value.anchor_id === resId);
+      if (currTags) {
+        setTags(currTags);
+      }
+    }
+  }, [resId, tagMapList]);
+
+  const currTags = tags?.map((item, id) => {
     return id < max ? (
-      <Tag key={id} closable onClose={() => {}}>
+      <Tag key={id} color={"#" + numToHex(item.color ?? 13684944)}>
         {item.name}
       </Tag>
     ) : undefined;
   });
-  return <div style={{ width: "100%", lineHeight: "2em" }}>{tags}</div>;
+  return <div style={{ width: "100%", lineHeight: "2em" }}>{currTags}</div>;
 };
 
 export default TagsAreaWidget;

+ 7 - 3
dashboard/src/components/template/Wbw/WbwDetail.tsx

@@ -32,7 +32,7 @@ import { tempSet } from "../../../reducers/setting";
 import { PopPlacement } from "./WbwPali";
 import store from "../../../store";
 import TagSelectButton from "../../tag/TagSelectButton";
-import { ITagData, ITagMapData } from "../../api/Tag";
+import { ITagMapData } from "../../api/Tag";
 
 interface IWidget {
   data: IWbw;
@@ -175,8 +175,12 @@ const WbwDetailWidget = ({
             <TagSelectButton
               resType="wbw"
               resId={data.uid}
-              disabled={true}
-              onCreate={(tags: ITagData[]) => {
+              onOpen={() => {
+                if (typeof onClose !== "undefined") {
+                  onClose();
+                }
+              }}
+              onCreate={(tags: ITagMapData[]) => {
                 if (typeof onTagCreate !== "undefined") {
                   onTagCreate(tags);
                 }

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

@@ -26,7 +26,7 @@ import WbwPaliDiscussionIcon from "./WbwPaliDiscussionIcon";
 import { TooltipPlacement } from "antd/lib/tooltip";
 import { temp } from "../../../reducers/setting";
 import TagsArea from "../../tag/TagsArea";
-import { ITagData } from "../../api/Tag";
+import { ITagMapData } from "../../api/Tag";
 
 export const PopPlacement = "setting.wbw.pop.placement";
 
@@ -74,7 +74,7 @@ const WbwPaliWidget = ({
 }: IWidget) => {
   const [popOpen, setPopOpen] = useState(false);
   const [popOnTop, setPopOnTop] = useState(false);
-  const [tags, setTags] = useState<ITagData[]>();
+  const [tags, setTags] = useState<ITagMapData[]>();
 
   const [paliColor, setPaliColor] = useState("unset");
   const divShell = useRef<HTMLDivElement>(null);
@@ -230,7 +230,7 @@ const WbwPaliWidget = ({
         );
         */
       }}
-      onTagCreate={(tags: ITagData[]) => {
+      onTagCreate={(tags: ITagMapData[]) => {
         setTags(tags);
       }}
     />
@@ -356,7 +356,7 @@ const WbwPaliWidget = ({
     return (
       <div className="pali_shell" ref={divShell}>
         <div style={{ position: "absolute", marginTop: -24 }}>
-          <TagsArea data={tags} max={1} />
+          <TagsArea resId={data.uid} data={tags} max={1} />
         </div>
         <span className="pali_shell_spell">
           {data.grammarId ? (

+ 1 - 0
dashboard/src/locales/en-US/forms.ts

@@ -89,6 +89,7 @@ const items = {
   "forms.status.refuse.label": "refused",
   "forms.status.cancel.label": "canceled",
   "forms.fields.sign-up-message.label": "sign up message",
+  "forms.fields.color.label": "color",
 };
 
 export default items;

+ 1 - 0
dashboard/src/locales/en-US/label.ts

@@ -46,6 +46,7 @@ const items = {
   "labels.email.sign-up.subject": "welcome join wikipali",
   "labels.sign-up": "Sign Up",
   "labels.done": "Done",
+  "labels.tag.list": "tag list",
 };
 
 export default items;

+ 1 - 0
dashboard/src/locales/zh-Hans/forms.ts

@@ -89,6 +89,7 @@ const items = {
   "forms.status.refuse.label": "已经拒绝",
   "forms.status.cancel.label": "已经撤回",
   "forms.fields.sign-up-message.label": "报名消息",
+  "forms.fields.color.label": "颜色",
 };
 
 export default items;

+ 1 - 0
dashboard/src/locales/zh-Hans/label.ts

@@ -51,6 +51,7 @@ const items = {
   "labels.email.sign-up.subject": "欢迎注册wikipāli",
   "labels.sign-up": "注册",
   "labels.done": "完成",
+  "labels.tag.list": "标签列表",
 };
 
 export default items;

+ 26 - 0
dashboard/src/pages/studio/tags/edit.tsx

@@ -0,0 +1,26 @@
+import { useIntl } from "react-intl";
+import { useParams } from "react-router-dom";
+import { Card } from "antd";
+
+import GoBack from "../../../components/studio/GoBack";
+import TagCreate from "../../../components/tag/TagCreate";
+
+const Widget = () => {
+  const intl = useIntl();
+  const { studioname, tagId } = useParams(); //url 参数
+
+  return (
+    <Card
+      title={
+        <GoBack
+          to={`/studio/${studioname}/tags/list`}
+          title={intl.formatMessage({ id: "labels.tag.list" })}
+        />
+      }
+    >
+      <TagCreate studio={studioname} tagId={tagId} />
+    </Card>
+  );
+};
+
+export default Widget;

+ 12 - 2
dashboard/src/reducers/discussion-count.ts

@@ -5,14 +5,17 @@ import { createSlice, PayloadAction } from "@reduxjs/toolkit";
 
 import type { RootState } from "../store";
 import { IDiscussionCountData } from "../components/api/Comment";
+import { ITagMapData } from "../components/api/Tag";
 
 export interface IUpgrade {
   resId: string;
   data: IDiscussionCountData[];
+  tags?: ITagMapData[];
 }
 
 interface IState {
   list: IDiscussionCountData[];
+  tags?: ITagMapData[];
 }
 
 const initialState: IState = { list: [] };
@@ -21,10 +24,14 @@ export const slice = createSlice({
   name: "discussion-count",
   initialState,
   reducers: {
-    publish: (state, action: PayloadAction<IDiscussionCountData[]>) => {
+    discussions: (state, action: PayloadAction<IDiscussionCountData[]>) => {
       console.debug("discussion-count publish", action.payload);
       state.list = action.payload;
     },
+    tags: (state, action: PayloadAction<ITagMapData[]>) => {
+      console.debug("discussion-count publish", action.payload);
+      state.tags = action.payload;
+    },
     upgrade: (state, action: PayloadAction<IUpgrade>) => {
       console.debug("discussion-count publish", action.payload);
       const old = state.list.filter(
@@ -35,10 +42,13 @@ export const slice = createSlice({
   },
 });
 
-export const { publish, upgrade } = slice.actions;
+export const { discussions, tags, upgrade } = slice.actions;
 
 export const discussionList = (
   state: RootState
 ): IDiscussionCountData[] | undefined => state.discussionCount.list;
 
+export const tagList = (state: RootState): ITagMapData[] | undefined =>
+  state.discussionCount.tags;
+
 export default slice.reducer;