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

Merge pull request #1560 from visuddhinanda/agile

页码搜索加速
visuddhinanda 2 лет назад
Родитель
Сommit
d3aadfc346

+ 1 - 0
.gitignore

@@ -1,3 +1,4 @@
 tmp
 *.log
 /.VSCodeCounter
+config.toml

+ 3 - 3
clients/php/lily-demo.php

@@ -4,7 +4,7 @@ require dirname(__FILE__) . '/vendor/autoload.php';
 
 function tex2pdf($host, $request)
 {
-    $client = new Palm\Lily\V1\TexClient($host, [
+    $client = new \Palm\Lily\V1\TexClient($host, [
         'credentials' => Grpc\ChannelCredentials::createInsecure(),
     ]);
 
@@ -16,7 +16,7 @@ function tex2pdf($host, $request)
     echo $response->getContentType() . '(' . strlen($response->getPayload()) . ' bytes)' . PHP_EOL;
 }
 
-$request = new Palm\Lily\V1\TexToRequest();
+$request = new \Palm\Lily\V1\TexToRequest();
 
 $request->getFiles()['main.tex'] = <<<'EOF'
 % 导言区
@@ -66,4 +66,4 @@ $request->getFiles()['section-2.tex'] = <<<'EOF'
 子章节2-2 正文
 EOF;
 
-tex2pdf('localhost:9999', $request);
+tex2pdf('192.168.43.100:9000', $request);

+ 7 - 0
dashboard/src/Router.tsx

@@ -79,9 +79,13 @@ import Studio from "./pages/studio";
 import StudioHome from "./pages/studio/home";
 
 import StudioPalicanon from "./pages/studio/palicanon";
+
 import StudioRecent from "./pages/studio/recent";
 import StudioRecentList from "./pages/studio/recent/list";
 
+import StudioTransfer from "./pages/studio/transfer";
+import StudioTransferList from "./pages/studio/transfer/list";
+
 import StudioChannel from "./pages/studio/channel";
 import StudioChannelList from "./pages/studio/channel/list";
 import StudioChannelSetting from "./pages/studio/channel/setting";
@@ -317,6 +321,9 @@ const Widget = () => {
           <Route path="invite" element={<StudioInvite />}>
             <Route path="list" element={<StudioInviteList />} />
           </Route>
+          <Route path="transfer" element={<StudioTransfer />}>
+            <Route path="list" element={<StudioTransferList />} />
+          </Route>
         </Route>
       </Routes>
     </ConfigProvider>

Разница между файлами не показана из-за своего большого размера
+ 10 - 0
dashboard/src/assets/icon/index.tsx


+ 5 - 1
dashboard/src/components/api/Corpus.ts

@@ -273,8 +273,12 @@ export interface ISentencePrResponse {
   };
 }
 
+export interface ISimSent {
+  sent: string;
+  sim: number;
+}
 export interface ISentenceSimListResponse {
   ok: boolean;
   message: string;
-  data: { rows: string[]; count: number };
+  data: { rows: ISimSent[]; count: number };
 }

+ 40 - 0
dashboard/src/components/api/Transfer.ts

@@ -0,0 +1,40 @@
+import { IStudio } from "../auth/StudioName";
+import { IUser } from "../auth/User";
+import { IChannel } from "../channel/Channel";
+import { TResType } from "../discussion/DiscussionListCard";
+
+export type ITransferStatus = "transferred" | "accept" | "refuse" | "cancel";
+export interface ITransferRequest {
+  res_type?: TResType;
+  res_id?: string;
+  new_owner?: string;
+  status?: ITransferStatus;
+}
+export interface ITransferResponseData {
+  id: string;
+  origin_owner: IStudio;
+  res_type: TResType;
+  res_id: string;
+  channel?: IChannel;
+  transferor: IUser;
+  new_owner: IStudio;
+  status: ITransferStatus;
+  editor?: IUser | null;
+  created_at: string;
+  updated_at: string;
+}
+export interface ITransferResponse {
+  ok: boolean;
+  message: string;
+  data: ITransferResponseData;
+}
+export interface ITransferResponseList {
+  ok: boolean;
+  message: string;
+  data: {
+    rows: ITransferResponseData[];
+    count: number;
+    out: number;
+    in: number;
+  };
+}

+ 25 - 0
dashboard/src/components/channel/ChannelTable.tsx

@@ -26,6 +26,8 @@ import StudioName, { IStudio } from "../../components/auth/StudioName";
 import StudioSelect from "../../components/channel/StudioSelect";
 import { IChannel } from "./Channel";
 import { getSorterUrl } from "../../utils";
+import TransferCreate from "../transfer/TransferCreate";
+import { TransferOutLinedIcon } from "../../assets/icon";
 
 const { Text } = Typography;
 
@@ -87,6 +89,9 @@ const ChannelTableWidget = ({
   const [myNumber, setMyNumber] = useState<number>(0);
   const [collaborationNumber, setCollaborationNumber] = useState<number>(0);
   const [collaborator, setCollaborator] = useState<string>();
+  const [transfer, setTransfer] = useState<string>();
+  const [transferName, setTransferName] = useState<string>();
+  const [transferOpen, setTransferOpen] = useState(false);
 
   useEffect(() => {
     ref.current?.reload();
@@ -351,6 +356,13 @@ const ChannelTableWidget = ({
                         ),
                         icon: <TeamOutlined />,
                       },
+                      {
+                        key: "transfer",
+                        label: intl.formatMessage({
+                          id: "columns.studio.transfer.title",
+                        }),
+                        icon: <TransferOutLinedIcon />,
+                      },
                       {
                         key: "remove",
                         label: intl.formatMessage({
@@ -365,6 +377,11 @@ const ChannelTableWidget = ({
                         case "remove":
                           showDeleteConfirm(row.uid, row.title);
                           break;
+                        case "transfer":
+                          setTransfer(row.uid);
+                          setTransferName(row.title);
+                          setTransferOpen(true);
+                          break;
                         default:
                           break;
                       }
@@ -528,6 +545,14 @@ const ChannelTableWidget = ({
           },
         }}
       />
+      <TransferCreate
+        studioName={studioName}
+        resId={transfer}
+        resType="channel"
+        resName={transferName}
+        open={transferOpen}
+        onOpenChange={(visible: boolean) => setTransferOpen(visible)}
+      />
     </>
   );
 };

+ 8 - 5
dashboard/src/components/fts/FullSearchInput.tsx

@@ -2,6 +2,7 @@ import { AutoComplete, Badge, Input, Select, Space, Typography } from "antd";
 import { SizeType } from "antd/lib/config-provider/SizeContext";
 import { useState } from "react";
 import { get } from "../../request";
+import { ISearchView } from "./FullTextSearchResult";
 
 const { Text } = Typography;
 const { Option } = Select;
@@ -32,8 +33,7 @@ interface IWidget {
   para?: number;
   size?: SizeType;
   width?: string | number;
-  searchPage?: boolean;
-  view?: string;
+  view?: ISearchView;
   onSearch?: Function;
   onSplit?: Function;
   onPageTypeChange?: Function;
@@ -46,7 +46,6 @@ const FullSearchInputWidget = ({
   width,
   onSearch,
   view = "pali",
-  searchPage = false,
   onPageTypeChange,
 }: IWidget) => {
   const [options, setOptions] = useState<ValueType[]>([]);
@@ -112,7 +111,7 @@ const FullSearchInputWidget = ({
   );
   return (
     <Space>
-      {searchPage ? selectBefore : undefined}
+      {view === "page" ? selectBefore : undefined}
       <AutoComplete
         style={{ width: width }}
         value={input}
@@ -157,7 +156,11 @@ const FullSearchInputWidget = ({
         <Input.Search
           size={size}
           width={width}
-          placeholder="search here"
+          placeholder={
+            view === "page"
+              ? "输入页码数字,或者卷号.页码如 1.1"
+              : "search here"
+          }
           onSearch={(value: string) => {
             console.log("on search", value, tags);
             if (typeof onSearch !== "undefined") {

+ 11 - 2
dashboard/src/components/fts/FullTextSearchResult.tsx

@@ -39,7 +39,7 @@ interface IFtsItem {
   path?: ITocPathNode[];
 }
 
-export type ISearchView = "pali" | "title";
+export type ISearchView = "pali" | "title" | "page";
 interface IWidget {
   keyWord?: string;
   tags?: string[];
@@ -148,9 +148,18 @@ const FullTxtSearchResultWidget = ({
           case "title":
             link = `/article/chapter/${item.book}-${item.paragraph}`;
             break;
+          case "page":
+            link = `/article/chapter/${item.book}-${item.paragraph}`;
+            break;
           default:
             break;
         }
+        let title = "unnamed";
+        if (item.paliTitle) {
+          if (item.paliTitle.length > 0) {
+            title = item.paliTitle;
+          }
+        }
         return (
           <List.Item>
             {loading ? (
@@ -174,7 +183,7 @@ const FullTxtSearchResultWidget = ({
                 </div>
                 <Title level={4} style={{ fontWeight: 500 }}>
                   <Link to={link} target="_blank">
-                    {item.title}
+                    {item.title ? item.title : title}
                   </Link>
                 </Title>
                 <div style={{ display: "none" }}>

+ 10 - 0
dashboard/src/components/studio/LeftSider.tsx

@@ -186,6 +186,16 @@ const LeftSiderWidget = ({ selectedKeys = "" }: IWidgetHeadBar) => {
           ),
           key: "invite",
         },
+        {
+          label: (
+            <Link to={`/studio/${studioname}/transfer/list`}>
+              {intl.formatMessage({
+                id: "columns.studio.transfer.title",
+              })}
+            </Link>
+          ),
+          key: "transfer",
+        },
       ],
     },
   ];

+ 19 - 1
dashboard/src/components/template/SentEdit/SentCell.tsx

@@ -17,6 +17,7 @@ import { getEnding } from "../../../reducers/nissaya-ending-vocabulary";
 import { nissayaBase } from "../Nissaya/NissayaMeaning";
 import { anchor, message } from "../../../reducers/discussion";
 import TextDiff from "../../general/TextDiff";
+import { sentSave as _sentSave } from "./SentCellEditable";
 
 interface IWidget {
   initValue?: ISentence;
@@ -115,7 +116,24 @@ const SentCellWidget = ({
             case "suggestion":
               setPrOpen(true);
               break;
+            case "paste":
+              navigator.clipboard.readText().then((value: string) => {
+                if (sentData && value !== "") {
+                  sentData.content = value;
+                  _sentSave(
+                    sentData,
+                    (res) => {
+                      setSentData(res);
+                      if (typeof onChange !== "undefined") {
+                        onChange(res);
+                      }
+                    },
+                    () => {}
+                  );
+                }
+              });
 
+              break;
             default:
               break;
           }
@@ -219,8 +237,8 @@ const SentCellWidget = ({
                     setIsEditMode(false);
                   }}
                   onSave={(data: ISentence) => {
-                    setSentData(data);
                     setIsEditMode(false);
+                    setSentData(data);
                     if (typeof onChange !== "undefined") {
                       onChange(data);
                     }

+ 49 - 3
dashboard/src/components/template/SentEdit/SentCellEditable.tsx

@@ -18,6 +18,52 @@ import Builder from "../Builder/Builder";
 
 const { Text } = Typography;
 
+export const sentSave = (
+  data: ISentence,
+  ok: (res: ISentence) => void,
+  finish: () => void
+) => {
+  let url = `/v2/sentence/${data.book}_${data.para}_${data.wordStart}_${data.wordEnd}_${data.channel.id}`;
+  url += "?mode=edit&html=true";
+  console.log("save url", url);
+  const body = {
+    book: data.book,
+    para: data.para,
+    wordStart: data.wordStart,
+    wordEnd: data.wordEnd,
+    channel: data.channel.id,
+    content: data.content,
+    channels: data.translationChannels?.join(),
+  };
+  put<ISentenceRequest, ISentenceResponse>(url, body)
+    .then((json) => {
+      if (json.ok) {
+        const newData: ISentence = {
+          id: json.data.id,
+          content: json.data.content,
+          html: json.data.html,
+          book: json.data.book,
+          para: json.data.paragraph,
+          wordStart: json.data.word_start,
+          wordEnd: json.data.word_end,
+          editor: json.data.editor,
+          channel: json.data.channel,
+          updateAt: json.data.updated_at,
+        };
+        ok(newData);
+      } else {
+        message.error(json.message);
+      }
+    })
+    .finally(() => {
+      finish();
+    })
+    .catch((e) => {
+      console.error("catch", e);
+      message.error(e.message);
+    });
+};
+
 interface IWidget {
   data: ISentence;
   isPr?: boolean;
@@ -91,8 +137,6 @@ const SentCellEditableWidget = ({
     };
     put<ISentenceRequest, ISentenceResponse>(url, body)
       .then((json) => {
-        setSaving(false);
-
         if (json.ok) {
           message.success(intl.formatMessage({ id: "flashes.success" }));
           if (typeof onSave !== "undefined") {
@@ -114,8 +158,10 @@ const SentCellEditableWidget = ({
           message.error(json.message);
         }
       })
-      .catch((e) => {
+      .finally(() => {
         setSaving(false);
+      })
+      .catch((e) => {
         console.error("catch", e);
         message.error(e.message);
       });

+ 39 - 24
dashboard/src/components/template/SentEdit/SentEditMenu.tsx

@@ -1,4 +1,4 @@
-import { Button, Dropdown, message } from "antd";
+import { Button, Dropdown, Tooltip, message } from "antd";
 import { useState } from "react";
 import {
   EditOutlined,
@@ -15,6 +15,7 @@ import {
   CommentOutlinedIcon,
   HandOutlinedIcon,
   JsonOutlinedIcon,
+  PasteOutLinedIcon,
 } from "../../../assets/icon";
 import { useIntl } from "react-intl";
 
@@ -128,29 +129,43 @@ const SentEditMenuWidget = ({
           display: isHover ? "block" : "none",
         }}
       >
-        <Button
-          icon={<EditOutlined />}
-          size="small"
-          title="edit"
-          onClick={() => {
-            if (typeof onModeChange !== "undefined") {
-              onModeChange("edit");
-            }
-          }}
-        />
-        <Button
-          icon={<CopyOutlined />}
-          size="small"
-          onClick={() => {
-            if (data?.content) {
-              navigator.clipboard.writeText(data.content).then(() => {
-                message.success("已经拷贝到剪贴板");
-              });
-            } else {
-              message.success("内容为空");
-            }
-          }}
-        />
+        <Tooltip title="编辑">
+          <Button
+            icon={<EditOutlined />}
+            size="small"
+            onClick={() => {
+              if (typeof onModeChange !== "undefined") {
+                onModeChange("edit");
+              }
+            }}
+          />
+        </Tooltip>
+        <Tooltip title="复制">
+          <Button
+            icon={<CopyOutlined />}
+            size="small"
+            onClick={() => {
+              if (data?.content) {
+                navigator.clipboard.writeText(data.content).then(() => {
+                  message.success("已经拷贝到剪贴板");
+                });
+              } else {
+                message.success("内容为空");
+              }
+            }}
+          />
+        </Tooltip>
+        <Tooltip title="粘贴">
+          <Button
+            icon={<PasteOutLinedIcon />}
+            size="small"
+            onClick={() => {
+              if (typeof onMenuClick !== "undefined") {
+                onMenuClick("paste");
+              }
+            }}
+          />
+        </Tooltip>
         <Dropdown
           disabled={data ? false : true}
           menu={{ items, onClick }}

+ 72 - 29
dashboard/src/components/template/SentEdit/SentSim.tsx

@@ -1,9 +1,9 @@
-import { Button, message } from "antd";
+import { Button, Divider, List, Space, Switch, message } from "antd";
 import { useEffect, useState } from "react";
 import { ReloadOutlined } from "@ant-design/icons";
 
 import { get } from "../../../request";
-import { ISentenceSimListResponse } from "../../api/Corpus";
+import { ISentenceSimListResponse, ISimSent } from "../../api/Corpus";
 import MdView from "../MdView";
 
 interface IWidget {
@@ -21,29 +21,40 @@ const SentSimWidget = ({
   para,
   wordStart,
   wordEnd,
-  limit,
+  limit = 5,
   channelsId,
   reload = false,
   onReload,
 }: IWidget) => {
-  const [sentData, setSentData] = useState<string[]>([]);
+  const [initLoading, setInitLoading] = useState(true);
+  const [loading, setLoading] = useState(false);
+  const [sim, setSim] = useState(0);
+  const [offset, setOffset] = useState(0);
+  const [sentData, setSentData] = useState<ISimSent[]>([]);
+  const [remain, setRemain] = useState(0);
 
   const load = () => {
-    let url = `/v2/sent-sim?view=sentence&book=${book}&paragraph=${para}&start=${wordStart}&end=${wordEnd}&limit=10&mode=edit`;
-    if (typeof limit !== "undefined") {
-      url = url + `&limit=${limit}`;
-    }
+    let url = `/v2/sent-sim?view=sentence&book=${book}&paragraph=${para}&start=${wordStart}&end=${wordEnd}&mode=edit`;
+    url += `&limit=${limit}`;
+    url += `&offset=${offset}`;
+    url += `&sim=${sim}`;
+
     url += channelsId ? `&channels=${channelsId.join()}` : "";
+    setLoading(true);
+    console.log("url", url);
     get<ISentenceSimListResponse>(url)
       .then((json) => {
         if (json.ok) {
-          console.log("pr load", json.data.rows);
-          setSentData(json.data.rows);
+          console.log("sim load", json.data.rows);
+          setSentData([...sentData, ...json.data.rows]);
+          setRemain(json.data.count - sentData.length - json.data.rows.length);
         } else {
           message.error(json.message);
         }
       })
       .finally(() => {
+        setInitLoading(false);
+        setLoading(false);
         if (reload && typeof onReload !== "undefined") {
           onReload();
         }
@@ -51,27 +62,59 @@ const SentSimWidget = ({
   };
   useEffect(() => {
     load();
-  }, []);
-  useEffect(() => {
-    if (reload) {
-      load();
-    }
-  }, [reload]);
+  }, [offset, sim]);
+
   return (
     <>
-      <div style={{ display: "flex", justifyContent: "space-between" }}>
-        <span></span>
-        <Button
-          type="link"
-          shape="round"
-          icon={<ReloadOutlined />}
-          onClick={() => load()}
-        />
-      </div>
-
-      {sentData.map((item, id) => {
-        return <MdView html={item} key={id} />;
-      })}
+      <List
+        loading={initLoading}
+        header={
+          <div style={{ display: "flex", justifyContent: "space-between" }}>
+            <span></span>
+            <Space>
+              {"只显示相同句"}
+              <Switch
+                onChange={(checked: boolean) => {
+                  if (checked) {
+                    setSim(1);
+                  } else {
+                    setSim(0);
+                  }
+                  setOffset(0);
+                  setSentData([]);
+                }}
+              />
+              <Button
+                type="link"
+                shape="round"
+                icon={<ReloadOutlined />}
+                onClick={() => {}}
+              />
+            </Space>
+          </div>
+        }
+        itemLayout="horizontal"
+        split={false}
+        loadMore={
+          <Divider>
+            <Button
+              disabled={remain <= 0}
+              onClick={() => {
+                setOffset((origin) => origin + limit);
+              }}
+              loading={loading}
+            >
+              load more
+            </Button>
+          </Divider>
+        }
+        dataSource={sentData}
+        renderItem={(item, index) => (
+          <List.Item>
+            <MdView html={item.sent} key={index} style={{ width: "100%" }} />
+          </List.Item>
+        )}
+      />
     </>
   );
 };

+ 95 - 0
dashboard/src/components/transfer/TransferCreate.tsx

@@ -0,0 +1,95 @@
+import { ModalForm, ProForm } from "@ant-design/pro-components";
+import { Alert, Form, message, notification } from "antd";
+import { TResType } from "./TransferList";
+import { post } from "../../request";
+import { ITransferRequest, ITransferResponse } from "../api/Transfer";
+import { useIntl } from "react-intl";
+import UserSelect from "../template/UserSelect";
+import { useEffect, useState } from "react";
+
+interface IWidget {
+  studioName?: string;
+  resType: TResType;
+  resId?: string;
+  resName?: string;
+  open?: boolean;
+  onOpenChange?: Function;
+  onCreate?: Function;
+}
+const TransferCreateWidget = ({
+  studioName,
+  resType,
+  resId,
+  resName,
+  open = false,
+  onOpenChange,
+  onCreate,
+}: IWidget) => {
+  const intl = useIntl();
+  const [form] = Form.useForm<{ studio: string }>();
+  const [modalVisit, setModalVisit] = useState(open);
+  useEffect(() => setModalVisit(open), [open]);
+  const strTransfer = intl.formatMessage({
+    id: `columns.studio.transfer.title`,
+  });
+  return (
+    <ModalForm<{
+      studio: string;
+    }>
+      open={modalVisit}
+      onOpenChange={(visible) => {
+        if (typeof onOpenChange !== "undefined") {
+          onOpenChange(visible);
+        }
+      }}
+      title={intl.formatMessage({
+        id: `columns.studio.transfer.title`,
+      })}
+      form={form}
+      autoFocusFirstInput
+      modalProps={{
+        destroyOnClose: true,
+        onCancel: () => console.log("run"),
+      }}
+      submitTimeout={2000}
+      onFinish={async (values) => {
+        console.log(values);
+        if (typeof resId === "undefined") {
+          return;
+        }
+        const data = {
+          res_type: resType,
+          res_id: resId,
+          new_owner: values.studio,
+        };
+        const res = await post<ITransferRequest, ITransferResponse>(
+          `/v2/transfer`,
+          data
+        );
+        if (res.ok) {
+          if (typeof onCreate === "undefined") {
+            notification.open({
+              message: strTransfer,
+              description: `${resType} ${resName} 已经转出。请等待对方确认。可以在转移管理中查看状态或取消。`,
+              duration: 0,
+            });
+          } else {
+            onCreate();
+          }
+        } else {
+          message.error(res.message, 10);
+        }
+        return true;
+      }}
+    >
+      <Alert
+        message={`将${resName} ${strTransfer}下面的用户。操作后需要等待对方确认后,资源才会被转移。可以在${strTransfer}查看和取消`}
+      />
+      <ProForm.Group>
+        <UserSelect name="studio" multiple={false} />
+      </ProForm.Group>
+    </ModalForm>
+  );
+};
+
+export default TransferCreateWidget;

+ 249 - 0
dashboard/src/components/transfer/TransferList.tsx

@@ -0,0 +1,249 @@
+import { useRef, useState } from "react";
+import { Button, Space, Tag, Typography, message, notification } from "antd";
+
+import { get, put } from "../../request";
+import { ActionType, ProList } from "@ant-design/pro-components";
+import { renderBadge } from "../channel/ChannelTable";
+import User, { IUser } from "../auth/User";
+import { IChannel } from "../channel/Channel";
+import { IStudio } from "../auth/StudioName";
+import UserName from "../auth/UserName";
+import TimeShow from "../general/TimeShow";
+import {
+  ITransferRequest,
+  ITransferResponse,
+  ITransferResponseList,
+  ITransferStatus,
+} from "../api/Transfer";
+import { useIntl } from "react-intl";
+import { BaseType } from "antd/lib/typography/Base";
+
+const { Text } = Typography;
+
+export type TResType = "article" | "channel" | "chapter" | "sentence" | "wbw";
+
+interface ITransfer {
+  id: string;
+  origin_owner: IStudio;
+  res_type: TResType;
+  res_id: string;
+  channel?: IChannel;
+  transferor: IUser;
+  new_owner: IStudio;
+  status: ITransferStatus;
+  editor?: IUser | null;
+  created_at: string;
+  updated_at: string;
+}
+interface IWidget {
+  studioName?: string;
+}
+const TransferListWidget = ({ studioName }: IWidget) => {
+  const ref = useRef<ActionType>();
+  const [activeKey, setActiveKey] = useState<React.Key | undefined>("in");
+  const [activeNumber, setActiveNumber] = useState<number>(0);
+  const [closeNumber, setCloseNumber] = useState<number>(0);
+  const intl = useIntl();
+
+  const openNotification = (description: string) => {
+    const args = {
+      message: intl.formatMessage({
+        id: `columns.studio.transfer.title`,
+      }),
+      description: description,
+      duration: 0,
+    };
+    notification.open(args);
+  };
+
+  const setStatus = (status: ITransferStatus, id: string) => {
+    const data: ITransferRequest = {
+      status: status,
+    };
+    put<ITransferRequest, ITransferResponse>(`/v2/transfer/${id}`, data).then(
+      (json) => {
+        if (json.ok) {
+          ref.current?.reload();
+          openNotification(
+            `已经` + intl.formatMessage({ id: `forms.status.${status}.label` })
+          );
+        } else {
+          message.error(json.message);
+        }
+      }
+    );
+  };
+  return (
+    <>
+      <ProList<ITransfer>
+        rowKey="id"
+        actionRef={ref}
+        metas={{
+          avatar: {
+            render(dom, entity, index, action, schema) {
+              return (
+                <>
+                  <User {...entity.transferor} showName={false} />
+                </>
+              );
+            },
+          },
+          title: {
+            render(dom, entity, index, action, schema) {
+              return (
+                <>
+                  {entity.origin_owner.studioName}/{entity.channel?.name}
+                </>
+              );
+            },
+          },
+          subTitle: {
+            render(dom, entity, index, action, schema) {
+              return <Tag>{entity.res_type}</Tag>;
+            },
+          },
+          description: {
+            search: false,
+            render(dom, entity, index, action, schema) {
+              return (
+                <Space>
+                  <UserName {...entity.transferor} />
+                  {"transfer at"}
+                  <TimeShow createdAt={entity.created_at} />
+                </Space>
+              );
+            },
+          },
+          content: {
+            render(dom, entity, index, action, schema) {
+              let style: BaseType | undefined;
+              switch (entity.status) {
+                case "accept":
+                  style = "success";
+                  break;
+                case "refuse":
+                  style = "warning";
+                  break;
+                case "cancel":
+                  style = "danger";
+                  break;
+                default:
+                  style = undefined;
+                  break;
+              }
+              return (
+                <Text type={style}>
+                  {intl.formatMessage({
+                    id: `forms.status.${entity.status}.label`,
+                  })}
+                </Text>
+              );
+            },
+          },
+          actions: {
+            render: (text, row, index, action) => [
+              activeKey === "in" ? (
+                <>
+                  <Button
+                    type="text"
+                    disabled={row.status !== "transferred"}
+                    onClick={() => setStatus("accept", row.id)}
+                  >
+                    {intl.formatMessage({
+                      id: `buttons.accept`,
+                    })}
+                  </Button>
+                  <Button
+                    disabled={row.status !== "transferred"}
+                    danger
+                    type="text"
+                    onClick={() => setStatus("refuse", row.id)}
+                  >
+                    {intl.formatMessage({
+                      id: `buttons.refuse`,
+                    })}
+                  </Button>
+                </>
+              ) : (
+                <>
+                  <Button
+                    type="text"
+                    disabled={row.status !== "transferred"}
+                    onClick={() => setStatus("cancel", row.id)}
+                  >
+                    {intl.formatMessage({
+                      id: `buttons.cancel`,
+                    })}
+                  </Button>
+                </>
+              ),
+            ],
+          },
+        }}
+        request={async (params = {}, sorter, filter) => {
+          let url = `/v2/transfer?view=studio&name=${studioName}&view2=${activeKey}`;
+          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 : "";
+          console.log("url", url);
+          const res = await get<ITransferResponseList>(url);
+          const items: ITransfer[] = res.data.rows.map((item, id) => {
+            return item;
+          });
+
+          setActiveNumber(res.data.in);
+          setCloseNumber(res.data.out);
+
+          return {
+            total: res.data.count,
+            succcess: true,
+            data: items,
+          };
+        }}
+        bordered
+        pagination={{
+          showQuickJumper: true,
+          showSizeChanger: true,
+          pageSize: 10,
+        }}
+        search={false}
+        options={{
+          search: false,
+        }}
+        toolbar={{
+          menu: {
+            activeKey,
+            items: [
+              {
+                key: "in",
+                label: (
+                  <span>
+                    转入
+                    {renderBadge(activeNumber, activeKey === "in")}
+                  </span>
+                ),
+              },
+              {
+                key: "out",
+                label: (
+                  <span>
+                    转出
+                    {renderBadge(closeNumber, activeKey === "out")}
+                  </span>
+                ),
+              },
+            ],
+            onChange(key) {
+              setActiveKey(key);
+              ref.current?.reload();
+            },
+          },
+        }}
+      />
+    </>
+  );
+};
+
+export default TransferListWidget;

+ 2 - 0
dashboard/src/locales/zh-Hans/buttons.ts

@@ -71,6 +71,8 @@ const items = {
   "buttons.timeline": "时间线",
   "buttons.discussion": "讨论",
   "buttons.suggestion": "修改建议",
+  "buttons.accept": "接受",
+  "buttons.refuse": "拒绝",
 };
 
 export default items;

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

@@ -80,6 +80,10 @@ const items = {
   "forms.message.question.required": "请输入您的问题(必填)",
   "forms.message.question.description.option": "问题详细描述(选填)",
   "forms.fields.replay.label": "回复",
+  "forms.status.transferred.label": "等待处理",
+  "forms.status.accept.label": "已经接受",
+  "forms.status.refuse.label": "已经拒绝",
+  "forms.status.cancel.label": "已经撤回",
 };
 
 export default items;

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

@@ -46,6 +46,7 @@ const items = {
   "columns.studio.setting.title": "设置",
   "columns.exp.title": "经验",
   "columns.studio.invite.title": "注册邀请",
+  "columns.studio.transfer.title": "转让",
   ...buttons,
   ...forms,
   ...tables,

+ 10 - 16
dashboard/src/pages/library/search/search.tsx

@@ -15,12 +15,16 @@ const Widget = () => {
   const [searchParams, setSearchParams] = useSearchParams();
   const [bookRoot, setBookRoot] = useState("default");
   const [bookPath, setBookPath] = useState<string[]>([]);
-  const [searchPage, setSearchPage] = useState(false);
   const navigate = useNavigate();
   const [pageType, setPageType] = useState("P");
-  const [view, setView] = useState("pali");
+  const [view, setView] = useState<ISearchView | undefined>("pali");
 
-  useEffect(() => {}, [key, searchParams]);
+  useEffect(() => {
+    const v = searchParams.get("view");
+    if (typeof v === "string") {
+      setView(v as ISearchView);
+    }
+  }, [key, searchParams]);
 
   useEffect(() => {
     let currRoot: string | null;
@@ -61,7 +65,6 @@ const Widget = () => {
                     size="large"
                     width={"500px"}
                     value={key}
-                    searchPage={searchPage}
                     view={view}
                     tags={searchParams.get("tags")?.split(",")}
                     onSearch={(value: string) => {
@@ -90,20 +93,11 @@ const Widget = () => {
                   ))}
                 </Breadcrumb>
                 <Tabs
+                  activeKey={view}
                   onChange={(activeKey: string) => {
-                    setView(activeKey);
-                    searchParams.set(view, activeKey);
+                    setView(activeKey as ISearchView);
+                    searchParams.set("view", activeKey);
                     setSearchParams(searchParams);
-                    switch (activeKey) {
-                      case "pali":
-                        setSearchPage(false);
-                        break;
-                      case "page":
-                        setSearchPage(true);
-                        break;
-                      default:
-                        break;
-                    }
                   }}
                   size="small"
                   items={[

+ 22 - 0
dashboard/src/pages/studio/transfer/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="transfer" />
+        <Content style={styleStudioContent}>
+          <Outlet />
+        </Content>
+      </Layout>
+    </Layout>
+  );
+};
+
+export default Widget;

+ 13 - 0
dashboard/src/pages/studio/transfer/list.tsx

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

Некоторые файлы не были показаны из-за большого количества измененных файлов