Browse Source

:sparkles: create chapter read and translate

visuddhinanda 3 năm trước cách đây
mục cha
commit
f8bf724141

+ 6 - 5
dashboard/src/Router.tsx

@@ -34,7 +34,8 @@ import LibraryDictRecent from "./pages/library/dict/recent";
 import LibraryAnthology from "./pages/library/anthology";
 import LibraryAnthologyShow from "./pages/library/anthology/show";
 import LibraryAnthologyList from "./pages/library/anthology/list";
-import LibraryArticle from "./pages/library/anthology/article";
+import LibraryArticle from "./pages/library/article";
+import LibraryArticleShow from "./pages/library/article/show";
 
 import LibraryBlog from "./pages/library/blog";
 import LibraryBlogOverview from "./pages/library/blog/overview";
@@ -165,15 +166,15 @@ const Widget = () => {
 			</Route>
 
 			<Route path="article" element={<LibraryArticle />}>
-				<Route path=":type/:id" element={<LibraryArticle />} />
+				<Route path=":type/:id" element={<LibraryArticleShow />} />
 				<Route
 					path=":type/:id/param/:param"
-					element={<LibraryArticle />}
+					element={<LibraryArticleShow />}
 				/>
-				<Route path=":type/:id/tran" element={<LibraryArticle />} />
+				<Route path=":type/:id/tran" element={<LibraryArticleShow />} />
 				<Route
 					path=":type/:id/tran/param/:param"
-					element={<LibraryArticle />}
+					element={<LibraryArticleShow />}
 				/>
 			</Route>
 

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

@@ -108,3 +108,27 @@ export interface IApiResponseChannelList {
 	message: string;
 	data: { rows: IApiResponseChannelListData[]; count: number };
 }
+
+export interface ISentenceRequst {
+	book: number;
+	para: number;
+	wordStart: number;
+	wordEnd: number;
+	channel: string;
+	content: string;
+}
+
+export interface ISentenceData {
+	book: number;
+	para: number;
+	word_start: number;
+	word_end: number;
+	channel_uid: string;
+	content: string;
+}
+
+export interface ISentenceResponse {
+	ok: boolean;
+	message: string;
+	data: ISentenceData;
+}

+ 32 - 0
dashboard/src/components/article/Article.tsx

@@ -0,0 +1,32 @@
+import { message } from "antd";
+import { useEffect, useState } from "react";
+import { get } from "../../request";
+import { IArticleResponse } from "../api/Article";
+import ArticleCard from "./ArticleCard";
+import { IWidgetArticleData } from "./ArticleView";
+
+interface IWidgetArticle {
+	type?: string;
+	articleId?: string;
+	mode?: "read" | "edit";
+}
+const Widget = ({ type, articleId, mode }: IWidgetArticle) => {
+	const [articleData, setArticleData] = useState<IWidgetArticleData>();
+
+	useEffect(() => {
+		if (typeof type !== "undefined" && typeof articleId !== "undefined") {
+			get<IArticleResponse>(`/v2/corpus/${type}/${articleId}`).then(
+				(json) => {
+					if (json.ok) {
+						setArticleData(json.data);
+					} else {
+						message.error(json.message);
+					}
+				}
+			);
+		}
+	}, [type, articleId]);
+	return <ArticleCard data={articleData} />;
+};
+
+export default Widget;

+ 72 - 0
dashboard/src/components/article/ArticleCard.tsx

@@ -0,0 +1,72 @@
+import { Button, Card, Dropdown, Menu, Space, Segmented } from "antd";
+import { MoreOutlined, MenuOutlined } from "@ant-design/icons";
+import type { MenuProps } from "antd";
+import ArticleView, { IWidgetArticleData } from "./ArticleView";
+
+interface IWidgetArticleCard {
+	data?: IWidgetArticleData;
+}
+const Widget = ({ data }: IWidgetArticleCard) => {
+	const onClick: MenuProps["onClick"] = (e) => {
+		console.log("click ", e);
+	};
+
+	const menu = (
+		<Menu
+			onClick={onClick}
+			items={[
+				{
+					key: "close",
+					label: "关闭",
+				},
+				{
+					key: "closeAll",
+					label: "关闭全部",
+				},
+				{
+					key: "closeOthers",
+					label: "关闭其他",
+				},
+				{
+					key: "closeRight",
+					label: "关闭右侧",
+				},
+			]}
+		/>
+	);
+
+	return (
+		<Card
+			size="small"
+			title={
+				<Space>
+					<Button size="small" icon={<MenuOutlined />} />
+					{data?.title}
+				</Space>
+			}
+			extra={
+				<Space>
+					<Segmented
+						options={[
+							{ label: "阅读", value: "read" },
+							{ label: "编辑", value: "edit" },
+						]}
+						value="read"
+						onChange={(value) => {
+							console.log(value);
+						}}
+					/>
+					<Dropdown overlay={menu} placement="bottomRight">
+						<Button shape="circle" icon={<MoreOutlined />}></Button>
+					</Dropdown>
+				</Space>
+			}
+			style={{ width: 600 }}
+			bodyStyle={{ height: 590, overflowY: "scroll" }}
+		>
+			<ArticleView {...data} />
+		</Card>
+	);
+};
+
+export default Widget;

+ 40 - 0
dashboard/src/components/article/ArticleView.tsx

@@ -0,0 +1,40 @@
+import { Typography, Divider } from "antd";
+import MdView from "../template/MdView";
+
+const { Paragraph, Title, Text } = Typography;
+
+export interface IWidgetArticleData {
+	id?: string;
+	title?: string;
+	subTitle?: string;
+	summary?: string;
+	content?: string;
+	created_at?: string;
+	updated_at?: string;
+}
+
+const Widget = ({
+	id,
+	title,
+	subTitle,
+	summary,
+	content,
+	created_at,
+	updated_at,
+}: IWidgetArticleData) => {
+	return (
+		<>
+			<Title level={1}>{title}</Title>
+			<Text type="secondary">{subTitle}</Text>
+			<Paragraph ellipsis={{ rows: 2, expandable: true, symbol: "more" }}>
+				{summary}
+			</Paragraph>
+			<Divider />
+			<div>
+				<MdView html={content ? content : "none"} />
+			</div>
+		</>
+	);
+};
+
+export default Widget;

+ 17 - 0
dashboard/src/components/auth/User.tsx

@@ -0,0 +1,17 @@
+import { Avatar } from "antd";
+export interface IUser {
+	id: string;
+	nickName: string;
+	realName: string;
+	avatar: string;
+}
+const Widget = ({ nickName, realName, avatar }: IUser) => {
+	return (
+		<>
+			<Avatar size="small">{nickName?.slice(0, 1)}</Avatar>
+			{nickName}
+		</>
+	);
+};
+
+export default Widget;

+ 9 - 0
dashboard/src/components/channel/Channel.tsx

@@ -0,0 +1,9 @@
+export interface IChannel {
+	name: string;
+	id: string;
+}
+const Widget = ({ name, id }: IChannel) => {
+	return <span>{name}</span>;
+};
+
+export default Widget;

+ 19 - 30
dashboard/src/components/corpus/ChapterCard.tsx

@@ -7,7 +7,7 @@ import type { TagNode } from "../tag/TagArea";
 import type { ChannelInfoProps } from "../api/Channel";
 import ChannelListItem from "../channel/ChannelListItem";
 
-const { Title, Paragraph, Link } = Typography;
+const { Title, Paragraph, Link, Text } = Typography;
 
 export interface ChapterData {
 	Title: string;
@@ -31,44 +31,30 @@ interface IWidgetChapterCard {
 const Widget = (prop: IWidgetChapterCard) => {
 	const path = JSON.parse(prop.data.Path);
 	const tags = prop.data.Tag;
-	const aa = {
-		marginTop: "auto",
-		marginBottom: "auto",
-		display: "-webkit-box",
-		//WebkitBoxOrient: "vertical",
-		//WebkitLineClamp: 3,
-		overflow: "hidden",
-	};
-
 	return (
 		<>
 			<Row>
-				<Col span={3}>封面</Col>
-				<Col span={21}>
+				<Col>
 					<Row>
 						<Col span={16}>
-							<Row>
-								<Col>
-									<Title level={5}>
-										<Link>{prop.data.Title}</Link>
-									</Title>
-								</Col>
-							</Row>
-							<Row>
-								<Col>{prop.data.PaliTitle}</Col>
-							</Row>
-							<Row>
-								<Col>
-									<TocPath data={path} />
-								</Col>
-							</Row>
+							<Title level={5}>
+								<Link>{prop.data.Title}</Link>
+							</Title>
+							<Text type="secondary">{prop.data.PaliTitle}</Text>
+							<TocPath data={path} />
 						</Col>
 						<Col span={8}>进度条</Col>
 					</Row>
 					<Row>
 						<Col>
-							<Paragraph>
-								<div style={aa}>{prop.data.Summary}</div>
+							<Paragraph
+								ellipsis={{
+									rows: 2,
+									expandable: false,
+									symbol: "more",
+								}}
+							>
+								{prop.data.Summary}
 							</Paragraph>
 						</Col>
 					</Row>
@@ -80,7 +66,10 @@ const Widget = (prop: IWidgetChapterCard) => {
 							<ChannelListItem data={prop.data.Channel} />
 						</Col>
 						<Col span={3}>
-							<TimeShow time={prop.data.UpdatedAt} title="UpdatedAt" />
+							<TimeShow
+								time={prop.data.UpdatedAt}
+								title="UpdatedAt"
+							/>
 						</Col>
 					</Row>
 				</Col>

+ 23 - 3
dashboard/src/components/template/MdTpl.tsx

@@ -1,8 +1,28 @@
+import Note from "./Note";
+import SentEdit from "./SentEdit";
+import SentRead from "./SentRead";
+import Term from "./Term";
+import Wd from "./Wd";
+
 interface IWidgetMdTpl {
-	name?: string;
+	tpl?: string;
+	props?: string;
 }
-const Widget = ({ name }: IWidgetMdTpl) => {
-	return <span>tpl name: {name}</span>;
+const Widget = ({ tpl, props }: IWidgetMdTpl) => {
+	switch (tpl) {
+		case "term":
+			return <Term props={props ? props : ""} />;
+		case "note":
+			return <Note props={props ? props : ""} />;
+		case "sentread":
+			return <SentRead props={props ? props : ""} />;
+		case "sentedit":
+			return <SentEdit props={props ? props : ""} />;
+		case "wd":
+			return <Wd props={props ? props : ""} />;
+		default:
+			return <>未定义模版({tpl})</>;
+	}
 };
 
 export default Widget;

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

@@ -5,7 +5,7 @@ interface IWidget {
 }
 const Widget = ({ html }: IWidget) => {
 	const jsx = XmlToReact(html);
-	return <div>{jsx}</div>;
+	return <>{jsx}</>;
 };
 
 export default Widget;

+ 31 - 0
dashboard/src/components/template/Note.tsx

@@ -0,0 +1,31 @@
+import { Popover } from "antd";
+import { InfoCircleOutlined } from "@ant-design/icons";
+import { Typography } from "antd";
+
+const { Paragraph, Link } = Typography;
+
+interface IWidgetNoteCtl {
+	trigger?: string;
+	note?: string;
+}
+const NoteCtl = ({ trigger, note }: IWidgetNoteCtl) => {
+	const noteCard = <Paragraph copyable>{note}</Paragraph>;
+	const show = trigger ? trigger : <InfoCircleOutlined />;
+	return (
+		<>
+			<Popover content={noteCard} placement="bottom">
+				<Link>{show}</Link>
+			</Popover>
+		</>
+	);
+};
+
+interface IWidgetTerm {
+	props: string;
+}
+const Widget = ({ props }: IWidgetTerm) => {
+	const prop = JSON.parse(decodeURI(props)) as IWidgetNoteCtl;
+	return <NoteCtl {...prop} />;
+};
+
+export default Widget;

+ 392 - 0
dashboard/src/components/template/SentEdit.tsx

@@ -0,0 +1,392 @@
+import { useIntl } from "react-intl";
+
+import {
+	Input,
+	Button,
+	Space,
+	Badge,
+	Tabs,
+	Dropdown,
+	Menu,
+	Card,
+	Typography,
+	message,
+} from "antd";
+
+import {
+	CloseOutlined,
+	TranslationOutlined,
+	BookOutlined,
+	BlockOutlined,
+	MoreOutlined,
+	SaveOutlined,
+	EditOutlined,
+	IssuesCloseOutlined,
+} from "@ant-design/icons";
+import type { IUser } from "../auth/User";
+import User from "../auth/User";
+import { IChannel } from "../channel/Channel";
+import TimeShow from "../utilities/TimeShow";
+import MdView from "./MdView";
+import { useState } from "react";
+import { ISentenceRequst, ISentenceResponse } from "../api/Corpus";
+import { put } from "../../request";
+
+const { TextArea } = Input;
+const { Text } = Typography;
+
+interface ISentCellMenu {
+	title: string;
+	children?: React.ReactNode;
+}
+const SentCellMenu = ({ title, children }: ISentCellMenu) => {
+	const [isHover, setIsHover] = useState(false);
+
+	const menu = (
+		<Menu
+			onClick={(e) => {
+				console.log(e);
+			}}
+			items={[
+				{
+					key: "en",
+					label: "相关段落",
+				},
+				{
+					key: "zh-Hans",
+					label: "Nissaya",
+				},
+				{
+					key: "zh-Hant",
+					label: "相似句",
+				},
+			]}
+		/>
+	);
+
+	return (
+		<div
+			onMouseEnter={() => {
+				setIsHover(true);
+			}}
+			onMouseLeave={() => {
+				setIsHover(false);
+			}}
+		>
+			<div
+				style={{
+					marginTop: "-1.2em",
+					position: "absolute",
+					display: isHover ? "block" : "none",
+				}}
+			>
+				<Dropdown overlay={menu} placement="bottomLeft">
+					<Button
+						type="primary"
+						icon={<MoreOutlined />}
+						size="small"
+					/>
+				</Dropdown>
+			</div>
+			{children}
+		</div>
+	);
+};
+
+interface ISentEditMenu {
+	children?: React.ReactNode;
+	onModeChange?: Function;
+}
+const SentEditMenu = ({ children, onModeChange }: ISentEditMenu) => {
+	const [isHover, setIsHover] = useState(false);
+
+	const menu = (
+		<Menu
+			onClick={(e) => {
+				console.log(e);
+			}}
+			items={[
+				{
+					key: "en",
+					label: "相关段落",
+				},
+				{
+					key: "zh-Hans",
+					label: "Nissaya",
+				},
+				{
+					key: "zh-Hant",
+					label: "相似句",
+				},
+			]}
+		/>
+	);
+
+	return (
+		<div
+			onMouseEnter={() => {
+				setIsHover(true);
+			}}
+			onMouseLeave={() => {
+				setIsHover(false);
+			}}
+		>
+			<div
+				style={{
+					marginTop: "-1.2em",
+					right: "0",
+					position: "absolute",
+					display: isHover ? "block" : "none",
+				}}
+			>
+				<Button
+					icon={<EditOutlined />}
+					size="small"
+					onClick={() => {
+						if (typeof onModeChange !== "undefined") {
+							onModeChange("edit");
+						}
+					}}
+				/>
+				<Button icon={<IssuesCloseOutlined />} size="small" />
+				<Dropdown overlay={menu} placement="bottomRight">
+					<Button icon={<MoreOutlined />} size="small" />
+				</Dropdown>
+			</div>
+			{children}
+		</div>
+	);
+};
+
+const SentTab = ({
+	tranNum,
+	nissayaNum,
+	commNum,
+	originNum,
+	simNum,
+}: IWidgetSentEditInner) => {
+	const intl = useIntl();
+	return (
+		<Tabs
+			size="small"
+			items={[
+				{
+					label: (
+						<Badge size="small" count={0}>
+							<CloseOutlined />
+						</Badge>
+					),
+					key: "close",
+					children: <></>,
+				},
+				{
+					label: (
+						<Badge size="small" count={tranNum ? tranNum : 0}>
+							<TranslationOutlined />
+							{intl.formatMessage({
+								id: "channel.type.translation.label",
+							})}
+						</Badge>
+					),
+					key: "tran",
+					children: <div>译文</div>,
+				},
+				{
+					label: (
+						<Badge size="small" count={nissayaNum ? nissayaNum : 0}>
+							<BookOutlined />
+							{intl.formatMessage({
+								id: "channel.type.nissaya.label",
+							})}
+						</Badge>
+					),
+					key: "nissaya",
+					children: `2`,
+				},
+				{
+					label: (
+						<Badge size="small" count={commNum ? commNum : 0}>
+							<BlockOutlined />
+							{intl.formatMessage({
+								id: "channel.type.commentary.label",
+							})}
+						</Badge>
+					),
+					key: "3",
+					children: `3`,
+				},
+			]}
+		/>
+	);
+};
+
+export interface ISentence {
+	content: string;
+	html: string;
+	book: number;
+	para: number;
+	wordStart: number;
+	wordEnd: number;
+	editor: IUser;
+	channel: IChannel;
+	updateAt: string;
+}
+
+interface ISentCellEditable {
+	data: ISentence;
+}
+const SentCellEditable = ({ data }: ISentCellEditable) => {
+	const intl = useIntl();
+	const [value, setValue] = useState(data.content);
+	const [saving, setSaving] = useState<boolean>(false);
+	const save = () => {
+		setSaving(true);
+		put<ISentenceRequst, ISentenceResponse>(
+			`/v2/sentence/${data.book}_${data.para}_${data.wordStart}_${data.wordEnd}_${data.channel.id}`,
+			{
+				book: data.book,
+				para: data.para,
+				wordStart: data.wordStart,
+				wordEnd: data.wordEnd,
+				channel: data.channel.id,
+				content: value,
+			}
+		).then((json) => {
+			setSaving(false);
+			if (json.ok) {
+				message.success(intl.formatMessage({ id: "flashes.success" }));
+			} else {
+				message.error(json.message);
+			}
+		});
+	};
+	return (
+		<div>
+			<TextArea
+				value={value}
+				onChange={(e) => setValue(e.target.value)}
+				placeholder="Controlled autosize"
+				autoSize={{ minRows: 3, maxRows: 5 }}
+			/>
+			<div style={{ display: "flex", justifyContent: "space-between" }}>
+				<div>
+					<span>
+						<Text keyboard>esc</Text>=
+						<Button size="small" type="link">
+							cancel
+						</Button>
+					</span>
+					<span>
+						<Text keyboard>enter</Text>=
+						<Button size="small" type="link">
+							new line
+						</Button>
+					</span>
+				</div>
+				<div>
+					<Text keyboard>Ctrl/⌘</Text>➕<Text keyboard>enter</Text>=
+					<Button
+						size="small"
+						type="primary"
+						icon={<SaveOutlined />}
+						loading={saving}
+						onClick={() => save()}
+					>
+						Save
+					</Button>
+				</div>
+			</div>
+		</div>
+	);
+};
+
+interface ISentCell {
+	data: ISentence;
+}
+const SentCell = ({ data }: ISentCell) => {
+	const [isEditMode, setIsEditMode] = useState(false);
+	return (
+		<SentEditMenu
+			onModeChange={(mode: string) => {
+				if (mode === "edit") {
+					setIsEditMode(true);
+				}
+			}}
+		>
+			<div style={{ display: isEditMode ? "none" : "block" }}>
+				<MdView html={data.html} />
+			</div>
+			<div style={{ display: isEditMode ? "block" : "none" }}>
+				<SentCellEditable data={data} />
+			</div>
+			<div>
+				<Space>
+					<User {...data.editor} />
+					<span>updated</span>
+					<TimeShow time={data.updateAt} title="UpdatedAt" />
+				</Space>
+			</div>
+		</SentEditMenu>
+	);
+};
+
+interface IWidgetSentEditInner {
+	origin?: ISentence[];
+	translation?: ISentence[];
+	layout?: "row" | "column";
+	tranNum?: number;
+	nissayaNum?: number;
+	commNum?: number;
+	originNum: number;
+	simNum?: number;
+}
+const SentEditInner = ({
+	origin,
+	translation,
+	layout = "column",
+	tranNum,
+	nissayaNum,
+	commNum,
+	originNum,
+	simNum,
+}: IWidgetSentEditInner) => {
+	return (
+		<Card>
+			<SentCellMenu title="blabla">
+				<div style={{ display: "flex", flexDirection: layout }}>
+					<div style={{ flex: "5", color: "#9f3a01" }}>
+						{origin?.map((item, id) => {
+							return <SentCell key={id} data={item} />;
+						})}
+					</div>
+					<div style={{ flex: "5" }}>
+						{translation?.map((item, id) => {
+							return <SentCell key={id} data={item} />;
+						})}
+					</div>
+				</div>
+				<SentTab
+					tranNum={tranNum}
+					nissayaNum={nissayaNum}
+					commNum={commNum}
+					originNum={originNum}
+					simNum={simNum}
+				/>
+			</SentCellMenu>
+		</Card>
+	);
+};
+
+interface IWidgetSentEdit {
+	props: string;
+}
+const Widget = ({ props }: IWidgetSentEdit) => {
+	const prop = JSON.parse(atob(props)) as IWidgetSentEditInner;
+	return (
+		<>
+			<SentEditInner {...prop} />
+		</>
+	);
+};
+
+export default Widget;

+ 54 - 0
dashboard/src/components/template/SentRead.tsx

@@ -0,0 +1,54 @@
+import { Tooltip, Button } from "antd";
+import MdView from "./MdView";
+
+interface IWidgetSentReadFrame {
+	origin?: string[];
+	translation?: string[];
+	layout?: "row" | "column";
+	sentId?: string;
+}
+const SentReadFrame = ({
+	origin,
+	translation,
+	layout = "column",
+	sentId,
+}: IWidgetSentReadFrame) => {
+	return (
+		<Tooltip
+			placement="topLeft"
+			color="white"
+			title={
+				<Button type="link" size="small">
+					aa
+				</Button>
+			}
+		>
+			<div style={{ display: "flex", flexDirection: layout }}>
+				<div style={{ flex: "5", color: "#9f3a01" }}>
+					{origin?.map((item, id) => {
+						return <MdView key={id} html={item} />;
+					})}
+				</div>
+				<div style={{ flex: "5" }}>
+					{translation?.map((item, id) => {
+						return <MdView key={id} html={item} />;
+					})}
+				</div>
+			</div>
+		</Tooltip>
+	);
+};
+
+interface IWidgetTerm {
+	props: string;
+}
+const Widget = ({ props }: IWidgetTerm) => {
+	const prop = JSON.parse(atob(props)) as IWidgetSentReadFrame;
+	return (
+		<>
+			<SentReadFrame {...prop} />
+		</>
+	);
+};
+
+export default Widget;

+ 61 - 0
dashboard/src/components/template/Term.tsx

@@ -0,0 +1,61 @@
+import { ProCard } from "@ant-design/pro-components";
+import { Button, Popover } from "antd";
+import { SearchOutlined, EditOutlined } from "@ant-design/icons";
+import { Typography } from "antd";
+
+const { Text, Link } = Typography;
+
+interface IWidgetTermCtl {
+	word?: string;
+	meaning?: string;
+	meaning2?: string;
+	channel?: string;
+}
+const TermCtl = ({ word, meaning, meaning2, channel }: IWidgetTermCtl) => {
+	const userCard = (
+		<>
+			<ProCard
+				title={word}
+				style={{ maxWidth: 500, minWidth: 300 }}
+				actions={[
+					<Button type="link" icon={<SearchOutlined />}>
+						更多
+					</Button>,
+					<Button type="link" icon={<SearchOutlined />}>
+						详情
+					</Button>,
+					<Button type="link" icon={<EditOutlined />}>
+						修改
+					</Button>,
+				]}
+			>
+				<div>详细内容</div>
+			</ProCard>
+		</>
+	);
+	const show = meaning ? meaning : word ? word : "unkow";
+	return (
+		<>
+			<Popover content={userCard} placement="bottom">
+				<Link>{show}</Link>
+			</Popover>
+			(<Text italic>{word}</Text>
+			{","}
+			<Text>{meaning2}</Text>)
+		</>
+	);
+};
+
+interface IWidgetTerm {
+	props: string;
+}
+const Widget = ({ props }: IWidgetTerm) => {
+	const prop = JSON.parse(decodeURI(props)) as IWidgetTermCtl;
+	return (
+		<>
+			<TermCtl {...prop} />
+		</>
+	);
+};
+
+export default Widget;

+ 23 - 0
dashboard/src/components/template/Wd.tsx

@@ -0,0 +1,23 @@
+import { Popover } from "antd";
+
+interface IWidgetWdCtl {
+	text?: string;
+}
+const WdCtl = ({ text }: IWidgetWdCtl) => {
+	const noteCard = "note";
+	return (
+		<Popover content={noteCard} placement="bottom">
+			{text}{" "}
+		</Popover>
+	);
+};
+
+interface IWidgetTerm {
+	props: string;
+}
+const Widget = ({ props }: IWidgetTerm) => {
+	const prop = JSON.parse(decodeURI(props)) as IWidgetWdCtl;
+	return <WdCtl {...prop} />;
+};
+
+export default Widget;

+ 17 - 0
dashboard/src/pages/library/article/index.tsx

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

+ 94 - 0
dashboard/src/pages/library/article/show.tsx

@@ -0,0 +1,94 @@
+import { useState, useEffect } from "react";
+import { useParams } from "react-router-dom";
+import { message, Switch } from "antd";
+import { Button, Drawer, Space } from "antd";
+import { SettingOutlined } from "@ant-design/icons";
+
+import { IArticleResponse } from "../../../components/api/Article";
+import Article from "../../../components/article/Article";
+import { IWidgetArticleData } from "../../../components/article/ArticleView";
+
+import { get } from "../../../request";
+
+/**
+ * type:
+ *   sent 句子
+ *   sim  相似句
+ *   v_para vri 自然段
+ *   page  页码
+ *   chapter 段落
+ *   article 文章
+ * @returns
+ */
+const Widget = () => {
+	const { type, id, param } = useParams(); //url 参数
+	const [open, setOpen] = useState(false);
+
+	const showDrawer = () => {
+		setOpen(true);
+	};
+
+	const onClose = () => {
+		setOpen(false);
+	};
+
+	return (
+		<div
+			className="site-drawer-render-in-current-wrapper"
+			style={{ display: "flex" }}
+		>
+			<div
+				style={{ display: "flex", width: "100%", overflowX: "scroll" }}
+			>
+				<div>
+					<Article type={type} articleId={id} />
+				</div>
+			</div>
+			<div style={{ width: "2em" }}>
+				<Button
+					shape="circle"
+					icon={<SettingOutlined />}
+					onClick={showDrawer}
+				></Button>
+			</div>
+			<Drawer
+				title="Setting"
+				placement="right"
+				onClose={onClose}
+				open={open}
+				getContainer={false}
+				style={{ position: "absolute" }}
+			>
+				<Space>
+					保存到用户设置
+					<Switch
+						defaultChecked
+						onChange={(checked) => {
+							console.log(checked);
+						}}
+					/>
+				</Space>
+				<Space>
+					显示原文
+					<Switch
+						defaultChecked
+						onChange={(checked) => {
+							console.log(checked);
+						}}
+					/>
+				</Space>
+				<Space>
+					点词查询
+					<Switch
+						defaultChecked
+						onChange={(checked) => {
+							console.log(checked);
+						}}
+					/>
+				</Space>
+			</Drawer>
+		</div>
+	);
+};
+
+export default Widget;