visuddhinanda 3 лет назад
Родитель
Сommit
690bab0f6e

+ 117 - 0
dashboard/src/components/fts/FtsBookList.tsx

@@ -0,0 +1,117 @@
+import { Badge, List } from "antd";
+import { useEffect, useState } from "react";
+
+import { get } from "../../request";
+
+interface IFtsData {
+  book: number;
+  paragraph: number;
+  title?: string;
+  paliTitle: string;
+  pcdBookId: number;
+  count: number;
+}
+interface IFtsResponse {
+  ok: boolean;
+  string: string;
+  data: {
+    rows: IFtsData[];
+    count: number;
+  };
+}
+interface IFtsItem {
+  book: number;
+  paragraph: number;
+  title?: string;
+  paliTitle?: string;
+  pcdBookId: number;
+  count: number;
+}
+interface IWidget {
+  keyWord?: string;
+  tags?: string[];
+  bookId?: number;
+  book?: number;
+  para?: number;
+  match?: string | null;
+  keyWord2?: string;
+  onSelect?: Function;
+}
+
+const Widget = ({
+  keyWord,
+  tags,
+  bookId,
+  book,
+  para,
+  keyWord2,
+  match,
+  onSelect,
+}: IWidget) => {
+  const [ftsData, setFtsData] = useState<IFtsItem[]>();
+  const [total, setTotal] = useState<number>();
+
+  useEffect(() => {
+    let url = `/v2/search-book-list?key=${keyWord}`;
+    if (typeof tags !== "undefined") {
+      url += `&tags=${tags}`;
+    }
+    if (match) {
+      url += `&match=${match}`;
+    }
+    console.log("url", url);
+    get<IFtsResponse>(url).then((json) => {
+      if (json.ok) {
+        let totalResult = 0;
+        for (const iterator of json.data.rows) {
+          totalResult += iterator.count;
+        }
+        const result: IFtsItem[] = json.data.rows.map((item) => {
+          return item;
+        });
+        setFtsData([
+          {
+            book: 0,
+            paragraph: 0,
+            title: "全部",
+            pcdBookId: 0,
+            count: totalResult,
+          },
+          ...result,
+        ]);
+        setTotal(json.data.count);
+      }
+    });
+  }, [keyWord, match, tags]);
+  return (
+    <List
+      header={`总计:` + total}
+      itemLayout="vertical"
+      size="small"
+      dataSource={ftsData}
+      renderItem={(item, id) => (
+        <List.Item>
+          <div
+            style={{
+              display: "flex",
+              justifyContent: "space-between",
+              cursor: "pointer",
+            }}
+            onClick={() => {
+              if (typeof onSelect !== "undefined") {
+                onSelect(item.pcdBookId);
+              }
+            }}
+          >
+            <span>
+              {id + 1}.{item.title ? item.title : item.paliTitle}
+            </span>
+            <Badge color="geekblue" count={item.count} />
+          </div>
+        </List.Item>
+      )}
+    />
+  );
+};
+
+export default Widget;

+ 62 - 0
dashboard/src/components/fts/FtsSetting.tsx

@@ -0,0 +1,62 @@
+import { Popover, Typography } from "antd";
+import { ISetting } from "../auth/setting/default";
+import SettingItem from "../auth/setting/SettingItem";
+const { Link } = Typography;
+
+interface IWidget {
+  trigger?: React.ReactNode;
+  orderBy?: string | null;
+  match?: string | null;
+  onChange?: Function;
+}
+const Widget = ({ trigger, orderBy = "rank", match, onChange }: IWidget) => {
+  const searchSetting: ISetting[] = [
+    {
+      key: "match",
+      label: "setting.search.match.label",
+      defaultValue: match ? match : "case",
+      widget: "select",
+      options: [
+        { label: "setting.search.match.complete.label", value: "complete" },
+        { label: "setting.search.match.case.label", value: "case" },
+        { label: "setting.search.match.similar.label", value: "similar" },
+      ],
+    },
+    {
+      key: "orderby",
+      label: "setting.search.orderby.label",
+      defaultValue: orderBy ? orderBy : "rank",
+      widget: "select",
+      options: [
+        { label: "setting.search.orderby.rank.label", value: "rank" },
+        { label: "setting.search.orderby.paragraph.label", value: "paragraph" },
+      ],
+    },
+  ];
+  return (
+    <Popover
+      overlayStyle={{ width: 300 }}
+      placement="bottom"
+      arrowPointAtCenter
+      content={
+        <>
+          {searchSetting.map((item) => (
+            <SettingItem
+              data={item}
+              onChange={(key: string, value: string | number | boolean) => {
+                if (typeof onChange !== "undefined") {
+                  onChange(key, value);
+                }
+              }}
+            />
+          ))}
+        </>
+      }
+      trigger="click"
+    >
+      <Link>{trigger}</Link>
+    </Popover>
+  );
+};
+
+export default Widget;

