visuddhinanda 3 лет назад
Родитель
Сommit
42b6656c61

+ 28 - 9
dashboard/src/components/anthology/AnthologyList.tsx

@@ -8,6 +8,7 @@ import {
   ExclamationCircleOutlined,
   TeamOutlined,
   DeleteOutlined,
+  EyeOutlined,
 } from "@ant-design/icons";
 
 import AnthologyCreate from "../../components/anthology/AnthologyCreate";
@@ -18,6 +19,8 @@ import {
 import { delete_, get } from "../../request";
 import { PublicityValueEnum } from "../../components/studio/table";
 import { useRef, useState } from "react";
+import ShareModal from "../share/ShareModal";
+import { EResType } from "../share/Share";
 
 const { Text } = Typography;
 
@@ -31,15 +34,19 @@ interface IItem {
   createdAt: number;
 }
 interface IWidget {
+  title?: string;
   studioName?: string;
   showCol?: string[];
   showCreate?: boolean;
+  showOption?: boolean;
   onTitleClick?: Function;
 }
 const Widget = ({
+  title,
   studioName,
   showCol,
   showCreate = true,
+  showOption = true,
   onTitleClick,
 }: IWidget) => {
   const intl = useIntl();
@@ -83,6 +90,7 @@ const Widget = ({
   return (
     <>
       <ProTable<IItem>
+        headerTitle={title}
         actionRef={ref}
         columns={[
           {
@@ -158,28 +166,38 @@ const Widget = ({
             title: intl.formatMessage({ id: "buttons.option" }),
             key: "option",
             width: 120,
+            hideInTable: !showOption,
             valueType: "option",
             render: (text, row, index, action) => [
               <Dropdown.Button
                 key={index}
                 type="link"
+                trigger={["click", "contextMenu"]}
                 menu={{
                   items: [
                     {
                       key: "open",
-                      label: intl.formatMessage({
-                        id: "buttons.open.in.library",
-                      }),
-                      icon: <TeamOutlined />,
-                      disabled: true,
+                      label: (
+                        <Link to={`/anthology/${row.id}`}>
+                          {intl.formatMessage({
+                            id: "buttons.open.in.library",
+                          })}
+                        </Link>
+                      ),
+                      icon: <EyeOutlined />,
                     },
                     {
                       key: "share",
-                      label: intl.formatMessage({
-                        id: "buttons.share",
-                      }),
+                      label: (
+                        <ShareModal
+                          trigger={intl.formatMessage({
+                            id: "buttons.share",
+                          })}
+                          resId={row.id}
+                          resType={EResType.collection}
+                        />
+                      ),
                       icon: <TeamOutlined />,
-                      disabled: true,
                     },
                     {
                       key: "remove",
@@ -259,6 +277,7 @@ const Widget = ({
         pagination={{
           showQuickJumper: true,
           showSizeChanger: true,
+          pageSize: 10,
         }}
         search={false}
         options={{

+ 17 - 1
dashboard/src/pages/studio/anthology/edit.tsx

@@ -1,11 +1,14 @@
 import { useState } from "react";
 import { Link, useParams } from "react-router-dom";
 import { useIntl } from "react-intl";
-import { Card, Space, Tabs } from "antd";
+import { Button, Card, Space, Tabs } from "antd";
+import { TeamOutlined } from "@ant-design/icons";
 
 import GoBack from "../../../components/studio/GoBack";
 import TocTree from "../../../components/anthology/EditableTocTree";
 import AnthologyInfoEdit from "../../../components/article/AnthologyInfoEdit";
+import ShareModal from "../../../components/share/ShareModal";
+import { EResType } from "../../../components/share/Share";
 
 const Widget = () => {
   const intl = useIntl();
@@ -20,6 +23,19 @@ const Widget = () => {
         }
         extra={
           <Space>
+            {anthology_id ? (
+              <ShareModal
+                trigger={
+                  <Button icon={<TeamOutlined />}>
+                    {intl.formatMessage({
+                      id: "buttons.share",
+                    })}
+                  </Button>
+                }
+                resId={anthology_id}
+                resType={EResType.collection}
+              />
+            ) : undefined}
             <Link to={`/anthology/${anthology_id}`} target="_blank">
               {intl.formatMessage({ id: "buttons.open.in.library" })}
             </Link>

+ 166 - 113
dashboard/src/pages/studio/article/edit.tsx

@@ -6,7 +6,8 @@ import {
   ProFormText,
   ProFormTextArea,
 } from "@ant-design/pro-components";
-import { Button, Card, Form, message, Space, Tabs } from "antd";
+import { TeamOutlined } from "@ant-design/icons";
+import { Button, Card, Form, message, Result, Space, Tabs } from "antd";
 
 import { get, put } from "../../../request";
 import {
@@ -18,6 +19,10 @@ import PublicitySelect from "../../../components/studio/PublicitySelect";
 import GoBack from "../../../components/studio/GoBack";
 import MDEditor from "@uiw/react-md-editor";
 import ArticleTplMaker from "../../../components/article/ArticleTplMaker";
+import ShareModal from "../../../components/share/ShareModal";
+import { EResType } from "../../../components/share/Share";
+import AddToAnthology from "../../../components/article/AddToAnthology";
+import ReadonlyLabel from "../../../components/general/ReadonlyLabel";
 
 interface IFormData {
   uid: string;
@@ -34,12 +39,35 @@ const Widget = () => {
   const intl = useIntl();
   const { studioname, articleid } = useParams(); //url 参数
   const [title, setTitle] = useState("loading");
+  const [unauthorized, setUnauthorized] = useState(false);
+  const [readonly, setReadonly] = useState(false);
 
   return (
     <Card
-      title={<GoBack to={`/studio/${studioname}/article/list`} title={title} />}
+      title={
+        <Space>
+          <GoBack to={`/studio/${studioname}/article/list`} title={title} />
+          {readonly ? <ReadonlyLabel /> : undefined}
+        </Space>
+      }
       extra={
         <Space>
+          {articleid ? (
+            <AddToAnthology studioName={studioname} articleIds={[articleid]} />
+          ) : undefined}
+          {articleid ? (
+            <ShareModal
+              trigger={
+                <Button icon={<TeamOutlined />}>
+                  {intl.formatMessage({
+                    id: "buttons.share",
+                  })}
+                </Button>
+              }
+              resId={articleid}
+              resType={EResType.article}
+            />
+          ) : undefined}
           <Link to={`/article/article/${articleid}`} target="_blank">
             {intl.formatMessage({ id: "buttons.open.in.library" })}
           </Link>
@@ -52,120 +80,145 @@ const Widget = () => {
         </Space>
       }
     >
-      <ProForm<IFormData>
-        onFinish={async (values: IFormData) => {
-          // TODO
+      {unauthorized ? (
+        <Result
+          status="403"
+          title="无权访问"
+          subTitle="您无权访问该内容。您可能没有登录,或者内容的所有者没有给您所需的权限。"
+          extra={<></>}
+        />
+      ) : (
+        <ProForm<IFormData>
+          onFinish={async (values: IFormData) => {
+            // TODO
 
-          const request = {
-            uid: articleid ? articleid : "",
-            title: values.title,
-            subtitle: values.subtitle,
-            summary: values.summary,
-            content: values.content,
-            content_type: "markdown",
-            status: values.status,
-            lang: values.lang,
-          };
-          console.log(request);
-          const res = await put<IArticleDataRequest, IArticleResponse>(
-            `/v2/article/${articleid}`,
-            request
-          );
-          console.log(res);
-          if (res.ok) {
-            message.success(intl.formatMessage({ id: "flashes.success" }));
-          } else {
-            message.error(res.message);
-          }
-        }}
-        request={async () => {
-          const res = await get<IArticleResponse>(`/v2/article/${articleid}`);
-          setTitle(res.data.title);
-          return {
-            uid: res.data.uid,
-            title: res.data.title,
-            subtitle: res.data.subtitle,
-            summary: res.data.summary,
-            content: res.data.content,
-            content_type: res.data.content_type,
-            lang: res.data.lang,
-            status: res.data.status,
-          };
-        }}
-      >
-        <Tabs
-          items={[
-            {
-              key: "info",
-              label: intl.formatMessage({ id: "course.basic.info.label" }),
-              children: (
-                <>
-                  <ProForm.Group>
-                    <ProFormText
-                      width="md"
-                      name="title"
-                      required
-                      label={intl.formatMessage({
-                        id: "forms.fields.title.label",
-                      })}
-                      rules={[
-                        {
-                          required: true,
-                          message: intl.formatMessage({
-                            id: "forms.message.title.required",
-                          }),
-                        },
-                      ]}
-                    />
-                    <ProFormText
-                      width="md"
-                      name="subtitle"
-                      label={intl.formatMessage({
-                        id: "forms.fields.subtitle.label",
-                      })}
-                    />
-                  </ProForm.Group>
-                  <ProForm.Group>
-                    <LangSelect width="md" />
-                    <PublicitySelect width="md" />
-                  </ProForm.Group>
+            const request = {
+              uid: articleid ? articleid : "",
+              title: values.title,
+              subtitle: values.subtitle,
+              summary: values.summary,
+              content: values.content,
+              content_type: "markdown",
+              status: values.status,
+              lang: values.lang,
+            };
+            console.log("save", request);
+            put<IArticleDataRequest, IArticleResponse>(
+              `/v2/article/${articleid}`,
+              request
+            )
+              .then((res) => {
+                console.log("save response", res);
+                if (res.ok) {
+                  message.success(
+                    intl.formatMessage({ id: "flashes.success" })
+                  );
+                } else {
+                  message.error(res.message);
+                }
+              })
+              .catch((e: IArticleResponse) => {
+                message.error(e.message);
+              });
+          }}
+          request={async () => {
+            const res = await get<IArticleResponse>(`/v2/article/${articleid}`);
+            console.log("article", res);
+            if (res.ok) {
+              const readonly = res.data.role === "editor" ? false : true;
+              setReadonly(readonly);
+              setTitle(res.data.title);
+            } else {
+              setUnauthorized(true);
+              setTitle("无权访问");
+            }
+
+            return {
+              uid: res.data.uid,
+              title: res.data.title,
+              subtitle: res.data.subtitle,
+              summary: res.data.summary,
+              content: res.data.content,
+              content_type: res.data.content_type,
+              lang: res.data.lang,
+              status: res.data.status,
+            };
+          }}
+        >
+          <Tabs
+            items={[
+              {
+                key: "info",
+                label: intl.formatMessage({ id: "course.basic.info.label" }),
+                children: (
+                  <>
+                    <ProForm.Group>
+                      <ProFormText
+                        width="md"
+                        name="title"
+                        required
+                        label={intl.formatMessage({
+                          id: "forms.fields.title.label",
+                        })}
+                        rules={[
+                          {
+                            required: true,
+                            message: intl.formatMessage({
+                              id: "forms.message.title.required",
+                            }),
+                          },
+                        ]}
+                      />
+                      <ProFormText
+                        width="md"
+                        name="subtitle"
+                        label={intl.formatMessage({
+                          id: "forms.fields.subtitle.label",
+                        })}
+                      />
+                    </ProForm.Group>
+                    <ProForm.Group>
+                      <LangSelect width="md" />
+                      <PublicitySelect width="md" />
+                    </ProForm.Group>
+                    <ProForm.Group>
+                      <ProFormTextArea
+                        name="summary"
+                        width="lg"
+                        label={intl.formatMessage({
+                          id: "forms.fields.summary.label",
+                        })}
+                      />
+                    </ProForm.Group>
+                  </>
+                ),
+              },
+              {
+                key: "content",
+                label: intl.formatMessage({ id: "forms.fields.content.label" }),
+                forceRender: true,
+                children: (
                   <ProForm.Group>
-                    <ProFormTextArea
-                      name="summary"
-                      width="lg"
-                      label={intl.formatMessage({
-                        id: "forms.fields.summary.label",
-                      })}
-                    />
+                    <Form.Item
+                      name="content"
+                      label={
+                        <Space>
+                          {intl.formatMessage({
+                            id: "forms.fields.content.label",
+                          })}
+                          <Button>预览</Button>
+                        </Space>
+                      }
+                    >
+                      <MDEditor />
+                    </Form.Item>
                   </ProForm.Group>
-                </>
-              ),
-            },
-            {
-              key: "content",
-              label: intl.formatMessage({ id: "forms.fields.content.label" }),
-              forceRender: true,
-              children: (
-                <ProForm.Group>
-                  <Form.Item
-                    name="content"
-                    label={
-                      <Space>
-                        {intl.formatMessage({
-                          id: "forms.fields.content.label",
-                        })}
-                        <Button>预览</Button>
-                      </Space>
-                    }
-                  >
-                    <MDEditor />
-                  </Form.Item>
-                </ProForm.Group>
-              ),
-            },
-          ]}
-        />
-      </ProForm>
+                ),
+              },
+            ]}
+          />
+        </ProForm>
+      )}
     </Card>
   );
 };

+ 74 - 48
dashboard/src/pages/studio/article/list.tsx

@@ -9,6 +9,7 @@ import {
   message,
   Space,
   Table,
+  Badge,
 } from "antd";
 import { ActionType, ProTable } from "@ant-design/pro-components";
 import {
@@ -16,20 +17,23 @@ import {
   DeleteOutlined,
   TeamOutlined,
   ExclamationCircleOutlined,
+  FolderAddOutlined,
+  ReconciliationOutlined,
 } from "@ant-design/icons";
 
 import ArticleCreate from "../../../components/article/ArticleCreate";
-import { delete_, get, post } from "../../../request";
+import { delete_, get } from "../../../request";
 import {
   IArticleListResponse,
-  IArticleMapAddRequest,
-  IArticleMapAddResponse,
   IDeleteResponse,
 } from "../../../components/api/Article";
 import { PublicityValueEnum } from "../../../components/studio/table";
 import { useRef, useState } from "react";
-import AnthologyModal from "../../../components/anthology/AnthologyModal";
 import ArticleTplMaker from "../../../components/article/ArticleTplMaker";
+import ShareModal from "../../../components/share/ShareModal";
+import { EResType } from "../../../components/share/Share";
+import AddToAnthology from "../../../components/article/AddToAnthology";
+import AnthologySelect from "../../../components/anthology/AnthologySelect";
 
 const { Text } = Typography;
 
@@ -39,6 +43,8 @@ interface DataItem {
   title: string;
   subtitle: string;
   summary: string;
+  anthologyCount?: number;
+  anthologyTitle?: string;
   publicity: number;
   createdAt: number;
 }
@@ -46,6 +52,7 @@ const Widget = () => {
   const intl = useIntl(); //i18n
   const { studioname } = useParams(); //url 参数
   const [openCreate, setOpenCreate] = useState(false);
+  const [anthologyId, setAnthologyId] = useState<string>();
 
   const showDeleteConfirm = (id: string, title: string) => {
     Modal.confirm({
@@ -81,7 +88,6 @@ const Widget = () => {
       },
     });
   };
-
   const ref = useRef<ActionType>();
   return (
     <>
@@ -107,20 +113,33 @@ const Widget = () => {
             ellipsis: true,
             render: (text, row, index, action) => {
               return (
-                <Link to={`/studio/${studioname}/article/${row.id}/edit`}>
-                  {row.title}
-                </Link>
+                <>
+                  <div>
+                    <Link to={`/studio/${studioname}/article/${row.id}/edit`}>
+                      {row.title}
+                    </Link>
+                  </div>
+                  <Text type="secondary">{row.subtitle}</Text>
+                </>
               );
             },
           },
           {
             title: intl.formatMessage({
-              id: "forms.fields.subtitle.label",
+              id: "columns.library.anthology.title",
             }),
             dataIndex: "subtitle",
             key: "subtitle",
-            tip: "过长会自动收缩",
-            ellipsis: true,
+            render: (text, row, index, action) => {
+              return (
+                <Space>
+                  {row.anthologyTitle}
+                  {row.anthologyCount ? (
+                    <Badge color="geekblue" count={row.anthologyCount} />
+                  ) : undefined}
+                </Space>
+              );
+            },
           },
           {
             title: intl.formatMessage({
@@ -131,7 +150,6 @@ const Widget = () => {
             tip: "过长会自动收缩",
             ellipsis: true,
           },
-
           {
             title: intl.formatMessage({
               id: "forms.fields.publicity.label",
@@ -163,6 +181,7 @@ const Widget = () => {
             render: (text, row, index, action) => {
               return [
                 <Dropdown.Button
+                  trigger={["click", "contextMenu"]}
                   key={index}
                   type="link"
                   menu={{
@@ -177,15 +196,31 @@ const Widget = () => {
                             trigger={<>模版</>}
                           />
                         ),
-                        icon: <TeamOutlined />,
+                        icon: <ReconciliationOutlined />,
                       },
                       {
                         key: "share",
-                        label: intl.formatMessage({
-                          id: "buttons.share",
-                        }),
+                        label: (
+                          <ShareModal
+                            trigger={intl.formatMessage({
+                              id: "buttons.share",
+                            })}
+                            resId={row.id}
+                            resType={EResType.article}
+                          />
+                        ),
                         icon: <TeamOutlined />,
-                        disabled: true,
+                      },
+                      {
+                        key: "addToAnthology",
+                        label: (
+                          <AddToAnthology
+                            trigger="加入文集"
+                            studioName={studioname}
+                            articleIds={[row.id]}
+                          />
+                        ),
+                        icon: <FolderAddOutlined />,
                       },
                       {
                         key: "remove",
@@ -257,40 +292,18 @@ const Widget = () => {
           onCleanSelected,
         }) => {
           return (
-            <Space size={16}>
-              <AnthologyModal
-                studioName={studioname}
-                trigger={<Button type="link">加入文集</Button>}
-                onSelect={(id: string) => {
-                  console.log(selectedRowKeys);
-                  post<IArticleMapAddRequest, IArticleMapAddResponse>(
-                    "/v2/article-map",
-                    {
-                      anthology_id: id,
-                      article_id: selectedRowKeys.map((item) =>
-                        item.toString()
-                      ),
-                      operation: "add",
-                    }
-                  )
-                    .finally(() => {
-                      onCleanSelected();
-                    })
-                    .then((json) => {
-                      if (json.ok) {
-                        message.success(json.data);
-                      } else {
-                        message.error(json.message);
-                      }
-                    })
-                    .catch((e) => console.error(e));
-                }}
-              />
-            </Space>
+            <AddToAnthology
+              studioName={studioname}
+              articleIds={selectedRowKeys.map((item) => item.toString())}
+              onFinally={() => {
+                onCleanSelected();
+              }}
+            />
           );
         }}
         request={async (params = {}, sorter, filter) => {
           console.log(params, sorter, filter);
+          console.log("anthology", anthologyId);
           let url = `/v2/article?view=studio&name=${studioname}`;
           const offset =
             ((params.current ? params.current : 1) - 1) *
@@ -299,8 +312,11 @@ const Widget = () => {
           if (typeof params.keyword !== "undefined") {
             url += "&search=" + (params.keyword ? params.keyword : "");
           }
-
+          if (typeof anthologyId !== "undefined") {
+            url += "&anthology=" + anthologyId;
+          }
           const res = await get<IArticleListResponse>(url);
+          console.log("article list", res);
           const items: DataItem[] = res.data.rows.map((item, id) => {
             const date = new Date(item.created_at);
             return {
@@ -309,6 +325,8 @@ const Widget = () => {
               title: item.title,
               subtitle: item.subtitle,
               summary: item.summary,
+              anthologyCount: item.anthology_count,
+              anthologyTitle: item.anthology_first?.title,
               publicity: item.status,
               createdAt: date.getTime(),
             };
@@ -324,12 +342,20 @@ const Widget = () => {
         pagination={{
           showQuickJumper: true,
           showSizeChanger: true,
+          pageSize: 10,
         }}
         search={false}
         options={{
           search: true,
         }}
         toolBarRender={() => [
+          <AnthologySelect
+            studioName={studioname}
+            onSelect={(value: string) => {
+              setAnthologyId(value);
+              ref.current?.reload();
+            }}
+          />,
           <Popover
             content={
               <ArticleCreate

+ 20 - 2
dashboard/src/pages/studio/channel/edit.tsx

@@ -6,7 +6,8 @@ import {
   ProFormText,
   ProFormTextArea,
 } from "@ant-design/pro-components";
-import { Card, message } from "antd";
+import { TeamOutlined } from "@ant-design/icons";
+import { Button, Card, message } from "antd";
 
 import { IApiResponseChannel } from "../../../components/api/Channel";
 import { get, put } from "../../../request";
@@ -14,6 +15,8 @@ import ChannelTypeSelect from "../../../components/channel/ChannelTypeSelect";
 import LangSelect from "../../../components/general/LangSelect";
 import PublicitySelect from "../../../components/studio/PublicitySelect";
 import GoBack from "../../../components/studio/GoBack";
+import ShareModal from "../../../components/share/ShareModal";
+import { EResType } from "../../../components/share/Share";
 
 interface IFormData {
   name: string;
@@ -28,10 +31,25 @@ const Widget = () => {
   const { channelid } = useParams(); //url 参数
   const { studioname } = useParams();
   const [title, setTitle] = useState("");
-
+  console.log("channel", channelid);
   return (
     <Card
       title={<GoBack to={`/studio/${studioname}/channel/list`} title={title} />}
+      extra={
+        channelid ? (
+          <ShareModal
+            trigger={
+              <Button icon={<TeamOutlined />}>
+                {intl.formatMessage({
+                  id: "buttons.share",
+                })}
+              </Button>
+            }
+            resId={channelid}
+            resType={EResType.channel}
+          />
+        ) : undefined
+      }
     >
       <ProForm<IFormData>
         onFinish={async (values: IFormData) => {

+ 21 - 1
dashboard/src/pages/studio/channel/show.tsx

@@ -1,17 +1,22 @@
 import { useEffect, useState } from "react";
 import { useParams } from "react-router-dom";
-import { Card, Tabs } from "antd";
+import { Button, Card, Tabs } from "antd";
+import { TeamOutlined } from "@ant-design/icons";
 
 import { get } from "../../../request";
 import GoBack from "../../../components/studio/GoBack";
 import { IApiResponseChannel } from "../../../components/api/Channel";
 import ChapterInChannelList from "../../../components/channel/ChapterInChannelList";
 import TermList from "../../../components/term/TermList";
+import ShareModal from "../../../components/share/ShareModal";
+import { useIntl } from "react-intl";
+import { EResType } from "../../../components/share/Share";
 
 const Widget = () => {
   const { channelId } = useParams(); //url 参数
   const { studioname } = useParams();
   const [title, setTitle] = useState("");
+  const intl = useIntl();
 
   useEffect(() => {
     get<IApiResponseChannel>(`/v2/channel/${channelId}`).then((json) => {
@@ -21,6 +26,21 @@ const Widget = () => {
   return (
     <Card
       title={<GoBack to={`/studio/${studioname}/channel/list`} title={title} />}
+      extra={
+        channelId ? (
+          <ShareModal
+            trigger={
+              <Button icon={<TeamOutlined />}>
+                {intl.formatMessage({
+                  id: "buttons.share",
+                })}
+              </Button>
+            }
+            resId={channelId}
+            resType={EResType.channel}
+          />
+        ) : undefined
+      }
     >
       <Tabs
         size="small"