ソースを参照

Merge branch 'agile' of github.com:iapt-platform/mint into agile

Jeremy Zheng 2 年 前
コミット
6021220a34

+ 2 - 0
dashboard/src/components/article/TypeArticle.tsx

@@ -73,6 +73,8 @@ const TypeArticleWidget = ({
             setArticleHtml([json.data.html]);
           } else if (json.data.content) {
             setArticleHtml([json.data.content]);
+          } else {
+            setArticleHtml([""]);
           }
           setExtra(
             <TocTree

+ 3 - 0
dashboard/src/components/auth/setting/SettingItem.tsx

@@ -24,10 +24,12 @@ const { Text } = Typography;
 interface IWidgetSettingItem {
   data?: ISetting;
   autoSave?: boolean;
+  bordered?: boolean;
   onChange?: Function;
 }
 const SettingItemWidget = ({
   data,
+  bordered = true,
   onChange,
   autoSave = true,
 }: IWidgetSettingItem) => {
@@ -146,6 +148,7 @@ const SettingItemWidget = ({
                   <Select
                     defaultValue={data.defaultValue}
                     style={{ width: 120 }}
+                    bordered={bordered}
                     onChange={(value: string) => {
                       console.log(`selected ${value}`);
                       if (autoSave) {

+ 132 - 30
dashboard/src/components/dict/CaseList.tsx

@@ -1,7 +1,11 @@
-import { Button, List, Tag, Typography } from "antd";
+import { Badge, Button, Card, Checkbox, Select, Space, Typography } from "antd";
+import { DownOutlined, UpOutlined } from "@ant-design/icons";
 import { useEffect, useState } from "react";
+
 import { get } from "../../request";
-import { ICaseListResponse } from "../api/Dict";
+import { ICaseItem, ICaseListResponse } from "../api/Dict";
+import { CheckboxValueType } from "antd/lib/checkbox/Group";
+import { CheckboxChangeEvent } from "antd/es/checkbox";
 const { Text } = Typography;
 
 export interface ICaseListData {
@@ -12,12 +16,37 @@ export interface ICaseListData {
 interface IWidget {
   word?: string;
   lines?: number;
+  onChange?: Function;
 }
-const CaseListWidget = ({ word, lines }: IWidget) => {
+const CaseListWidget = ({ word, lines, onChange }: IWidget) => {
   const [caseData, setCaseData] = useState<ICaseListData[]>();
-  const [first, setFirst] = useState<string>();
-  const [count, setCount] = useState<number>();
   const [showAll, setShowAll] = useState(lines ? false : true);
+  const [words, setWords] = useState<ICaseItem[]>();
+  const [currWord, setCurrWord] = useState<string>();
+  const [checkedList, setCheckedList] = useState<CheckboxValueType[]>([]);
+
+  useEffect(() => {
+    setCaseData(
+      words
+        ?.find((value) => value.word === currWord)
+        ?.case.sort((a, b) => b.count - a.count)
+    );
+  }, [currWord, words]);
+
+  useEffect(() => {
+    if (typeof onChange !== "undefined" && checkedList.length > 0) {
+      onChange(checkedList);
+    }
+  }, [checkedList]);
+
+  useEffect(() => {
+    if (caseData) {
+      setCheckedList(caseData?.map((item) => item.word));
+    } else {
+      setCheckedList([]);
+    }
+  }, [caseData]);
+
   useEffect(() => {
     if (typeof word === "undefined") {
       return;
@@ -25,41 +54,114 @@ const CaseListWidget = ({ word, lines }: IWidget) => {
     get<ICaseListResponse>(`/v2/case/${word}`).then((json) => {
       console.log("case", json);
       if (json.ok && json.data.rows.length > 0) {
+        setWords(json.data.rows);
         const first = json.data.rows.sort((a, b) => b.count - a.count)[0];
-        setCaseData(first.case.sort((a, b) => b.count - a.count));
-        setCount(first.count);
-        setFirst(first.word);
+        setCurrWord(first.word);
       }
     });
   }, [word]);
+
+  let checkAll = true;
+  let indeterminate = false;
+  if (caseData && checkedList) {
+    checkAll = caseData?.length === checkedList?.length;
+    indeterminate =
+      checkedList.length > 0 && checkedList.length < caseData.length;
+  }
+
+  const onWordChange = (list: CheckboxValueType[]) => {
+    setCheckedList(list);
+  };
+
+  const onCheckAllChange = (e: CheckboxChangeEvent) => {
+    if (caseData) {
+      setCheckedList(
+        e.target.checked ? caseData?.map((item) => item.word) : []
+      );
+    } else {
+      setCheckedList([]);
+    }
+  };
+
+  const showWords = showAll ? caseData : caseData?.slice(0, lines);
   return (
     <div style={{ padding: 4 }}>
-      <List
-        header={`${first}:${count}`}
-        footer={
+      <Card
+        size="small"
+        extra={
           lines ? (
             <Button type="link" onClick={() => setShowAll(!showAll)}>
-              {showAll ? "折叠" : "展开"}
+              {showAll ? (
+                <Space>
+                  {"折叠"}
+                  <UpOutlined />
+                </Space>
+              ) : (
+                <Space>
+                  {"展开"}
+                  <DownOutlined />
+                </Space>
+              )}
             </Button>
-          ) : undefined
+          ) : (
+            <></>
+          )
         }
-        size="small"
-        dataSource={showAll ? caseData : caseData?.slice(0, lines)}
-        renderItem={(item) => (
-          <List.Item>
-            <div
-              style={{
-                display: "flex",
-                justifyContent: "space-between",
-                width: "100%",
-              }}
-            >
-              <Text strong={item.bold > 0 ? true : false}>{item.word}</Text>
-              <Tag>{item.count}</Tag>
-            </div>
-          </List.Item>
-        )}
-      />
+        title={
+          <Select
+            value={currWord}
+            bordered={false}
+            onChange={(value: string) => {
+              setCurrWord(value);
+            }}
+            options={words?.map((item, id) => {
+              return {
+                label: (
+                  <Space>
+                    {item.word}
+                    <Badge
+                      count={item.count}
+                      color={"lime"}
+                      status="default"
+                      size="small"
+                    />
+                  </Space>
+                ),
+                value: item.word,
+              };
+            })}
+          />
+        }
+      >
+        <Checkbox
+          indeterminate={indeterminate}
+          onChange={onCheckAllChange}
+          checked={checkAll}
+        >
+          Check all
+        </Checkbox>
+        <Checkbox.Group
+          style={{ display: "grid" }}
+          options={showWords?.map((item, id) => {
+            return {
+              label: (
+                <Space>
+                  <Text strong={item.bold > 0 ? true : false}>{item.word}</Text>
+                  <Badge
+                    size="small"
+                    count={item.count}
+                    overflowCount={9999}
+                    status="default"
+                  />
+                </Space>
+              ),
+              value: item.word,
+            };
+          })}
+          value={checkedList}
+          onChange={onWordChange}
+        />
+      </Card>
     </div>
   );
 };

+ 94 - 46
dashboard/src/components/export/ExportModal.tsx

@@ -1,4 +1,12 @@
-import { Modal, Progress, Select, Switch, Typography, message } from "antd";
+import {
+  Collapse,
+  Modal,
+  Progress,
+  Select,
+  Switch,
+  Typography,
+  message,
+} from "antd";
 import { useEffect, useRef, useState } from "react";
 import { get } from "../../request";
 import { ArticleType } from "../article/Article";
@@ -15,6 +23,7 @@ interface IExportResponse {
 interface IStatus {
   progress: number;
   message: string;
+  log?: string[];
 }
 interface IExportStatusResponse {
   ok: boolean;
@@ -67,17 +76,20 @@ const ExportModalWidget = ({
     }
     const url = `/v2/export/${filenameRef.current}`;
     console.log("url", url);
-    get<IExportStatusResponse>(url).then((json) => {
-      if (json.ok) {
-        console.log("filename", json);
-        setExportStatus(json.data.status);
-        if (json.data.status.progress === 1) {
-          setFilename(undefined);
-          setUrl(json.data.url);
+    get<IExportStatusResponse>(url)
+      .then((json) => {
+        if (json.ok) {
+          console.log("filename", json);
+          setExportStatus(json.data.status);
+          if (json.data.status.progress === 1) {
+            setFilename(undefined);
+            setUrl(json.data.url);
+          }
+        } else {
+          console.error(json.message);
         }
-      } else {
-      }
-    });
+      })
+      .catch((e) => console.error(e));
   };
 
   useEffect(() => {
@@ -85,26 +97,52 @@ const ExportModalWidget = ({
     return () => clearInterval(interval);
   }, []);
 
-  const exportChapter = (
-    book: number,
-    para: number,
-    channel: string,
-    format: string
-  ): void => {
-    let url = `/v2/export?book=${book}&par=${para}&channel=${channel}&format=${format}`;
+  const getUrl = (): string => {
+    if (!articleId) {
+      throw new Error("id error");
+    }
+    let url = `/v2/export?type=${type}&id=${articleId}&format=${format}`;
+    url += channelId ? `&channel=${channelId}` : "";
     url += "&origin=" + (hasOrigin ? "true" : "false");
     url += "&translation=" + (hasTranslation ? "true" : "false");
+    switch (type) {
+      case "chapter":
+        const para = articleId?.split("-").map((item) => parseInt(item));
+        if (para?.length === 2) {
+          url += `&book=${para[0]}&par=${para[1]}`;
+        } else {
+          throw new Error("段落编号错误");
+        }
+        if (!channelId) {
+          throw new Error("请选择版本");
+        }
+        break;
+      case "article":
+        url += `&id=${articleId}`;
+        url += anthologyId ? `&anthology=${anthologyId}` : "";
+        break;
+      default:
+        throw new Error("此类型暂时无法导出" + type);
+        break;
+    }
+    return url;
+  };
+  const exportRun = (): void => {
+    const url = getUrl();
     console.log("url", url);
     setExportStart(true);
-    get<IExportResponse>(url).then((json) => {
-      if (json.ok) {
-        const filename = json.data;
-        console.log("filename", filename);
-        setFilename(filename);
-      } else {
-      }
-    });
+    get<IExportResponse>(url)
+      .then((json) => {
+        if (json.ok) {
+          const filename = json.data;
+          console.log("filename", filename);
+          setFilename(filename);
+        } else {
+        }
+      })
+      .catch((e) => {});
   };
+
   const closeModal = () => {
     if (typeof onClose !== "undefined") {
       onClose();
@@ -119,18 +157,11 @@ const ExportModalWidget = ({
       open={isModalOpen}
       onOk={() => {
         console.log("type", type);
-        if (type === "chapter") {
-          if (articleId && channelId) {
-            const para = articleId.split("-").map((item) => parseInt(item));
-            const channels = channelId.split("_");
-            if (para.length === 2) {
-              exportChapter(para[0], para[1], channels[0], "html");
-            } else {
-              console.error("段落编号错误", articleId);
-            }
-          }
-        } else {
-          message.error("目前只支持章节导出");
+        try {
+          exportRun();
+        } catch (error) {
+          message.error((error as Error).message);
+          console.error(error);
         }
       }}
       onCancel={closeModal}
@@ -212,7 +243,6 @@ const ExportModalWidget = ({
       />
 
       <div style={{ display: exportStart ? "block" : "none" }}>
-        <Text>{exportStatus ? exportStatus.message : "正在生成……"}</Text>
         <Progress
           percent={exportStatus ? Math.round(exportStatus?.progress * 100) : 0}
           status={
@@ -223,13 +253,31 @@ const ExportModalWidget = ({
               : "normal"
           }
         />
-        {url ? (
-          <a href={url} target="_blank" rel="noreferrer">
-            {"下载"}
-          </a>
-        ) : (
-          <></>
-        )}
+        <Collapse collapsible="icon" ghost>
+          <Collapse.Panel
+            header={
+              <div style={{ display: "flex", justifyContent: "space-between" }}>
+                <Text>
+                  {exportStatus ? exportStatus.message : "正在生成……"}
+                </Text>
+                {url ? (
+                  <a href={url} target="_blank" rel="noreferrer">
+                    {"下载"}
+                  </a>
+                ) : (
+                  <></>
+                )}
+              </div>
+            }
+            key="1"
+          >
+            <div style={{ height: 200, overflowY: "auto" }}>
+              {exportStatus?.log?.map((item, id) => {
+                return <div key={id}>{item}</div>;
+              })}
+            </div>
+          </Collapse.Panel>
+        </Collapse>
       </div>
     </Modal>
   );

+ 1 - 0
dashboard/src/components/export/ShareButton.tsx

@@ -58,6 +58,7 @@ const ShareButtonWidget = ({
               label: "添加到文集",
               key: "add_to_anthology",
               icon: <InboxOutlined />,
+              disabled: type === "article" ? false : true,
             },
             {
               label: "创建副本",

+ 28 - 2
dashboard/src/components/fts/FtsBookList.tsx

@@ -29,8 +29,10 @@ interface IFtsItem {
 }
 interface IWidget {
   keyWord?: string;
+  keyWords?: string[];
+  engin?: "wbw" | "tulip";
   tags?: string[];
-  bookId?: number;
+  bookId?: string | null;
   book?: number;
   para?: number;
   match?: string | null;
@@ -41,6 +43,8 @@ interface IWidget {
 
 const FtsBookListWidget = ({
   keyWord,
+  keyWords,
+  engin = "wbw",
   tags,
   bookId,
   book,
@@ -53,8 +57,24 @@ const FtsBookListWidget = ({
   const [ftsData, setFtsData] = useState<IFtsItem[]>();
   const [total, setTotal] = useState<number>();
 
+  const focusBooks = bookId?.split(",");
+  console.log("focusBooks", focusBooks);
   useEffect(() => {
-    let url = `/v2/search-book-list?view=${view}&key=${keyWord}`;
+    let words;
+    let api = "";
+    switch (engin) {
+      case "wbw":
+        api = "search-pali-wbw-books";
+        words = keyWords?.join();
+        break;
+      case "tulip":
+        api = "search-book-list";
+        words = keyWord;
+        break;
+      default:
+        break;
+    }
+    let url = `/v2/${api}?view=${view}&key=${words}`;
     if (typeof tags !== "undefined") {
       url += `&tags=${tags}`;
     }
@@ -64,6 +84,7 @@ const FtsBookListWidget = ({
     console.log("url", url);
     get<IFtsResponse>(url).then((json) => {
       if (json.ok) {
+        console.log("data", json.data.rows);
         let totalResult = 0;
         for (const iterator of json.data.rows) {
           totalResult += iterator.count;
@@ -95,9 +116,14 @@ const FtsBookListWidget = ({
         <List.Item>
           <div
             style={{
+              padding: 4,
+              borderRadius: 4,
               display: "flex",
               justifyContent: "space-between",
               cursor: "pointer",
+              backgroundColor: focusBooks?.includes(item.pcdBookId.toString())
+                ? "lightblue"
+                : "unset",
             }}
             onClick={() => {
               if (typeof onSelect !== "undefined") {

+ 31 - 3
dashboard/src/components/fts/FullTextSearchResult.tsx

@@ -43,6 +43,8 @@ interface IFtsItem {
 export type ISearchView = "pali" | "title" | "page";
 interface IWidget {
   keyWord?: string;
+  keyWords?: string[];
+  engin?: "wbw" | "tulip";
   tags?: string[];
   bookId?: string | null;
   book?: number;
@@ -55,6 +57,8 @@ interface IWidget {
 }
 const FullTxtSearchResultWidget = ({
   keyWord,
+  keyWords,
+  engin = "wbw",
   tags,
   bookId,
   book,
@@ -72,11 +76,25 @@ const FullTxtSearchResultWidget = ({
 
   useEffect(
     () => setCurrPage(1),
-    [view, keyWord, tags, bookId, match, pageType]
+    [view, keyWord, keyWords, tags, bookId, match, pageType]
   );
 
   useEffect(() => {
-    let url = `/v2/search?view=${view}&key=${keyWord}`;
+    let words;
+    let api = "";
+    switch (engin) {
+      case "wbw":
+        api = "search-pali-wbw";
+        words = keyWords?.join();
+        break;
+      case "tulip":
+        api = "search";
+        words = keyWord;
+        break;
+      default:
+        break;
+    }
+    let url = `/v2/${api}?view=${view}&key=${words}`;
     if (typeof tags !== "undefined") {
       url += `&tags=${tags}`;
     }
@@ -120,7 +138,17 @@ const FullTxtSearchResultWidget = ({
         }
       })
       .finally(() => setLoading(false));
-  }, [bookId, currPage, keyWord, match, orderBy, pageType, tags, view]);
+  }, [
+    bookId,
+    currPage,
+    keyWord,
+    keyWords,
+    match,
+    orderBy,
+    pageType,
+    tags,
+    view,
+  ]);
   return (
     <List
       style={{ width: "100%" }}

+ 11 - 0
dashboard/src/components/fts/search.css

@@ -13,3 +13,14 @@
   color: #177ddc;
   font-style: unset;
 }
+
+.bld {
+  font-weight: 700;
+}
+
+.hl {
+  background-color: yellow;
+}
+.note {
+  color: #177ddc;
+}

+ 20 - 2
dashboard/src/pages/library/search/search.tsx

@@ -1,6 +1,6 @@
 import { useNavigate, useParams, useSearchParams } from "react-router-dom";
 import { useEffect, useState } from "react";
-import { Row, Col, Breadcrumb, Space, Tabs } from "antd";
+import { Row, Col, Breadcrumb, Space, Tabs, Select } from "antd";
 import FullSearchInput from "../../../components/fts/FullSearchInput";
 import BookTree from "../../../components/corpus/BookTree";
 import FullTextSearchResult, {
@@ -18,6 +18,7 @@ const Widget = () => {
   const navigate = useNavigate();
   const [pageType, setPageType] = useState("P");
   const [view, setView] = useState<ISearchView | undefined>("pali");
+  const [caseWord, setCaseWord] = useState<string[]>();
 
   useEffect(() => {
     const v = searchParams.get("view");
@@ -100,6 +101,16 @@ const Widget = () => {
                     setSearchParams(searchParams);
                   }}
                   size="small"
+                  tabBarExtraContent={
+                    <Select
+                      defaultValue="wbw"
+                      bordered={false}
+                      options={[
+                        { value: "wbw", label: "wbw" },
+                        { value: "tulip", label: "tulip(beta)" },
+                      ]}
+                    />
+                  }
                   items={[
                     {
                       label: `巴利原文`,
@@ -122,6 +133,7 @@ const Widget = () => {
                   view={view as ISearchView}
                   pageType={pageType}
                   keyWord={key}
+                  keyWords={caseWord}
                   tags={searchParams.get("tags")?.split(",")}
                   bookId={searchParams.get("book")}
                   orderBy={searchParams.get("orderby")}
@@ -130,12 +142,18 @@ const Widget = () => {
               </Space>
             </Col>
             <Col xs={0} sm={0} md={5}>
-              <CaseList word={key} lines={5} />
+              <CaseList
+                word={key}
+                lines={5}
+                onChange={(value: string[]) => setCaseWord(value)}
+              />
               <FtsBookList
                 view={view}
                 keyWord={key}
+                keyWords={caseWord}
                 tags={searchParams.get("tags")?.split(",")}
                 match={searchParams.get("match")}
+                bookId={searchParams.get("book")}
                 onSelect={(bookId: number) => {
                   if (bookId !== 0) {
                     searchParams.set("book", bookId.toString());

+ 5 - 0
deploy/mint.yml

@@ -68,3 +68,8 @@
       ansible.builtin.shell:
         cmd: dbmate up
         chdir: "{{ app_deploy_root }}/agile/rpc/tulip/tulip/db"
+    - name: restrat mint-{{ app_deploy_env }}-tulip service
+      become: true
+      ansible.builtin.systemd:
+        state: restarted
+        name: mint-{{ app_deploy_env }}-tulip.service

+ 2 - 2
deploy/roles/mint-v2/tasks/laravel.yml

@@ -63,12 +63,12 @@
 
 - name: auto-loader optimization for v2
   ansible.builtin.shell:
-    cmd: composer install --optimize-autoloader --no-dev
+    cmd: composer update --optimize-autoloader --no-dev
     chdir: "{{ app_deploy_root }}/htdocs"
 
 - name: auto-loader optimization for v1
   ansible.builtin.shell:
-    cmd: composer install --optimize-autoloader --no-dev
+    cmd: composer update --optimize-autoloader --no-dev
     chdir: "{{ app_deploy_root }}/htdocs/public"
 
 - name: caching configuration

+ 2 - 1
deploy/roles/mint-v2/templates/v2/env.j2

@@ -86,4 +86,5 @@ RABBITMQ_USER="{{ app_rabbitmq_user }}"
 RABBITMQ_PASSWORD="{{ app_rabbitmq_password }}"
 
 CDN_URLS={{ app_cdn_urls }}
-ATTACHMENTS_BUCKET_NAME={{ app_attachments_bucket_name }}
+ATTACHMENTS_TEMPORARY_BUCKET_NAME={{ app_attachments_bucket_name }}-t
+ATTACHMENTS_PERMANTENT_BUCKET_NAME={{ app_attachments_bucket_name }}-p

+ 2 - 0
rpc/.gitignore

@@ -0,0 +1,2 @@
+*.tar
+*.md5

+ 1 - 1
rpc/morus/build.sh

@@ -8,7 +8,7 @@ export CODE="mint-morus"
 buildah pull ubuntu:latest
 buildah bud --layers -t $CODE .
 podman save --format=oci-archive -o $CODE-$VERSION.tar $CODE
-md5sum $CODE-$VERSION.tar > md5.txt
+md5sum $CODE-$VERSION.tar > $CODE-$VERSION.md5
 
 echo "done($CODE-$VERSION.tar)."