+ 117 - 0
dashboard/src/components/fts/FullSearchInput.tsx

@@ -0,0 +1,117 @@
+import { AutoComplete, Badge, Input, Typography } from "antd";
+import { SizeType } from "antd/lib/config-provider/SizeContext";
+import { useState } from "react";
+import { get } from "../../request";
+
+const { Text } = Typography;
+
+export interface IWordIndexData {
+  word: string;
+  count: number;
+  bold: number;
+}
+export interface IWordIndexListResponse {
+  ok: boolean;
+  message: string;
+  data: {
+    rows: IWordIndexData[];
+    count: number;
+  };
+}
+
+interface ValueType {
+  key?: string;
+  label: React.ReactNode;
+  value: string | number;
+}
+interface IWidget {
+  value?: string;
+  tags?: string[];
+  book?: number;
+  para?: number;
+  size?: SizeType;
+  width?: string | number;
+  onSearch?: Function;
+  onSplit?: Function;
+}
+const Widget = ({
+  value,
+  onSplit,
+  tags,
+  size = "middle",
+  width,
+  onSearch,
+}: IWidget) => {
+  const [options, setOptions] = useState<ValueType[]>([]);
+  const [input, setInput] = useState<string | undefined>(value);
+
+  const renderItem = (word: string, count: number, bold: number) => ({
+    value: word,
+    label: (
+      <div>
+        <div
+          style={{
+            display: "flex",
+            justifyContent: "space-between",
+          }}
+        >
+          <span>{bold > 0 ? <Text strong>{word}</Text> : word}</span>
+          <Badge color="geekblue" count={count} />
+        </div>
+      </div>
+    ),
+  });
+  const search = (value: string) => {
+    console.log("search", value);
+    if (value === "") {
+      return;
+    }
+
+    get<IWordIndexListResponse>(
+      `/v2/pali-word-index?view=key&key=${value}`
+    ).then((json) => {
+      const words: ValueType[] = json.data.rows.map((item) => {
+        return renderItem(item.word, item.count, item.bold);
+      });
+      setOptions(words);
+    });
+  };
+  return (
+    <>
+      <AutoComplete
+        style={{ width: width }}
+        value={input}
+        popupClassName="certain-category-search-dropdown"
+        dropdownMatchSelectWidth={400}
+        options={options}
+        onChange={(value: string, option: ValueType | ValueType[]) => {
+          console.log("input", value);
+          setInput(value);
+        }}
+        onSearch={(value: string) => {
+          console.log("auto complete on search", value, tags);
+          search(value);
+        }}
+        onSelect={(value: string, option: ValueType) => {
+          if (typeof onSearch !== "undefined") {
+            onSearch(value);
+          }
+        }}
+      >
+        <Input.Search
+          size={size}
+          width={width}
+          placeholder="input here"
+          onSearch={(value: string) => {
+            console.log("on search", value, tags);
+            if (typeof onSearch !== "undefined") {
+              onSearch(value);
+            }
+          }}
+        />
+      </AutoComplete>
+    </>
+  );
+};
+
+export default Widget;

+ 140 - 0
dashboard/src/components/fts/FullTextSearchResult.tsx

