فهرست منبع

Merge pull request #1039 from visuddhinanda/agile

:sparkles: 添加鼠标移入自动查词
visuddhinanda 3 سال پیش
والد
کامیت
912cb4edbd
33فایلهای تغییر یافته به همراه1195 افزوده شده و 624 حذف شده
  1. 9 4
      dashboard/src/components/api/Channel.ts
  2. 27 0
      dashboard/src/components/api/Corpus.ts
  3. 34 34
      dashboard/src/components/api/Dict.ts
  4. 9 2
      dashboard/src/components/article/Article.tsx
  5. 12 1
      dashboard/src/components/article/ArticleCard.tsx
  6. 6 3
      dashboard/src/components/channel/Channel.tsx
  7. 116 109
      dashboard/src/components/corpus/BookTreeList.tsx
  8. 61 77
      dashboard/src/components/corpus/ChapterList.tsx
  9. 3 3
      dashboard/src/components/dict/DictEdit.tsx
  10. 1 2
      dashboard/src/components/dict/SelectCase.tsx
  11. 146 150
      dashboard/src/components/library/HeadBar.tsx
  12. 0 1
      dashboard/src/components/template/MdView.tsx
  13. 2 3
      dashboard/src/components/template/SentEdit.tsx
  14. 6 1
      dashboard/src/components/template/SentEdit/SentContent.tsx
  15. 14 11
      dashboard/src/components/template/SentEdit/SuggestionTabs.tsx
  16. 84 34
      dashboard/src/components/template/Wbw/WbwCase.tsx
  17. 1 1
      dashboard/src/components/template/Wbw/WbwDetail.tsx
  18. 57 12
      dashboard/src/components/template/Wbw/WbwDetailBasic.tsx
  19. 62 7
      dashboard/src/components/template/Wbw/WbwFactorMeaning.tsx
  20. 49 16
      dashboard/src/components/template/Wbw/WbwFactors.tsx
  21. 55 25
      dashboard/src/components/template/Wbw/WbwMeaning.tsx
  22. 16 0
      dashboard/src/components/template/Wbw/WbwPage.tsx
  23. 45 19
      dashboard/src/components/template/Wbw/WbwPali.tsx
  24. 17 0
      dashboard/src/components/template/Wbw/WbwPara.tsx
  25. 149 50
      dashboard/src/components/template/Wbw/WbwWord.tsx
  26. 27 2
      dashboard/src/components/template/Wbw/wbw.css
  27. 31 5
      dashboard/src/components/template/WbwSent.tsx
  28. 1 0
      dashboard/src/locales/zh-Hans/buttons.ts
  29. 7 1
      dashboard/src/locales/zh-Hans/dict/index.ts
  30. 32 0
      dashboard/src/reducers/article-mode.ts
  31. 38 0
      dashboard/src/reducers/inline-dict.ts
  32. 4 0
      dashboard/src/store.ts
  33. 74 51
      dashboard/src/utils.ts

+ 9 - 4
dashboard/src/components/api/Channel.ts

@@ -1,19 +1,24 @@
 import { IStudioApiResponse, Role } from "./Auth";
-
+export type TChannelType =
+  | "translation"
+  | "nissaya"
+  | "original"
+  | "wbw"
+  | "commentary";
 export interface IChannelApiData {
   id: string;
   name: string;
-  type: string;
+  type: TChannelType;
 }
 