@@ -0,0 +1,140 @@
+import { List, Space, Tag, Typography } from "antd";
+import { useEffect, useState } from "react";
+import { Link } from "react-router-dom";
+
+import { get } from "../../request";
+import TocPath, { ITocPathNode } from "../corpus/TocPath";
+import Marked from "../general/Marked";
+import "./search.css";
+
+const { Title, Text } = Typography;
+
+interface IFtsData {
+  rank: number;
+  highlight: string;
+  book: number;
+  paragraph: number;
+  content: string;
+  title?: string;
+  paliTitle?: string;
+  path?: ITocPathNode[];
+}
+interface IFtsResponse {
+  ok: boolean;
+  string: string;
+  data: {
+    rows: IFtsData[];
+    count: number;
+  };
+}
+interface IFtsItem {
+  book: number;
+  paragraph: number;
+  title?: string;
+  paliTitle?: string;
+  content?: string;
+  path?: ITocPathNode[];
+}
+interface IWidget {
+  keyWord?: string;
+  tags?: string[];
+  bookId?: string | null;
+  book?: number;
+  para?: number;
+  orderBy?: string | null;
+  match?: string | null;
+  keyWord2?: string;
+}
+const Widget = ({
+  keyWord,
+  tags,
+  bookId,
+  book,
+  para,
+  orderBy,
+  match,
+  keyWord2,
+}: IWidget) => {
+  const [ftsData, setFtsData] = useState<IFtsItem[]>();
+
+  const [total, setTotal] = useState<number>();
+  const [currPage, setCurrPage] = useState<number>(1);
+
+  useEffect(() => {
+    let url = `/v2/search?key=${keyWord}`;
+    if (typeof tags !== "undefined") {
+      url += `&tags=${tags}`;
+    }
+    if (bookId) {
+      url += `&book=${bookId}`;
+    }
+    if (orderBy) {
+      url += `&orderby=${orderBy}`;
+    }
+    if (match) {
+      url += `&match=${match}`;
+    }
+    const offset = (currPage - 1) * 10;
+    url += `&limit=10&offset=${offset}`;
+    console.log("fetch url", url);
+    get<IFtsResponse>(url).then((json) => {
+      if (json.ok) {
+        const result: IFtsItem[] = json.data.rows.map((item) => {
+          return {
+            book: item.book,
+            paragraph: item.paragraph,
+            title: item.title ? item.title : item.paliTitle,
+            paliTitle: item.paliTitle,
+            content: item.highlight,
+            path: item.path,
+          };
+        });
+        setFtsData(result);
+        setTotal(json.data.count);
+      }
+    });
+  }, [bookId, currPage, keyWord, match, orderBy, tags]);
+  return (
+    <List
+      itemLayout="vertical"
+      size="small"
+      dataSource={ftsData}
+      pagination={{
+        onChange: (page) => {
+          console.log(page);
+          setCurrPage(page);
+        },
+        showQuickJumper: true,
+        showSizeChanger: false,
+        pageSize: 10,
+        total: total,
+        position: "both",
+        showTotal: (total) => {
+          return `结果: ${total}`;
+        },
+      }}
+      renderItem={(item) => (
+        <List.Item>
+          <Title level={5}>
+            <Link to={`/article/para?book=${item.book}&par=${item.paragraph}`}>
+              {item.title}
+            </Link>
+          </Title>
+          <div>
+            <Text type="secondary">{item.paliTitle}</Text>
+          </div>
+          <Space>
+            <TocPath data={item.path} />
+            {"/"}
+            <Tag>{item.paragraph}</Tag>
+          </Space>
+          <div className="search_content">
+            <Marked text={item.content} />
+          </div>
+        </List.Item>
+      )}
+    />
+  );
+};
+
+export default Widget;

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

@@ -0,0 +1,5 @@
+.search_content del {
+  background-color: yellow;
+  color: black;
+  text-decoration: none;
+}

+ 20 - 0
dashboard/src/pages/library/search/index.tsx