-export type ChannelInfoProps = {
+export interface ChannelInfoProps {
   channelName: string;
   channelId: string;
   channelType: string;
   studioName: string;
   studioId: string;
   studioType: string;
-};
+}
 
 export type IFinal = [number, boolean];
 export interface IApiResponseChannelData {

+ 27 - 0
dashboard/src/components/api/Corpus.ts

@@ -1,5 +1,6 @@
 import { IUser } from "../auth/User";
 import { IChannel } from "../channel/Channel";
+import { TagNode } from "../tag/TagArea";
 
 export interface IApiPaliChapterList {
   id: string;
@@ -152,3 +153,29 @@ export interface IPaliTocListResponse {
   message: string;
   data: { rows: IPaliToc[]; count: number };
 }
+
+export interface IPaliBookListResponse {
+  name: string;
+  tag: string[];
+  children?: IPaliBookListResponse[];
+}
+
+export interface IChapterData {
+  title: string;
+  toc: string;
+  book: number;
+  para: number;
+  path: string;
+  tags: TagNode[];
+  channel: { name: string; owner_uid: string };
+  summary: string;
+  view: number;
+  like: number;
+  created_at: string;
+  updated_at: string;
+}
+export interface IChapterListResponse {
+  ok: boolean;
+  message: string;
+  data: { rows: IChapterData[]; count: number };
+}

+ 34 - 34
dashboard/src/components/api/Dict.ts

@@ -1,41 +1,41 @@
-export interface IDictlDataRequest {
-	id: number;
-	word: string;
-	type: string;
-	grammar: string;
-	mean: string;
-	parent: string;
-	note: string;
-	factors: string;
-	factormean: string;
-	language: string;
-	confidence: number;
+export interface IDictDataRequest {
+  id: number;
+  word: string;
+  type: string;
+  grammar: string;
+  mean: string;
+  parent: string;
+  note: string;
+  factors: string;
+  factormean: string;
+  language: string;
+  confidence: number;
 }
 export interface IApiResponseDictlData {
-	id: number;
-	word: string;
-	type: string;
-	grammar: string;
-	mean: string;
-	parent: string;
-	note: string;
-	factors: string;
-	factormean: string;
-	language: string;
-	confidence: number;
-	creator_id: number;
-	updated_at: string;
+  id: number;
+  word: string;
+  type: string;
+  grammar: string;
+  mean: string;
+  parent: string;
+  note: string;
+  factors: string;
+  factormean: string;
+  language: string;
+  confidence: number;
+  creator_id: number;
+  updated_at: string;
 }
 export interface IApiResponseDict {
-	ok: boolean;
-	message: string;
-	data: IApiResponseDictlData;
+  ok: boolean;
+  message: string;
+  data: IApiResponseDictlData;
 }
 export interface IApiResponseDictList {
-	ok: boolean;
-	message: string;
-	data: {
-		rows: IApiResponseDictlData[];
-		count: number;
-	};
+  ok: boolean;
+  message: string;
+  data: {
+    rows: IApiResponseDictlData[];
+    count: number;
+  };
 }

+ 9 - 2
dashboard/src/components/article/Article.tsx

@@ -4,7 +4,7 @@ import { get } from "../../request";
 import { IArticleDataResponse, IArticleResponse } from "../api/Article";
 import ArticleView from "./ArticleView";
 
-export type ArticleMode = "read" | "edit";
+export type ArticleMode = "read" | "edit" | "wbw";
 export type ArticleType =
   | "article"
   | "chapter"
@@ -26,6 +26,7 @@ const Widget = ({
   active = false,
 }: IWidgetArticle) => {
   const [articleData, setArticleData] = useState<IArticleDataResponse>();
+  const [articleMode, setArticleMode] = useState<ArticleMode>(mode);
   let channels: string[] = [];
   if (typeof articleId !== "undefined") {
     const aId = articleId.split("_");
@@ -38,6 +39,12 @@ const Widget = ({
     if (!active) {
       return;
     }
+    if (mode !== "read" && articleMode !== "read") {
+      setArticleMode(mode);
+      console.log("set mode", mode, articleMode);
+      return;
+    }
+    setArticleMode(mode);
     if (typeof type !== "undefined" && typeof articleId !== "undefined") {
       get<IArticleResponse>(`/v2/${type}/${articleId}/${mode}`).then((json) => {
         if (json.ok) {
@@ -47,7 +54,7 @@ const Widget = ({
         }
       });
     }
-  }, [active, type, articleId, mode]);
+  }, [active, type, articleId, mode, articleMode]);
   return (
     <ArticleView
       id={articleData?.uid}

+ 12 - 1
dashboard/src/components/article/ArticleCard.tsx

@@ -5,6 +5,9 @@ import { IWidgetArticleData } from "./ArticleView";
 import { useIntl } from "react-intl";
 import { useState } from "react";
 import ArticleCardMainMenu from "./ArticleCardMainMenu";
+import store from "../../store";
+import { modeChange } from "../../reducers/article-mode";
+import { ArticleMode } from "./Article";
 
 interface IWidgetArticleCard {
   type?: string;
@@ -58,13 +61,21 @@ const Widget = ({
           label: intl.formatMessage({ id: "buttons.edit" }),
           value: "edit",
         },
+        {
+          label: intl.formatMessage({ id: "buttons.wbw" }),
+          value: "wbw",
+        },
       ]}
       value={mode}
       onChange={(value) => {
         if (typeof onModeChange !== "undefined") {
-          onModeChange(value.toString());
+          if (mode === "read" || value.toString() === "read") {
+            onModeChange(value.toString());
+          }
         }
         setMode(value.toString());
+        //发布mode变更
+        store.dispatch(modeChange(value.toString() as ArticleMode));
       }}
     />
   );

+ 6 - 3
dashboard/src/components/channel/Channel.tsx

@@ -1,9 +1,12 @@
+import { TChannelType } from "../api/Channel";
+
 export interface IChannel {
-	name: string;
-	id: string;
+  name: string;
+  id: string;
+  type?: TChannelType;
 }
 const Widget = ({ name, id }: IChannel) => {
-	return <span>{name}</span>;
+  return <span>{name}</span>;
 };
 
 export default Widget;

+ 116 - 109
dashboard/src/components/corpus/BookTreeList.tsx

@@ -2,128 +2,135 @@ import { Link } from "react-router-dom";
 import { useState, useEffect } from "react";
 import { List, Breadcrumb, Card, Select, Space } from "antd";
 import { PaliToEn } from "../../utils";
+import { get } from "../../request";
+import { IPaliBookListResponse } from "../api/Corpus";
 const { Option } = Select;
 
 interface IWidgetBookTreeList {
-	root?: string;
-	path?: string[];
-	onChange?: Function;
+  root?: string;
+  path?: string[];
+  onChange?: Function;
 }
 export interface IEventBookTreeOnchange {
-	path: string[];
-	tag: string[];
+  path: string[];
+  tag: string[];
 }
 const Widget = (prop: IWidgetBookTreeList) => {
-	let treeData: NewTree[] = [];
-	let currRoot = prop.root;
-	const defuaultData: NewTree[] = [];
-	const [currData, setCurrData] = useState(defuaultData);
+  let treeData: NewTree[] = [];
+  let currRoot = prop.root;
+  const defuaultData: NewTree[] = [];
+  const [currData, setCurrData] = useState(defuaultData);
 
-	const defaultPath: pathData[] = prop.path
-		? prop.path.map((item) => {
-				return { to: item, title: item };
-		  })
-		: [];
-	const [bookPath, setBookPath] = useState(defaultPath);
+  const defaultPath: pathData[] = prop.path
+    ? prop.path.map((item) => {
+        return { to: item, title: item };
+      })
+    : [];
+  const [bookPath, setBookPath] = useState(defaultPath);
 
-	useEffect(() => {
-		if (prop.root) fetchBookTree(prop.root);
-	}, [prop.root]);
+  useEffect(() => {
+    if (prop.root) fetchBookTree(prop.root);
+  }, [prop.root]);
 
-	type OrgTree = {
-		name: string;
-		tag: string[];
-		children: OrgTree[];
-	};
-	type NewTree = {
-		title: string;
-		dir: string;
-		key: string;
-		tag: string[];
-		children: NewTree[];
-	};
+  type OrgTree = {
+    name: string;
+    tag: string[];
+    children: OrgTree[];
+  };
+  type NewTree = {
+    title: string;
+    dir: string;
+    key: string;
+    tag: string[];
+    children: NewTree[];
+  };
 
-	function fetchBookTree(value: string) {
-		function treeMap(params: OrgTree): NewTree {
-			return {
-				title: params.name,
-				dir: PaliToEn(params.name),
-				key: params.tag.join(),
-				tag: params.tag,
-				children: Array.isArray(params.children) ? params.children.map(treeMap) : [],
-			};
-		}
-		let url = `http://127.0.0.1:8000/api/v2/palibook/${value}`;
-		fetch(url)
-			.then(function (response) {
-				console.log("ajex:", response);
-				return response.json();
-			})
-			.then(function (myJson) {
-				console.log("ajex", myJson);
-				treeData = myJson.map(treeMap);
-				setCurrData(treeData);
-			});
-	}
+  function fetchBookTree(category: string) {
+    function treeMap(params: IPaliBookListResponse): NewTree {
+      return {
+        title: params.name,
+        dir: PaliToEn(params.name),
+        key: params.tag.join(),
+        tag: params.tag,
+        children: Array.isArray(params.children)
+          ? params.children.map(treeMap)
+          : [],
+      };
+    }
 
-	interface pathData {
-		to: string;
-		title: string;
-	}
+    get<IPaliBookListResponse[]>(`/v2/palibook/${category}`).then((json) => {
+      console.log("ajax", json);
+      treeData = json.map(treeMap);
+      setCurrData(treeData);
+    });
+  }
 
-	function pushDir(dir: string, title: string, tag: string[]): void {
-		const newPath: string = bookPath.length > 0 ? bookPath.slice(-1)[0].to + "-" + dir : dir;
-		bookPath.push({ to: newPath, title: title });
-		setBookPath(bookPath);
-		if (prop.onChange) {
-			prop.onChange({
-				path: newPath.split("-"),
-				tag: tag,
-			});
-		}
-	}
-	const handleChange = (value: string) => {
-		console.log(`selected ${value}`);
-		fetchBookTree(value);
-		currRoot = value;
-		setBookPath([]);
-	};
-	// TODO
-	return (
-		<>
-			<Space>
-				<Select style={{ width: 90 }} defaultValue={prop.root} loading={false} onChange={handleChange}>
-					<Option value="defualt">Defualt</Option>
-					<Option value="cscd">CSCD</Option>
-				</Select>
-				<Breadcrumb>
-					{bookPath.map((item, id) => {
-						return (
-							<Breadcrumb.Item key={id}>
-								<Link to={`/palicanon/list/${currRoot}/${item.to}`}>{item.title}</Link>
-							</Breadcrumb.Item>
-						);
-					})}
-				</Breadcrumb>
-			</Space>
-			<Card>
-				<List
-					dataSource={currData}
-					renderItem={(item) => (
-						<List.Item
-							onClick={() => {
-								console.log("click", item.title);
-								setCurrData(item.children);
-								pushDir(item.dir, item.title, item.tag);
-							}}
-						>
-							{item.title}
-						</List.Item>
-					)}
-				/>
-			</Card>
-		</>
-	);
+  interface pathData {
+    to: string;
+    title: string;
+  }
+
+  function pushDir(dir: string, title: string, tag: string[]): void {
+    const newPath: string =
+      bookPath.length > 0 ? bookPath.slice(-1)[0].to + "-" + dir : dir;
+    bookPath.push({ to: newPath, title: title });
+    setBookPath(bookPath);
+    if (prop.onChange) {
+      prop.onChange({
+        path: newPath.split("-"),
+        tag: tag,
+      });
+    }
+  }
+  const handleChange = (value: string) => {
+    console.log(`selected ${value}`);
+    fetchBookTree(value);
+    currRoot = value;
+    setBookPath([]);
+  };
+  // TODO
+  return (
+    <>
+      <Space>
+        <Select
+          style={{ width: 90 }}
+          defaultValue={prop.root}
+          loading={false}
+          onChange={handleChange}
+        >
+          <Option value="defualt">Defualt</Option>
+          <Option value="cscd">CSCD</Option>
+        </Select>
+        <Breadcrumb>
+          {bookPath.map((item, id) => {
+            return (
+              <Breadcrumb.Item key={id}>
+                <Link to={`/palicanon/list/${currRoot}/${item.to}`}>
+                  {item.title}
+                </Link>
+              </Breadcrumb.Item>
+            );
+          })}
+        </Breadcrumb>
+      </Space>
+      <Card>
+        <List
+          dataSource={currData}
+          renderItem={(item) => (
+            <List.Item
+              onClick={() => {
+                console.log("click", item.title);
+                setCurrData(item.children);
+                pushDir(item.dir, item.title, item.tag);
+              }}
+            >
+              {item.title}
+            </List.Item>
+          )}
+        />
+      </Card>
+    </>
+  );
 };
 
 export default Widget;

+ 61 - 77
dashboard/src/components/corpus/ChapterList.tsx

@@ -3,94 +3,78 @@ import { List } from "antd";
 import ChapterCard from "./ChapterCard";
 import type { ChapterData } from "./ChapterCard";
 import type { ChannelFilterProps } from "../channel/ChannelList";
+import { IChapterData, IChapterListResponse } from "../api/Corpus";
+import { get } from "../../request";
 
 const defaultChannelFilterProps: ChannelFilterProps = {
-	chapterProgress: 0.9,
-	lang: "en",
-	channelType: "translation",
+  chapterProgress: 0.9,
+  lang: "en",
+  channelType: "translation",
 };
 
 interface IWidgetChannelList {
-	filter?: ChannelFilterProps;
-	tags?: string[];
+  filter?: ChannelFilterProps;
+  tags?: string[];
 }
-const defaultData: ChapterData[] = [];
 
-interface IChapterData {
-	title: string;
-	toc: string;
-	book: number;
-	para: number;
-	path: string;
-	tags: string;
-	channel: { name: string; owner_uid: string };
-	summary: string;
-	view: number;
-	like: number;
-	created_at: string;
-	updated_at: string;
-}
-
-const Widget = ({ filter = defaultChannelFilterProps, tags = [] }: IWidgetChannelList) => {
-	const [tableData, setTableData] = useState(defaultData);
+const Widget = ({
+  filter = defaultChannelFilterProps,
+  tags = [],
+}: IWidgetChannelList) => {
+  const [tableData, setTableData] = useState<ChapterData[]>([]);
 
-	useEffect(() => {
-		console.log("useEffect");
+  useEffect(() => {
+    console.log("useEffect");
 
-		fetchData(filter, tags);
-	}, [tags, filter]);
+    fetchData(filter, tags);
+  }, [tags, filter]);
 
-	function fetchData(filter: ChannelFilterProps, tags: string[]) {
-		const strTags = tags.length > 0 ? "&tags=" + tags.join() : "";
-		console.log("strtags", strTags);
-		let url = `http://127.0.0.1:8000/api/v2/progress?view=chapter${strTags}`;
-		fetch(url)
-			.then(function (response) {
-				console.log("ajex:", response);
-				return response.json();
-			})
-			.then(function (myJson) {
-				console.log("ajex", myJson);
-				let newTree = myJson.data.rows.map((item: IChapterData) => {
-					return {
-						Title: item.title,
-						PaliTitle: item.toc,
-						Path: item.path,
-						Book: item.book,
-						Paragraph: item.para,
-						Summary: item.summary,
-						Tag: item.tags,
-						Channel: {
-							ChannelName: item.channel.name,
-							ChannelId: "",
-							ChannelType: "translation",
-							StudioName: item.channel.name,
-							StudioId: item.channel.owner_uid,
-							StudioType: "",
-						},
-						CreatedAt: item.created_at,
-						UpdatedAt: item.updated_at,
-						Hit: item.view,
-						Like: item.like,
-						ChannelInfo: "string",
-					};
-				});
-				setTableData(newTree);
-			});
-	}
+  function fetchData(filter: ChannelFilterProps, tags: string[]) {
+    const strTags = tags.length > 0 ? "&tags=" + tags.join() : "";
+    get<IChapterListResponse>(`/v2/progress?view=chapter${strTags}`).then(
+      (json) => {
+        console.log("ajax", json);
+        let newTree = json.data.rows.map((item: IChapterData) => {
+          return {
+            Title: item.title,
+            PaliTitle: item.toc,
+            Path: item.path,
+            Book: item.book,
+            Paragraph: item.para,
+            Summary: item.summary,
+            Tag: item.tags,
+            Channel: {
+              channelName: item.channel.name,
+              channelId: "",
+              channelType: "translation",
+              studioName: item.channel.name,
+              studioId: item.channel.owner_uid,
+              studioType: "",
+            },
+            CreatedAt: item.created_at,
+            UpdatedAt: item.updated_at,
+            Hit: item.view,
+            Like: item.like,
+            ChannelInfo: "string",
+          };
+        });
+        setTableData(newTree);
+      }
+    );
+  }
 
-	return (
-		<List
-			itemLayout="vertical"
-			size="large"
-			dataSource={tableData}
-			renderItem={(item) => (
-				<List.Item>
-					<ChapterCard data={item} />
-				</List.Item>
-			)}
-		/>
-	);
+  return (
+    <List
+      itemLayout="vertical"
+      size="large"
+      dataSource={tableData}
+      renderItem={(item) => (
+        <List.Item>
+          <ChapterCard data={item} />
+        </List.Item>
+      )}
+    />
+  );
 };
 
 export default Widget;

+ 3 - 3
dashboard/src/components/dict/DictEdit.tsx

@@ -5,7 +5,7 @@ import { message } from "antd";
 
 import DictEditInner from "./DictEditInner";
 import { IDictFormData } from "./DictCreate";
-import { IApiResponseDict, IDictlDataRequest } from "../api/Dict";
+import { IApiResponseDict, IDictDataRequest } from "../api/Dict";
 import { get, put } from "../../request";
 import { useEffect } from "react";
 
@@ -22,7 +22,7 @@ const Widget = (prop: IWidgetDictEdit) => {
         onFinish={async (values: IDictFormData) => {
           // TODO
           console.log(values);
-          const request: IDictlDataRequest = {
+          const request: IDictDataRequest = {
             id: values.id,
             word: values.word,
             type: values.type,
@@ -35,7 +35,7 @@ const Widget = (prop: IWidgetDictEdit) => {
             language: values.lang,
             confidence: values.confidence,
           };
-          const res = await put<IDictlDataRequest, IApiResponseDict>(
+          const res = await put<IDictDataRequest, IApiResponseDict>(
             `/v2/userdict/${prop.wordId}`,
             request
           );

+ 1 - 2
dashboard/src/components/dict/SelectCase.tsx

@@ -233,11 +233,10 @@ const Widget = ({ defaultValue, onCaseChange }: IWidget) => {
       onCaseChange(value);
     }
   };
-
+  console.log("case", defaultValue);
   return (
     <Cascader
       options={options}
-      defaultValue={defaultValue}
       placeholder="Please select case"
       onChange={onChange}
     />

+ 146 - 150
dashboard/src/components/library/HeadBar.tsx

@@ -12,161 +12,157 @@ import ToStudio from "../auth/ToStudio";
 const { Header } = Layout;
 
 const onClick: MenuProps["onClick"] = (e) => {
-	console.log("click ", e);
+  console.log("click ", e);
 };
 
 type IWidgetHeadBar = {
-	selectedKeys?: string;
+  selectedKeys?: string;
 };
 const Widget = ({ selectedKeys = "" }: IWidgetHeadBar) => {
-	//Library head bar
-	const intl = useIntl(); //i18n
-	// TODO
-	const items: MenuProps["items"] = [
-		{
-			label: (
-				<Link to="/community/list">
-					{intl.formatMessage({
-						id: "columns.library.community.title",
-					})}
-				</Link>
-			),
-			key: "community",
-		},
-		{
-			label: (
-				<Link to="/palicanon/list">
-					{intl.formatMessage({
-						id: "columns.library.palicanon.title",
-					})}
-				</Link>
-			),
-			key: "palicanon",
-		},
-		{
-			label: (
-				<Link to="/course/list">
-					{intl.formatMessage({ id: "columns.library.course.title" })}
-				</Link>
-			),
-			key: "course",
-		},
-		{
-			label: (
-				<Link to="/dict/recent">
-					{intl.formatMessage({ id: "columns.library.dict.title" })}
-				</Link>
-			),
-			key: "dict",
-		},
-		{
-			label: (
-				<Link to="/anthology/list">
-					{intl.formatMessage({
-						id: "columns.library.anthology.title",
-					})}
-				</Link>
-			),
-			key: "anthology",
-		},
-		{
-			label: (
-				<a
-					href="https://asset-hk.wikipali.org/help/zh-Hans"
-					target="_blank"
-					rel="noreferrer"
-				>
-					{intl.formatMessage({ id: "columns.library.help.title" })}
-				</a>
-			),
-			key: "help",
-		},
-		{
-			label: (
-				<Space>
-					{intl.formatMessage({ id: "columns.library.more.title" })}
-				</Space>
-			),
-			key: "more",
-			children: [
-				{
-					label: (
-						<a
-							href="https://asset-hk.wikipali.org/handbook/zh-Hans"
-							target="_blank"
-							rel="noreferrer"
-						>
-							{intl.formatMessage({
-								id: "columns.library.palihandbook.title",
-							})}
-						</a>
-					),
-					key: "palihandbook",
-				},
-				{
-					label: (
-						<Link to="/calendar">
-							{intl.formatMessage({
-								id: "columns.library.calendar.title",
-							})}
-						</Link>
-					),
-					key: "calendar",
-				},
-				{
-					label: (
-						<Link to="/convertor">
-							{intl.formatMessage({
-								id: "columns.library.convertor.title",
-							})}
-						</Link>
-					),
-					key: "convertor",
-				},
-				{
-					label: (
-						<Link to="/statistics">
-							{intl.formatMessage({
-								id: "columns.library.statistics.title",
-							})}
-						</Link>
-					),
-					key: "statistics",
-				},
-			],
-		},
-	];
-	return (
-		<Header className="header">
-			<Row justify="space-between">
-				<Col flex="100px">
-					<Link to="/">
-						<img
-							alt="code"
-							style={{ height: "3em" }}
-							src={img_banner}
-						/>
-					</Link>
-				</Col>
-				<Col span={8}>
-					<Menu
-						onClick={onClick}
-						selectedKeys={[selectedKeys]}
-						mode="horizontal"
-						theme="dark"
-						items={items}
-					/>
-				</Col>
-				<Col span={4}>
-					<Space>
-						<ToStudio />
-						<SignInAvatar />
-						<UiLangSelect />
-					</Space>
-				</Col>
-			</Row>
-		</Header>
-	);
+  //Library head bar
+  const intl = useIntl(); //i18n
+  // TODO
+  const items: MenuProps["items"] = [
+    {
+      label: (
+        <Link to="/community/list">
+          {intl.formatMessage({
+            id: "columns.library.community.title",
+          })}
+        </Link>
+      ),
+      key: "community",
+    },
+    {
+      label: (
+        <Link to="/palicanon/list">
+          {intl.formatMessage({
+            id: "columns.library.palicanon.title",
+          })}
+        </Link>
+      ),
+      key: "palicanon",
+    },
+    {
+      label: (
+        <Link to="/course/list">
+          {intl.formatMessage({ id: "columns.library.course.title" })}
+        </Link>
+      ),
+      key: "course",
+    },
+    {
+      label: (
+        <Link to="/dict/recent">
+          {intl.formatMessage({ id: "columns.library.dict.title" })}
+        </Link>
+      ),
+      key: "dict",
+    },
+    {
+      label: (
+        <Link to="/anthology/list">
+          {intl.formatMessage({
+            id: "columns.library.anthology.title",
+          })}
+        </Link>
+      ),
+      key: "anthology",
+    },
+    {
+      label: (
+        <a
+          href="https://asset-hk.wikipali.org/help/zh-Hans"
+          target="_blank"
+          rel="noreferrer"
+        >
+          {intl.formatMessage({ id: "columns.library.help.title" })}
+        </a>
+      ),
+      key: "help",
+    },
+    {
+      label: (
+        <Space>
+          {intl.formatMessage({ id: "columns.library.more.title" })}
+        </Space>
+      ),
+      key: "more",
+      children: [
+        {
+          label: (
+            <a
+              href="https://asset-hk.wikipali.org/handbook/zh-Hans"
+              target="_blank"
+              rel="noreferrer"
+            >
+              {intl.formatMessage({
+                id: "columns.library.palihandbook.title",
+              })}
+            </a>
+          ),
+          key: "palihandbook",
+        },
+        {
+          label: (
+            <Link to="/calendar">
+              {intl.formatMessage({
+                id: "columns.library.calendar.title",
+              })}
+            </Link>
+          ),
+          key: "calendar",
+        },
+        {
+          label: (
+            <Link to="/convertor">
+              {intl.formatMessage({
+                id: "columns.library.convertor.title",
+              })}
+            </Link>
+          ),
+          key: "convertor",
+        },
+        {
+          label: (
+            <Link to="/statistics">
+              {intl.formatMessage({
+                id: "columns.library.statistics.title",
+              })}
+            </Link>
+          ),
+          key: "statistics",
+        },
+      ],
+    },
+  ];
+  return (
+    <Header className="header" style={{ lineHeight: "44px", height: 44 }}>
+      <Row justify="space-between">
+        <Col flex="100px">
+          <Link to="/">
+            <img alt="code" style={{ height: "3em" }} src={img_banner} />
+          </Link>
+        </Col>
+        <Col span={8}>
+          <Menu
+            onClick={onClick}
+            selectedKeys={[selectedKeys]}
+            mode="horizontal"
+            theme="dark"
+            items={items}
+          />
+        </Col>
+        <Col span={4}>
+          <Space>
+            <ToStudio />
+            <SignInAvatar />
+            <UiLangSelect />
+          </Space>
+        </Col>
+      </Row>
+    </Header>
+  );
 };
 
 export default Widget;

+ 0 - 1
dashboard/src/components/template/MdView.tsx

@@ -6,7 +6,6 @@ interface IWidget {
   convertor?: TCodeConvertor;
 }
 const Widget = ({ html, wordWidget = false, convertor }: IWidget) => {
-  console.log("word", wordWidget);
   const jsx = XmlToReact(html, wordWidget, convertor);
   return <>{jsx}</>;
 };

+ 2 - 3
dashboard/src/components/template/SentEdit.tsx

@@ -5,9 +5,8 @@ import { IChannel } from "../channel/Channel";
 import SentContent from "./SentEdit/SentContent";
 import SentMenu from "./SentEdit/SentMenu";
 import SentTab from "./SentEdit/SentTab";
-import { suggestion } from "../../reducers/suggestion";
 
-interface ISuggestiongCount {
+interface ISuggestionCount {
   suggestion?: number;
   qa?: number;
 }
@@ -21,7 +20,7 @@ export interface ISentence {
   editor: IUser;
   channel: IChannel;
   updateAt: string;
-  suggestionCount?: ISuggestiongCount;
+  suggestionCount?: ISuggestionCount;
 }
 
 export interface IWidgetSentEditInner {

+ 6 - 1
dashboard/src/components/template/SentEdit/SentContent.tsx

@@ -1,5 +1,6 @@
 import { ISentence } from "../SentEdit";
 import SentCell from "./SentCell";
+import { WbwSentCtl } from "../WbwSent";
 interface IWidgetSentContent {
   origin?: ISentence[];
   translation?: ISentence[];
@@ -14,7 +15,11 @@ const Widget = ({
     <div style={{ display: "flex", flexDirection: layout }}>
       <div style={{ flex: "5", color: "#9f3a01" }}>
         {origin?.map((item, id) => {
-          return <SentCell key={id} data={item} wordWidget={true} />;
+          if (item.channel.type === "wbw") {
+            return <WbwSentCtl key={id} data={JSON.parse(item.content)} />;
+          } else {
+            return <SentCell key={id} data={item} wordWidget={true} />;
+          }
         })}
       </div>
       <div style={{ flex: "5" }}>

+ 14 - 11
dashboard/src/components/template/SentEdit/SuggestionTabs.tsx

@@ -12,14 +12,13 @@ interface IWidget {
 const Widget = ({ data }: IWidget) => {
   const [value, setValue] = useState("close");
   const [showPanel, setShowPanel] = useState("none");
-  const [showSuggestion, setShowSuggestion] = useState("none");
+  const [showSuggestion, setShowSuggestion] = useState(false);
 
   const onChange = ({ target: { value } }: RadioChangeEvent) => {
     console.log("radio1 checked", value);
     switch (value) {
       case "suggestion":
-        setShowSuggestion("block");
-        setShowPanel("block");
+        setShowSuggestion(true);
         break;
     }
     setValue(value);
@@ -60,15 +59,19 @@ const Widget = ({ data }: IWidget) => {
           <Radio value="close" style={{ display: "none" }}></Radio>
         </Radio.Group>
       </div>
-      <div style={{ display: showPanel }}>
-        <div style={{ display: showSuggestion, paddingLeft: "1em" }}>
-          <div>
-            <SuggestionAdd data={data} />
-          </div>
-          <div>
-            <SuggestionList {...data} />
+      <div>
+        {showSuggestion ? (
+          <div style={{ paddingLeft: "1em" }}>
+            <div>
+              <SuggestionAdd data={data} />
+            </div>
+            <div>
+              <SuggestionList {...data} />
+            </div>
           </div>
-        </div>
+        ) : (
+          <></>
+        )}
       </div>
     </div>
   );

+ 84 - 34
dashboard/src/components/template/Wbw/WbwCase.tsx

@@ -1,51 +1,101 @@
 import { useIntl } from "react-intl";
 import { Typography, Button } from "antd";
 import { SwapOutlined } from "@ant-design/icons";
+import type { MenuProps } from "antd";
+import { Dropdown } from "antd";
 
-import { IWbw } from "./WbwWord";
+import { IWbw, TWbwDisplayMode } from "./WbwWord";
+import { PaliReal } from "../../../utils";
 import "./wbw.css";
 
 const { Text } = Typography;
 
+const items: MenuProps["items"] = [
+  {
+    key: "n+m+sg+nom",
+    label: "n+m+sg+nom",
+  },
+  {
+    key: "un",
+    label: "un",
+  },
+];
+
 interface IWidget {
   data: IWbw;
+  display?: TWbwDisplayMode;
   onSplit?: Function;
+  onChange?: Function;
 }
-const Widget = ({ data, onSplit }: IWidget) => {
+const Widget = ({ data, display, onSplit, onChange }: IWidget) => {
   const intl = useIntl();
+  const onClick: MenuProps["onClick"] = (e) => {
+    console.log("click ", e);
+    if (typeof onChange !== "undefined") {
+      onChange(e.key);
+    }
+  };
+
   const showSplit: boolean = data.factors?.value.includes("+") ? true : false;
-  return (
-    <div className="wbw_word_item" style={{ display: "flex" }}>
-      <Text type="secondary">
-        <div>
-          {data.case?.value.map((item, id) => {
-            return (
-              <span key={id} className="case">
-                {intl.formatMessage({
-                  id: `dict.fields.type.${item}.short.label`,
-                })}
-              </span>
-            );
-          })}
-          {showSplit ? (
-            <Button
-              className="wbw_split"
-              size="small"
-              shape="circle"
-              icon={<SwapOutlined />}
-              onClick={() => {
-                if (typeof onSplit !== "undefined") {
-                  onSplit(true);
-                }
-              }}
-            />
-          ) : (
-            <></>
-          )}
-        </div>
-      </Text>
-    </div>
-  );
+  let caseElement: JSX.Element | JSX.Element[] | undefined;
+  if (
+    display === "block" &&
+    (typeof data.case === "undefined" ||
+      data.case.value.length === 0 ||
+      data.case.value[0] === "")
+  ) {
+    //空白的语法信息在逐词解析模式显示占位字符串
+    caseElement = (
+      <span>{intl.formatMessage({ id: "dict.fields.case.label" })}</span>
+    );
+  } else {
+    caseElement = data.case?.value.map((item, id) => {
+      if (item !== "") {
+        return (
+          <span key={id} className="case">
+            {intl.formatMessage({
+              id: `dict.fields.type.${item}.short.label`,
+            })}
+          </span>
+        );
+      } else {
+        return <></>;
+      }
+    });
+  }
+
+  if (typeof data.real !== "undefined" && PaliReal(data.real.value) !== "") {
+    return (
+      <div className="wbw_word_item" style={{ display: "flex" }}>
+        <Text type="secondary">
+          <div>
+            <Dropdown menu={{ items, onClick }} placement="bottomLeft">
+              <span>{caseElement}</span>
+            </Dropdown>
+
+            {showSplit ? (
+              <Button
+                className="wbw_split"
+                size="small"
+                shape="circle"
+                icon={<SwapOutlined />}
+                onClick={() => {
+                  if (typeof onSplit !== "undefined") {
+                    onSplit(true);
+                  }
+                }}
+              />
+            ) : (
+              <></>
+            )}
+          </div>
+        </Text>
+      </div>
+    );
+  } else {
+    //标点符号
+    return <></>;
+  }
 };
 
 export default Widget;

+ 1 - 1
dashboard/src/components/template/Wbw/WbwDetail.tsx

@@ -91,7 +91,7 @@ const Widget = ({ data, onClose, onSave }: IWidget) => {
                 <WbwDetailBasic
                   data={data}
                   onChange={(e: IWbwField) => {
-                    console.log(e);
+                    console.log("WbwDetailBasic onchange", e);
                     fieldChanged(e.field, e.value);
                   }}
                 />

+ 57 - 12
dashboard/src/components/template/Wbw/WbwDetailBasic.tsx

@@ -1,6 +1,6 @@
 import { useState } from "react";
 import { useIntl } from "react-intl";
-import { Divider, Form, Select, Input } from "antd";
+import { Divider, Form, Select, Input, Cascader } from "antd";
 import { Collapse } from "antd";
 
 import SelectCase from "../../dict/SelectCase";
@@ -17,6 +17,13 @@ export interface IWordBasic {
   factorMeaning?: string;
   parent?: string;
 }
+
+interface CascaderOption {
+  value: string | number;
+  label: string;
+  children?: CascaderOption[];
+}
+
 interface IWidget {
   data: IWbw;
   onChange?: Function;
@@ -30,11 +37,49 @@ const Widget = ({ data, onChange }: IWidget) => {
     labelCol: { span: 4 },
     wrapperCol: { span: 20 },
   };
+  const onMeaningChange = (value: string | string[]) => {
+    console.log(`Selected: ${value}`);
+    if (typeof onChange !== "undefined") {
+      if (typeof value === "string") {
+        onChange({ field: "meaning", value: value });
+      } else {
+        onChange({ field: "meaning", value: value.join("$") });
+      }
+    }
+  };
+
+  const options: CascaderOption[] = [
+    {
+      value: "n",
+      label: intl.formatMessage({ id: "dict.fields.type.n.label" }),
+    },
+    {
+      value: "ti",
+      label: intl.formatMessage({ id: "dict.fields.type.ti.label" }),
+    },
+    {
+      value: "v",
+      label: intl.formatMessage({ id: "dict.fields.type.v.label" }),
+    },
+    {
+      value: "ind",
+      label: intl.formatMessage({ id: "dict.fields.type.ind.label" }),
+    },
+    {
+      value: "un",
+      label: intl.formatMessage({ id: "dict.fields.type.un.label" }),
+    },
+    {
+      value: "adj",
+      label: intl.formatMessage({ id: "dict.fields.type.adj.label" }),
+    },
+  ];
 
   return (
     <>
       <Form
         {...formItemLayout}
+        className="wbw_detail_basic"
         name="basic"
         form={form}
         initialValues={{
@@ -42,6 +87,8 @@ const Widget = ({ data, onChange }: IWidget) => {
           factors: data.factors?.value,
           factorMeaning: data.factorMeaning?.value,
           parent: data.parent?.value,
+          case: data.case?.value,
+          case1: data.case?.value,
         }}
       >
         <Form.Item
@@ -52,16 +99,7 @@ const Widget = ({ data, onChange }: IWidget) => {
           <Select
             allowClear
             mode="tags"
-            onChange={(value: string | string[]) => {
-              console.log(`Selected: ${value}`);
-              if (typeof onChange !== "undefined") {
-                if (typeof value === "string") {
-                  onChange({ field: "meaning", value: value });
-                } else {
-                  onChange({ field: "meaning", value: value.join("$") });
-                }
-              }
-            }}
+            onChange={onMeaningChange}
             style={{ width: "100%" }}
             placeholder={intl.formatMessage({
               id: "forms.fields.meaning.label",
@@ -69,7 +107,6 @@ const Widget = ({ data, onChange }: IWidget) => {
             options={items.map((item) => ({ label: item, value: item }))}
             dropdownRender={(menu) => (
               <>
-                {" "}
                 {menu}
                 <Divider style={{ margin: "8px 0" }}>更多</Divider>
                 <WbwMeaningSelect
@@ -87,6 +124,7 @@ const Widget = ({ data, onChange }: IWidget) => {
                     form.setFieldsValue({
                       meaning: currMeanings,
                     });
+                    onMeaningChange(currMeanings);
                   }}
                 />
               </>
@@ -109,6 +147,13 @@ const Widget = ({ data, onChange }: IWidget) => {
           label={intl.formatMessage({ id: "forms.fields.case.label" })}
           tooltip={intl.formatMessage({ id: "forms.fields.case.tooltip" })}
           name="case"
+        >
+          <Cascader options={options} placeholder="Please select case" />
+        </Form.Item>
+        <Form.Item
+          label={intl.formatMessage({ id: "forms.fields.case.label" })}
+          tooltip={intl.formatMessage({ id: "forms.fields.case.tooltip" })}
+          name="case1"
         >
           <SelectCase
             onCaseChange={(value: (string | number)[]) => {

+ 62 - 7
dashboard/src/components/template/Wbw/WbwFactorMeaning.tsx

@@ -1,17 +1,72 @@
+import { useIntl } from "react-intl";
+import type { MenuProps } from "antd";
+import { Dropdown } from "antd";
 import { Typography } from "antd";
 
-import { IWbw } from "./WbwWord";
+import { IWbw, TWbwDisplayMode } from "./WbwWord";
+import { PaliReal } from "../../../utils";
 const { Text } = Typography;
 
+const items: MenuProps["items"] = [
+  {
+    key: "factor1+意思",
+    label: "factor1+意思",
+  },
+  {
+    key: "factor2+意思",
+    label: "factor2+意思",
+  },
+  {
+    key: "factor3+意思",
+    label: "factor3+意思",
+  },
+];
+
 interface IWidget {
   data: IWbw;
+  display?: TWbwDisplayMode;
+  onChange?: Function;
 }
-const Widget = ({ data }: IWidget) => {
-  return (
-    <div>
-      <Text type="secondary">{data.factorMeaning?.value}</Text>
-    </div>
-  );
+const Widget = ({ data, display, onChange }: IWidget) => {
+  const intl = useIntl();
+
+  const onClick: MenuProps["onClick"] = (e) => {
+    console.log("click ", e);
+    if (typeof onChange !== "undefined") {
+      onChange(e.key);
+    }
+  };
+
+  let factorMeaning = <></>;
+  if (
+    display === "block" &&
+    (typeof data.factorMeaning === "undefined" ||
+      data.factorMeaning.value === "")
+  ) {
+    //空白的意思在逐词解析模式显示占位字符串
+    factorMeaning = (
+      <Text type="secondary">
+        {intl.formatMessage({ id: "dict.fields.factormeaning.label" })}
+      </Text>
+    );
+  } else {
+    factorMeaning = <span>{data.factorMeaning?.value}</span>;
+  }
+
+  if (typeof data.real !== "undefined" && PaliReal(data.real.value) !== "") {
+    return (
+      <div>
+        <Text type="secondary">
+          <Dropdown menu={{ items, onClick }} placement="bottomLeft">
+            {factorMeaning}
+          </Dropdown>
+        </Text>
+      </div>
+    );
+  } else {
+    //标点符号
+    return <></>;
+  }
 };
 
 export default Widget;

+ 49 - 16
dashboard/src/components/template/Wbw/WbwFactors.tsx

@@ -1,38 +1,71 @@
+import { useIntl } from "react-intl";
 import type { MenuProps } from "antd";
 import { Dropdown } from "antd";
 import { Typography } from "antd";
 
-import { IWbw } from "./WbwWord";
+import { IWbw, TWbwDisplayMode } from "./WbwWord";
+import { PaliReal } from "../../../utils";
 const { Text } = Typography;
 
 const items: MenuProps["items"] = [
   {
-    key: "1",
-    label: "factors",
+    key: "factor1+word",
+    label: "factor1+word",
   },
   {
-    key: "2",
-    label: "factors",
+    key: "factor2+word",
+    label: "factor2+word",
   },
   {
-    key: "3",
-    label: "factors",
+    key: "factor3+word",
+    label: "factor3+word",
   },
 ];
-
 interface IWidget {
   data: IWbw;
+  display?: TWbwDisplayMode;
+  onChange?: Function;
 }
-const Widget = ({ data }: IWidget) => {
-  return (
-    <div>
+
+const Widget = ({ data, display, onChange }: IWidget) => {
+  const intl = useIntl();
+
+  const onClick: MenuProps["onClick"] = (e) => {
+    console.log("click ", e);
+    if (typeof onChange !== "undefined") {
+      onChange(e.key);
+    }
+  };
+
+  let factors = <></>;
+  if (
+    display === "block" &&
+    (typeof data.factors === "undefined" || data.factors.value === "")
+  ) {
+    //空白的意思在逐词解析模式显示占位字符串
+    factors = (
       <Text type="secondary">
-        <Dropdown menu={{ items }} placement="bottomLeft">
-          <span>{data.factors ? data.factors?.value : "拆分"}</span>
-        </Dropdown>
+        {intl.formatMessage({ id: "dict.fields.factors.label" })}
       </Text>
-    </div>
-  );
+    );
+  } else {
+    factors = <span>{data.factors?.value}</span>;
+  }
+
+  if (typeof data.real !== "undefined" && PaliReal(data.real.value) !== "") {
+    return (
+      <div>
+        <Text type="secondary">
+          <Dropdown menu={{ items, onClick }} placement="bottomLeft">
+            {factors}
+          </Dropdown>
+        </Text>
+      </div>
+    );
+  } else {
+    //标点符号
+    return <></>;
+  }
 };
 
 export default Widget;

+ 55 - 25
dashboard/src/components/template/Wbw/WbwMeaning.tsx

@@ -1,34 +1,64 @@
-import { Popover } from "antd";
-import { IWbw } from "./WbwWord";
+import { useIntl } from "react-intl";
+import { Popover, Typography } from "antd";
+
+import { PaliReal } from "../../../utils";
+import { IWbw, TWbwDisplayMode } from "./WbwWord";
 import WbwMeaningSelect from "./WbwMeaningSelect";
 
+const { Text } = Typography;
+
 interface IWidget {
   data: IWbw;
+  display?: TWbwDisplayMode;
   onChange?: Function;
 }
-const Widget = ({ data, onChange }: IWidget) => {
-  return (
-    <div>
-      <Popover
-        content={
-          <div style={{ width: 500 }}>
-            <WbwMeaningSelect
-              data={data}
-              onSelect={(e: string) => {
-                if (typeof onChange !== "undefined") {
-                  onChange(e);
-                }
-              }}
-            />
-          </div>
-        }
-        placement="bottomLeft"
-        trigger="hover"
-      >
-        {data.meaning?.value}
-      </Popover>
-    </div>
-  );
+const Widget = ({ data, display = "block", onChange }: IWidget) => {
+  const intl = useIntl();
+
+  let meaning = <></>;
+  if (
+    display === "block" &&
+    (typeof data.meaning === "undefined" ||
+      data.meaning.value.length === 0 ||
+      data.meaning.value[0] === "")
+  ) {
+    //空白的意思在逐词解析模式显示占位字符串
+    meaning = (
+      <Text type="secondary">
+        {intl.formatMessage({ id: "dict.fields.meaning.label" })}
+      </Text>
+    );
+  } else {
+    meaning = <span>{data.meaning?.value}</span>;
+  }
+  if (typeof data.real !== "undefined" && PaliReal(data.real.value) !== "") {
+    //非标点符号
+    return (
+      <div>
+        <Popover
+          content={
+            <div style={{ width: 500 }}>
+              <WbwMeaningSelect
+                data={data}
+                onSelect={(e: string) => {
+                  if (typeof onChange !== "undefined") {
+                    onChange(e);
+                  }
+                }}
+              />
+            </div>
+          }
+          placement="bottomLeft"
+          trigger="hover"
+        >
+          {meaning}
+        </Popover>
+      </div>
+    );
+  } else {
+    //标点符号
+    return <></>;
+  }
 };
 
 export default Widget;

+ 16 - 0
dashboard/src/components/template/Wbw/WbwPage.tsx

@@ -0,0 +1,16 @@
+import { Tag } from "antd";
+
+import { IWbw } from "./WbwWord";
+
+interface IWidget {
+  data: IWbw;
+}
+const Widget = ({ data }: IWidget) => {
+  return (
+    <span>
+      <Tag>{data.word.value}</Tag>
+    </span>
+  );
+};
+
+export default Widget;

+ 45 - 19
dashboard/src/components/template/Wbw/WbwPali.tsx

@@ -6,6 +6,7 @@ import WbwDetail from "./WbwDetail";
 import { IWbw } from "./WbwWord";
 import { bookMarkColor } from "./WbwDetailBookMark";
 import "./wbw.css";
+import { PaliReal } from "../../../utils";
 
 interface IWidget {
   data: IWbw;
@@ -56,26 +57,51 @@ const Widget = ({ data, onSave }: IWidget) => {
   ) : (
     <></>
   );
-  return (
-    <div className="pali_shell">
-      <Popover
-        content={wbwDetail}
-        placement="bottom"
-        trigger="click"
-        open={open}
-        onOpenChange={handleClickChange}
-      >
-        <span
-          className="pali"
-          style={{ backgroundColor: paliColor, padding: 4, borderRadius: 5 }}
-        >
-          {data.word.value}
-        </span>
-      </Popover>
-      {noteIcon}
-      {bookMarkIcon}
-    </div>
+  const classPali = data.style?.value === "note" ? "wbw_note" : "pali";
+  let padding: string;
+  if (typeof data.real !== "undefined" && PaliReal(data.real.value) !== "") {
+    padding = "4px";
+  } else {
+    padding = "4px 0";
+  }
+  const paliWord = (
+    <span
+      className={classPali}
+      style={{
+        backgroundColor: paliColor,
+        padding: padding,
+        borderRadius: 5,
+      }}
+    >
+      {data.word.value}
+    </span>
   );
+
+  if (typeof data.real !== "undefined" && PaliReal(data.real.value) !== "") {
+    //非标点符号
+    return (
+      <div className="pali_shell">
+        <Popover
+          content={wbwDetail}
+          placement="bottom"
+          trigger="click"
+          open={open}
+          onOpenChange={handleClickChange}
+        >
+          {paliWord}
+        </Popover>
+        {noteIcon}
+        {bookMarkIcon}
+      </div>
+    );
+  } else {
+    //标点符号
+    return (
+      <div className="pali_shell" style={{ cursor: "unset" }}>
+        {paliWord}
+      </div>
+    );
+  }
 };
 
 export default Widget;

+ 17 - 0
dashboard/src/components/template/Wbw/WbwPara.tsx

@@ -0,0 +1,17 @@
+import { Button } from "antd";
+import { PicCenterOutlined } from "@ant-design/icons";
+
+import { IWbw } from "./WbwWord";
+
+interface IWidget {
+  data: IWbw;
+}
+const Widget = ({ data }: IWidget) => {
+  return (
+    <span>
+      <Button size="small" type="link" icon={<PicCenterOutlined />} />
+    </span>
+  );
+};
+
+export default Widget;

+ 149 - 50
dashboard/src/components/template/Wbw/WbwWord.tsx

@@ -1,12 +1,18 @@
-import { useState, useEffect } from "react";
+import { useState, useEffect, useRef } from "react";
 import WbwCase from "./WbwCase";
 import { bookMarkColor } from "./WbwDetailBookMark";
 import WbwFactorMeaning from "./WbwFactorMeaning";
 import WbwFactors from "./WbwFactors";
 import WbwMeaning from "./WbwMeaning";
 import WbwPali from "./WbwPali";
-import WbwWord from "./WbwWord";
 import "./wbw.css";
+import WbwPara from "./WbwPara";
+import WbwPage from "./WbwPage";
+import { useAppSelector } from "../../../hooks";
+import { add, wordList } from "../../../reducers/inline-dict";
+import { get } from "../../../request";
+import { IApiResponseDictList } from "../../api/Dict";
+import store from "../../../store";
 
 export type TFieldName =
   | "word"
@@ -53,6 +59,7 @@ export interface IWbw {
   meaning?: WbwElement2;
   type?: WbwElement;
   grammar?: WbwElement;
+  style?: WbwElement;
   case?: WbwElement2;
   parent?: WbwElement;
   factors?: WbwElement;
@@ -70,9 +77,10 @@ export interface IWbwFields {
   factorMeaning?: boolean;
   case?: boolean;
 }
+export type TWbwDisplayMode = "block" | "inline";
 interface IWidget {
   data: IWbw;
-  display?: "block" | "inline";
+  display?: TWbwDisplayMode;
   fields?: IWbwFields;
   onChange?: Function;
   onSplit?: Function;
@@ -85,63 +93,154 @@ const Widget = ({
   onSplit,
 }: IWidget) => {
   const [wordData, setWordData] = useState(data);
+  const [fieldDisplay, setFieldDisplay] = useState(fields);
+  const intervalRef = useRef<number | null>(null); //防抖计时器句柄
+  const inlineWords = useAppSelector(wordList);
+
   useEffect(() => {
     setWordData(data);
-  }, [data]);
-  const styleWbw: React.CSSProperties = {
-    display: display === "block" ? "block" : "flex",
-  };
+    setFieldDisplay(fields);
+  }, [data, fields]);
+
   const color = wordData.bookMarkColor
     ? bookMarkColor[wordData.bookMarkColor.value]
     : "unset";
-  return (
-    <div className={`wbw_word ${display}`} style={styleWbw}>
-      <WbwPali
-        data={wordData}
-        onSave={(e: IWbw) => {
-          console.log("save", e);
-          const newData: IWbw = JSON.parse(JSON.stringify(e));
-          setWordData(newData);
-          if (typeof onChange !== "undefined") {
-            onChange(e);
+  const wbwCtl = wordData.type?.value === ".ctl." ? "wbw_ctl" : "";
+  const wbwAnchor = wordData.grammar?.value === ".a." ? "wbw_anchor" : "";
+
+  const styleWbw: React.CSSProperties = {
+    display: display === "block" ? "block" : "flex",
+  };
+
+  /**
+   * 停止查字典计时
+   * 在两种情况下停止计时
+   * 1. 开始查字典
+   * 2. 防抖时间内鼠标移出单词区
+   */
+  const stopLookup = () => {
+    if (intervalRef.current) {
+      window.clearInterval(intervalRef.current);
+      intervalRef.current = null;
+    }
+  };
+  /**
+   * 查字典
+   * @param word 要查的单词
+   */
+  const lookup = (word: string) => {
+    stopLookup();
+    //查询这个词在内存字典里是否有
+    if (inlineWords.has(word)) {
+      //已经有了,退出
+      return;
+    }
+    get<IApiResponseDictList>(`/v2/wbwlookup?word=${word}`).then((json) => {
+      console.log("lookup ok", json.data.count);
+      store.dispatch(add([word, json.data.rows]));
+    });
+    console.log("lookup", word);
+  };
+  if (wordData.type?.value === ".ctl.") {
+    if (wordData.word.value.includes("para")) {
+      return <WbwPara data={wordData} />;
+    } else {
+      return <WbwPage data={wordData} />;
+    }
+  } else {
+    return (
+      <div
+        className={`wbw_word ${display} ${wbwCtl} ${wbwAnchor} `}
+        style={styleWbw}
+        onMouseEnter={() => {
+          if (intervalRef.current === null) {
+            intervalRef.current = window.setInterval(
+              lookup,
+              200,
+              wordData.word.value
+            );
           }
         }}
-      />
-      <div
-        className="wbw_body"
-        style={{
-          background: `linear-gradient(90deg, rgba(255, 255, 255, 0), ${color})`,
+        onMouseLeave={() => {
+          stopLookup();
         }}
       >
-        {fields?.meaning ? (
-          <WbwMeaning
-            data={wordData}
-            onChange={(e: string) => {
-              console.log("meaning change", e);
-              const newData: IWbw = JSON.parse(JSON.stringify(wordData));
-              newData.meaning = { value: [e], status: 5 };
-              setWordData(newData);
-            }}
-          />
-        ) : undefined}
-        {fields?.factors ? <WbwFactors data={wordData} /> : undefined}
-        {fields?.factorMeaning ? (
-          <WbwFactorMeaning data={wordData} />
-        ) : undefined}
-        {fields?.case ? (
-          <WbwCase
-            data={wordData}
-            onSplit={(e: boolean) => {
-              console.log("onSplit", wordData.factors?.value);
-              if (typeof onSplit !== "undefined") {
-                onSplit(e);
-              }
-            }}
-          />
-        ) : undefined}
+        <WbwPali
+          data={wordData}
+          onSave={(e: IWbw) => {
+            console.log("save", e);
+            const newData: IWbw = JSON.parse(JSON.stringify(e));
+            setWordData(newData);
+            if (typeof onChange !== "undefined") {
+              onChange(e);
+            }
+          }}
+        />
+        <div
+          className="wbw_body"
+          style={{
+            background: `linear-gradient(90deg, rgba(255, 255, 255, 0), ${color})`,
+          }}
+        >
+          {fieldDisplay?.meaning ? (
+            <WbwMeaning
+              data={wordData}
+              display={display}
+              onChange={(e: string) => {
+                console.log("meaning change", e);
+                const newData: IWbw = JSON.parse(JSON.stringify(wordData));
+                newData.meaning = { value: [e], status: 5 };
+                setWordData(newData);
+                if (typeof onChange !== "undefined") {
+                  onChange(newData);
+                }
+              }}
+            />
+          ) : undefined}
+          {fieldDisplay?.factors ? (
+            <WbwFactors
+              data={wordData}
+              display={display}
+              onChange={(e: string) => {
+                console.log("factor change", e);
+                const newData: IWbw = JSON.parse(JSON.stringify(wordData));
+                newData.factors = { value: e, status: 5 };
+                setWordData(newData);
+              }}
+            />
+          ) : undefined}
+          {fieldDisplay?.factorMeaning ? (
+            <WbwFactorMeaning
+              data={wordData}
+              display={display}
+              onChange={(e: string) => {
+                const newData: IWbw = JSON.parse(JSON.stringify(wordData));
+                newData.factorMeaning = { value: e, status: 5 };
+                setWordData(newData);
+              }}
+            />
+          ) : undefined}
+          {fieldDisplay?.case ? (
+            <WbwCase
+              data={wordData}
+              display={display}
+              onSplit={(e: boolean) => {
+                console.log("onSplit", wordData.factors?.value);
+                if (typeof onSplit !== "undefined") {
+                  onSplit(e);
+                }
+              }}
+              onChange={(e: string) => {
+                const newData: IWbw = JSON.parse(JSON.stringify(wordData));
+                newData.case = { value: e.split("+"), status: 5 };
+                setWordData(newData);
+              }}
+            />
+          ) : undefined}
+        </div>
       </div>
-    </div>
-  );
+    );
+  }
 };
 
 export default Widget;

+ 27 - 2
dashboard/src/components/template/Wbw/wbw.css

@@ -3,15 +3,35 @@
   padding-right: 0;
   max-width: 60vw;
 }
+.wbw_detail_basic .ant-form-item {
+  margin: 0 0 4px;
+}
+.wbw_detail_basic .ant-collapse > .ant-collapse-item > .ant-collapse-header {
+  padding-top: 2px;
+  padding-bottom: 2px;
+}
+.wbw_ctl {
+  display: none;
+}
+.wbw_anchor {
+  display: none;
+}
+
 .wbw_split {
   visibility: hidden;
 }
 .wbw_word:hover .wbw_split {
   visibility: visible;
 }
+.pali:hover {
+  cursor: pointer;
+}
+.inline .pali:hover {
+  text-decoration-line: underline;
+  text-underline-offset: 5px;
+}
 .block .pali {
   font-weight: 500;
-  font-size: 110%;
   padding: 0px 2px;
   margin: 0px;
   line-height: 1.5em;
@@ -22,12 +42,17 @@
 .inline .pali {
   color: brown;
 }
+.wbw_note {
+  color: blue;
+}
+.block .wbw_note {
+  font-weight: 500;
+}
 .block .wbw_body {
   padding-right: 0.5em;
   padding-top: 0.2em;
 }
 .wbw_word_item {
-  min-width: 3em;
   padding: 0;
   cursor: pointer;
 }

+ 31 - 5
dashboard/src/components/template/WbwSent.tsx

@@ -1,4 +1,6 @@
-import { useState } from "react";
+import { useEffect, useState } from "react";
+import { useAppSelector } from "../../hooks";
+import { mode } from "../../reducers/article-mode";
 import WbwWord, { IWbw, IWbwFields } from "./Wbw/WbwWord";
 
 interface IWidget {
@@ -6,9 +8,33 @@ interface IWidget {
   display?: "block" | "inline";
   fields?: IWbwFields;
 }
-export const WbwSentCtl = ({ data, display, fields }: IWidget) => {
+export const WbwSentCtl = ({ data, display = "inline", fields }: IWidget) => {
   const [wordData, setWordData] = useState(data);
-
+  const [wbwMode, setWbwMode] = useState(display);
+  const [fieldDisplay, setFieldDisplay] = useState(fields);
+  const newMode = useAppSelector(mode);
+  useEffect(() => {
+    switch (newMode) {
+      case "edit":
+        setWbwMode("inline");
+        setFieldDisplay({
+          meaning: true,
+          factors: false,
+          factorMeaning: false,
+          case: false,
+        });
+        break;
+      case "wbw":
+        setWbwMode("block");
+        setFieldDisplay({
+          meaning: true,
+          factors: true,
+          factorMeaning: true,
+          case: true,
+        });
+        break;
+    }
+  }, [newMode]);
   return (
     <div style={{ display: "flex", flexWrap: "wrap" }}>
       {wordData.map((item, id) => {
@@ -16,8 +42,8 @@ export const WbwSentCtl = ({ data, display, fields }: IWidget) => {
           <WbwWord
             data={item}
             key={id}
-            display={display}
-            fields={fields}
+            display={wbwMode}
+            fields={fieldDisplay}
             onChange={(e: IWbw) => {
               console.log("word changed", e);
               console.log("word id", id);

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

@@ -3,6 +3,7 @@ const items = {
   "buttons.create": "新建",
   "buttons.edit": "编辑",
   "buttons.read": "阅读",
+  "buttons.wbw": "逐词解析",
   "buttons.delete": "删除",
   "buttons.delete.all": "批量删除",
   "buttons.selected": "已经选择",

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

@@ -3,7 +3,8 @@ const items = {
   "dict.fields.sn.label": "序号",
   "dict.fields.word.label": "单词",
   "dict.fields.type.label": "类型",
-  "dict.fields.grammar.label": "语法信息",
+  "dict.fields.grammar.label": "语法",
+  "dict.fields.case.label": "格位",
   "dict.fields.parent.label": "词干",
   "dict.fields.meaning.label": "意思",
   "dict.fields.factors.label": "组份",
@@ -17,6 +18,8 @@ const items = {
   "dict.fields.type.ti.short.label": "三",
   "dict.fields.type.v.label": "动词",
   "dict.fields.type.v.short.label": "动",
+  "dict.fields.type.v:ind.label": "动不变",
+  "dict.fields.type.v:ind.short.label": "动不变",
   "dict.fields.type.ind.label": "不变",
   "dict.fields.type.ind.short.label": "不",
   "dict.fields.type.m.label": "阳性",
@@ -43,6 +46,8 @@ const items = {
   "dict.fields.type.voc.short.label": "呼",
   "dict.fields.type.abl.label": "来源格",
   "dict.fields.type.abl.short.label": "源",
+  "dict.fields.type.loc.label": "处格",
+  "dict.fields.type.loc.short.label": "处",
   "dict.fields.type.base.label": "词干",
   "dict.fields.type.base.short.label": "干",
   "dict.fields.type.imp.label": "命令",
@@ -117,6 +122,7 @@ const items = {
   "dict.fields.type.interj.short.label": "感",
   "dict.fields.type.pre.label": "前缀",
   "dict.fields.type.pre.short.label": "前",
+  "dict.fields.type.suf.label": "后缀",
   "dict.fields.type.suf.short.label": "后",
   "dict.fields.type.end.label": "语尾",
   "dict.fields.type.end.short.label": "尾",

+ 32 - 0
dashboard/src/reducers/article-mode.ts

@@ -0,0 +1,32 @@
+/**
+ * 查字典,添加术语命令
+ */
+import { createSlice, PayloadAction } from "@reduxjs/toolkit";
+import { ArticleMode } from "../components/article/Article";
+
+import type { RootState } from "../store";
+
+interface IState {
+  id?: string;
+  mode?: ArticleMode;
+}
+
+const initialState: IState = {};
+
+export const slice = createSlice({
+  name: "articleMode",
+  initialState,
+  reducers: {
+    modeChange: (state, action: PayloadAction<ArticleMode>) => {
+      state.mode = action.payload;
+      console.log("mode", action.payload);
+    },
+  },
+});
+
+export const { modeChange } = slice.actions;
+
+export const mode = (state: RootState): ArticleMode | undefined =>
+  state.articleMode.mode;
+
+export default slice.reducer;

+ 38 - 0
dashboard/src/reducers/inline-dict.ts

@@ -0,0 +1,38 @@
+import { createSlice, PayloadAction } from "@reduxjs/toolkit";
+
+import type { RootState } from "../store";
+import { IDictDataRequest } from "../components/api/Dict";
+
+/**
+ * 在查询字典后,将查询结果放入map
+ * key: 单词词头
+ * value: 查询到的单词列表
+ */
+interface IState {
+  wordMap: Map<string, IDictDataRequest[]>;
+  word?: string;
+  value?: IDictDataRequest[];
+}
+
+const initialState: IState = {
+  wordMap: new Map<string, IDictDataRequest[]>([["word", []]]),
+};
+
+export const slice = createSlice({
+  name: "inline-dict",
+  initialState,
+  reducers: {
+    add: (state, action: PayloadAction<[string, IDictDataRequest[]]>) => {
+      state.wordMap.set(action.payload[0], action.payload[1]);
+    },
+  },
+});
+
+export const { add } = slice.actions;
+
+export const inlineDict = (state: RootState): IState => state.inlineDict;
+
+export const wordList = (state: RootState): Map<string, IDictDataRequest[]> =>
+  state.inlineDict.wordMap;
+
+export default slice.reducer;

+ 4 - 0
dashboard/src/store.ts

@@ -6,6 +6,8 @@ import openArticleReducer from "./reducers/open-article";
 import settingReducer from "./reducers/setting";
 import commandReducer from "./reducers/command";
 import suggestionReducer from "./reducers/suggestion";
+import articleModeReducer from "./reducers/article-mode";
+import inlineDictReducer from "./reducers/inline-dict";
 
 const store = configureStore({
   reducer: {
@@ -15,6 +17,8 @@ const store = configureStore({
     setting: settingReducer,
     command: commandReducer,
     suggestion: suggestionReducer,
+    articleMode: articleModeReducer,
+    inlineDict: inlineDictReducer,
   },
 });
 

+ 74 - 51
dashboard/src/utils.ts

@@ -1,58 +1,81 @@
-export function ApiFetch(url: string, method = "GET", data?: any): Promise<Response> {
-	const apiHost = process.env.REACT_APP_API_HOST;
-	interface ajaxParam {
-		method: string;
-		body?: string;
-		headers?: any;
-	}
-	let param: ajaxParam = {
-		method: method,
-	};
-	if (typeof data !== "undefined") {
-		param.body = JSON.stringify(data);
-	}
-	if (localStorage.getItem("token")) {
-		param.headers = { token: localStorage.getItem("token") };
-	}
-	return new Promise((resolve, reject) => {
-		let apiUrl = apiHost + url;
-		console.log("api", apiUrl);
-		fetch(apiUrl, param)
-			.then((response) => response.json())
-			.then((response) => {
-				resolve(response);
-			})
-			.catch((error) => {
-				reject(error);
-			});
-	});
+export function ApiFetch(
+  url: string,
+  method = "GET",
+  data?: any
+): Promise<Response> {
+  const apiHost = process.env.REACT_APP_API_HOST;
+  interface ajaxParam {
+    method: string;
+    body?: string;
+    headers?: any;
+  }
+  let param: ajaxParam = {
+    method: method,
+  };
+  if (typeof data !== "undefined") {
+    param.body = JSON.stringify(data);
+  }
+  if (localStorage.getItem("token")) {
+    param.headers = { token: localStorage.getItem("token") };
+  }
+  return new Promise((resolve, reject) => {
+    let apiUrl = apiHost + url;
+    console.log("api", apiUrl);
+    fetch(apiUrl, param)
+      .then((response) => response.json())
+      .then((response) => {
+        resolve(response);
+      })
+      .catch((error) => {
+        reject(error);
+      });
+  });
 }
 
 export function ApiGetText(url: string): Promise<String> {
-	const apiHost = process.env.REACT_APP_API_HOST ? process.env.REACT_APP_API_HOST : "http://localhost/api";
-	return new Promise((resolve, reject) => {
-		let apiUrl = apiHost + url;
-		console.log("api", apiUrl);
-		fetch(apiUrl)
-			.then((response) => response.text())
-			.then((response) => {
-				resolve(response);
-			})
-			.catch((error) => {
-				reject(error);
-			});
-	});
+  const apiHost = process.env.REACT_APP_API_HOST
+    ? process.env.REACT_APP_API_HOST
+    : "http://localhost/api";
+  return new Promise((resolve, reject) => {
+    let apiUrl = apiHost + url;
+    console.log("api", apiUrl);
+    fetch(apiUrl)
+      .then((response) => response.text())
+      .then((response) => {
+        resolve(response);
+      })
+      .catch((error) => {
+        reject(error);
+      });
+  });
 }
 
 export function PaliToEn(pali: string): string {
-	let output: string = pali.toLowerCase();
-	output = output.replaceAll(" ", "_");
-	output = output.replaceAll("-", "_");
-	output = output.replaceAll("ā", "a");
-	output = output.replaceAll("ī", "i");
-	output = output.replaceAll("ū", "u");
-	output = output.replaceAll("ḍ", "d");
-	output = output.replaceAll("ṭ", "t");
-	output = output.replaceAll("ḷ", "l");
-	return output;
+  let output: string = pali.toLowerCase();
+  output = output.replaceAll(" ", "_");
+  output = output.replaceAll("-", "_");
+  output = output.replaceAll("ā", "a");
+  output = output.replaceAll("ī", "i");
+  output = output.replaceAll("ū", "u");
+  output = output.replaceAll("ḍ", "d");
+  output = output.replaceAll("ṭ", "t");
+  output = output.replaceAll("ḷ", "l");
+  return output;
+}
+
+export function PaliReal(inStr: string): string {
+  if (typeof inStr === "undefined") {
+    return "";
+  }
+  const paliLetter = "abcdefghijklmnoprstuvyāīūṅñṭḍṇḷṃ";
+  let output: string = "";
+  inStr = inStr.toLowerCase();
+  inStr = inStr.replace(/ṁ/g, "ṃ");
+  inStr = inStr.replace(/ŋ/g, "ṃ");
+  for (const iterator of inStr) {
+    if (paliLetter.includes(iterator)) {
+      output += iterator;
+    }
+  }
+  return output;
 }