@@ -0,0 +1,20 @@
+import { Layout } from "antd";
+
+import HeadBar from "../../../components/library/HeadBar";
+import FooterBar from "../../../components/library/FooterBar";
+import { Outlet } from "react-router-dom";
+
+const Widget = () => {
+  // TODO
+  return (
+    <>
+      <Layout>
+        <HeadBar />
+        <Outlet />
+        <FooterBar />
+      </Layout>
+    </>
+  );
+};
+
+export default Widget;

+ 113 - 0
dashboard/src/pages/library/search/search.tsx

@@ -0,0 +1,113 @@
+import { useNavigate, useParams, useSearchParams } from "react-router-dom";
+import { useEffect, useState } from "react";
+import { Row, Col, Breadcrumb, Space } from "antd";
+import FullSearchInput from "../../../components/fts/FullSearchInput";
+import BookTree from "../../../components/corpus/BookTree";
+import FullTextSearchResult from "../../../components/fts/FullTextSearchResult";
+import FtsBookList from "../../../components/fts/FtsBookList";
+import FtsSetting from "../../../components/fts/FtsSetting";
+
+const Widget = () => {
+  const { key } = useParams();
+  const [searchParams, setSearchParams] = useSearchParams();
+  const [bookRoot, setBookRoot] = useState("default");
+  const [bookPath, setBookPath] = useState<string[]>([]);
+  const navigate = useNavigate();
+
+  useEffect(() => {}, [key, searchParams]);
+
+  useEffect(() => {
+    let currRoot: string | null;
+    currRoot = localStorage.getItem("pali_path_root");
+    if (currRoot === null) {
+      currRoot = "default";
+    }
+    setBookRoot(currRoot);
+  }, []);
+  // TODO
+  return (
+    <>
+      <Row>
+        <Col flex="auto"></Col>
+        <Col flex="1440px">
+          <Row>
+            <Col xs={0} sm={6} md={5}>
+              <BookTree
+                root={bookRoot}
+                path={bookPath}
+                onChange={(key: string, path: string[]) => {
+                  console.log("key", key);
+                  if (key === "") {
+                    searchParams.delete("tags");
+                  } else {
+                    searchParams.set("tags", key);
+                  }
+                  searchParams.delete("book");
+                  setSearchParams(searchParams);
+                  setBookPath(path);
+                }}
+              />
+            </Col>
+            <Col xs={24} sm={18} md={14}>
+              <Space direction="vertical" style={{ padding: 10 }}>
+                <Space>
+                  <FullSearchInput
+                    size="large"
+                    width={"500px"}
+                    value={key}
+                    tags={searchParams.get("tags")?.split(",")}
+                    onSearch={(value: string) => {
+                      navigate(`/search/key/${value}`);
+                    }}
+                  />
+                  <FtsSetting
+                    trigger="高级"
+                    orderBy={searchParams.get("orderby")}
+                    match={searchParams.get("match")}
+                    onChange={(
+                      key: string,
+                      value: string | number | boolean
+                    ) => {
+                      searchParams.set(key, value.toString());
+                      setSearchParams(searchParams);
+                    }}
+                  />
+                </Space>
+                <Breadcrumb>
+                  {bookPath.map((item, id) => (
+                    <Breadcrumb.Item key={id}>{item}</Breadcrumb.Item>
+                  ))}
+                </Breadcrumb>
+                <FullTextSearchResult
+                  keyWord={key}
+                  tags={searchParams.get("tags")?.split(",")}
+                  bookId={searchParams.get("book")}
+                  orderBy={searchParams.get("orderby")}
+                  match={searchParams.get("match")}
+                />
+              </Space>
+            </Col>
+            <Col xs={0} sm={0} md={5}>
+              <FtsBookList
+                keyWord={key}
+                tags={searchParams.get("tags")?.split(",")}
+                match={searchParams.get("match")}
+                onSelect={(bookId: number) => {
+                  if (bookId !== 0) {
+                    searchParams.set("book", bookId.toString());
+                  } else {
+                    searchParams.delete("book");
+                  }
+                  setSearchParams(searchParams);
+                }}
+              />
+            </Col>
+          </Row>
+        </Col>
+        <Col flex="auto"></Col>
+      </Row>
+    </>
+  );
+};
+
+export default Widget;