Procházet zdrojové kódy

Merge pull request #1021 from visuddhinanda/agile

:memo: v1 api demo
visuddhinanda před 3 roky
rodič
revize
be7c744a67

binární
dashboard/public/favicon.ico


+ 23 - 1
dashboard/src/components/library/HeadBar.tsx

@@ -1,5 +1,5 @@
 import { Link } from "react-router-dom";
-import { Layout } from 'antd';
+import { Layout, Space } from 'antd';
 import { useIntl } from "react-intl";
 import type { MenuProps } from 'antd';
 import { Menu } from 'antd';
@@ -40,6 +40,28 @@ const Widget = ({selectedKeys = ''}: IWidgetHeadBar) => {
 			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 (
 	<Layout>

+ 16 - 7
dashboard/src/components/studio/HeadBar.tsx

@@ -1,14 +1,23 @@
 import { Link } from "react-router-dom";
-import { Space } from "antd";
+import { Col, Row,Input, Layout } from 'antd';
+const { Search } = Input;
+const { Header } = Layout;
+const onSearch = (value: string) => console.log(value);
 
 const Widget = () => {
   return (
-  	<div>
-		studio head bar
-		<Space>
-			<Link to="/">首页</Link>
-		</Space>
-	</div>
+	<Header className="header">
+		<Row>
+			<Col flex="100px">
+				<Link to="/">studio</Link>
+			</Col>
+			<Col flex="auto">
+				<Search placeholder="input search text" onSearch={onSearch} style={{ width: 200 }} />
+			</Col>
+			<Col flex="200px">登录信息</Col>
+		</Row>		
+	</Header>
+
   );
 };
 

+ 107 - 46
dashboard/src/components/studio/LeftSider.tsx

@@ -1,55 +1,116 @@
 import { Link } from "react-router-dom";
-import { Space } from "antd";
 import { useIntl } from "react-intl";
 import { useParams } from "react-router-dom";
+import type { MenuProps } from 'antd';
+import { Affix , Layout} from "antd";
+import { Menu } from 'antd';
+import { AppstoreOutlined, HomeOutlined, TeamOutlined } from '@ant-design/icons';
+
+const { Sider } = Layout;
+
+const onClick: MenuProps['onClick'] = e => {
+    console.log('click ', e);
+  };
+
+type IWidgetHeadBar ={
+	selectedKeys?: string
+}
+const Widget = ({selectedKeys = ''}: IWidgetHeadBar) => {
+//Library head bar
+const intl = useIntl();//i18n
+const { studioname } = useParams();
+// TODO
+const linkPalicanon = "/studio/"+studioname+"/palicanon";
+const linkRecent = "/studio/"+studioname+"/recent";
+const linkChannel = "/studio/"+studioname+"/channel";
+const linkGroup = "/studio/"+studioname+"/group";
+const linkUserdict = "/studio/"+studioname+"/dict";
+const linkTerm = "/studio/"+studioname+"/term";
+const linkArticle = "/studio/"+studioname+"/article";
+const linkAnthology = "/studio/"+studioname+"/anthology";
+const linkAnalysis = "/studio/"+studioname+"/analysis";
+
+const items: MenuProps['items'] = [
+	{
+		label: "常用",
+		key: 'basic',
+		icon: <HomeOutlined />,
+		children:[
+			{
+				label: (<Link to = {linkPalicanon}>{intl.formatMessage({ id: "columns.studio.palicanon.title" })}</Link>),
+				key: 'palicanon',
+			},
+			{
+				label: (<Link to = {linkRecent}>{intl.formatMessage({ id: "columns.studio.recent.title" })}</Link>),
+				key: 'recent',
+			},
+			{
+				label: (<Link to = {linkChannel}>{intl.formatMessage({ id: "columns.studio.channel.title" })}</Link>),
+				key: 'channel',
+			  },
+			  {
+				label: (<Link to = {linkAnalysis}>{intl.formatMessage({ id: "columns.studio.analysis.title" })}</Link>),
+				key: 'analysis',
+			  },
+				  
+		]
+	  },
+	  {
+		label: "高级",
+		key: 'advance',
+		icon: <AppstoreOutlined/>,
+		children:[
+			{
+				label: (<Link to = {linkUserdict}>{intl.formatMessage({ id: "columns.studio.userdict.title" })}</Link>),
+				key: 'userdict',
+			  },
+			  {
+				label: (<Link to = {linkTerm}>{intl.formatMessage({ id: "columns.studio.term.title" })}</Link>),
+				key: 'term',
+			  },
+			  {
+				label: (<Link to = {linkArticle}>{intl.formatMessage({ id: "columns.studio.article.title" })}</Link>),
+				key: 'article',
+			  },
+			  {
+				label: (<Link to = {linkAnthology}>{intl.formatMessage({ id: "columns.studio.anthology.title" })}</Link>),
+				key: 'anthology',
+			  },
+		
+		],
+	  },
+	  {
+		label: "协作",
+		key: 'collaboration',
+		icon:<TeamOutlined />,
+		children:[
+			{
+				label: (<Link to = {linkGroup}>{intl.formatMessage({ id: "columns.studio.group.title" })}</Link>),
+				key: 'group',
+			  },
+		
+		]
+	  },
+]
 
-const Widget = () => {
-	//Library head bar
-	const intl = useIntl();//i18n
-	const { studioname } = useParams();
-	// TODO
-	const linkPalicanon = "/studio/"+studioname+"/palicanon";
-	const linkRecent = "/studio/"+studioname+"/recent";
-	const linkChannel = "/studio/"+studioname+"/channel";
-	const linkGroup = "/studio/"+studioname+"/group";
-	const linkUserdict = "/studio/"+studioname+"/dict";
-	const linkTerm = "/studio/"+studioname+"/term";
-	const linkArticle = "/studio/"+studioname+"/article";
-	const linkAnthology = "/studio/"+studioname+"/anthology";
-	const linkAnalysis = "/studio/"+studioname+"/analysis";
   return (
-  <div>
-	<Space>
-		<Link to={linkPalicanon}>
-			{intl.formatMessage({ id: "columns.studio.palicanon.title" })}
-		</Link>
-		<Link to={linkRecent}>
-			{intl.formatMessage({ id: "columns.studio.recent.title" })}
-		</Link>
-		<Link to={linkChannel}>
-			{intl.formatMessage({ id: "columns.studio.channel.title" })}
-		</Link>
-		<Link to={linkGroup}>
-			{intl.formatMessage({ id: "columns.studio.group.title" })}
-		</Link>
-		<Link to={linkUserdict}>
-			{intl.formatMessage({ id: "columns.studio.userdict.title" })}
-		</Link>
-		<Link to={linkTerm}>
-			{intl.formatMessage({ id: "columns.studio.term.title" })}
-		</Link>
-		<Link to={linkArticle}>
-			{intl.formatMessage({ id: "columns.studio.article.title" })}
-		</Link>
-		<Link to={linkAnthology}>
-			{intl.formatMessage({ id: "columns.studio.anthology.title" })}
-		</Link>
-		<Link to={linkAnalysis}>
-			{intl.formatMessage({ id: "columns.studio.analysis.title" })}
-		</Link>
-	</Space>
+	
+	<Affix offsetTop={0} >
+		<Sider 
+		   width={200} 
+		   breakpoint="lg"
+		   className="site-layout-background">
+			<Menu
+			theme="light"
+			onClick={onClick}
+			defaultSelectedKeys={[selectedKeys]}
+			defaultOpenKeys={['basic','advance','collaboration']}
+			mode="inline"
+			items={items}
+			/>			
+		</Sider>
+	</Affix>
 
-  </div>
   );
 };
 

+ 109 - 0
dashboard/src/components/studio/group/GroupFile.tsx

@@ -0,0 +1,109 @@
+import { useIntl } from "react-intl";
+import { useState } from 'react';
+import { ProList } from '@ant-design/pro-components';
+import { Space, Tag, Button, Layout } from 'antd';
+const {  Content } = Layout;
+
+const defaultData = [
+	{
+	  id: '1',
+	  name: '庄春江工作站',
+	  tag:[{title:"可编辑",color:"success"}],
+	  image:
+		'https://gw.alipayobjects.com/zos/antfincdn/efFD%24IOql2/weixintupian_20170331104822.jpg',
+		description: 'IAPT|2022-1-3',
+	},
+	{
+	  id: '2',
+	  name: '元亨寺·CBETA',
+	  tag:[{title:"可编辑",color:"success"}],
+	  image:
+		'https://gw.alipayobjects.com/zos/antfincdn/efFD%24IOql2/weixintupian_20170331104822.jpg',
+		description: '我是一条测试的描述',
+	},
+	{
+	  id: '3',
+	  name: '叶均居士',
+	  tag:[{title:"只读",color:"default"}],
+	  image:
+		'https://gw.alipayobjects.com/zos/antfincdn/efFD%24IOql2/weixintupian_20170331104822.jpg',
+		description: '我是一条测试的描述',
+	},
+	{
+	  id: '4',
+	  name: '玛欣德尊者',
+	  tag:[{title:"只读",color:"default"}],
+	  image:
+		'https://gw.alipayobjects.com/zos/antfincdn/efFD%24IOql2/weixintupian_20170331104822.jpg',
+		description: '我是一条测试的描述',
+	},
+  ];
+  type DataItem = typeof defaultData[number];
+  type IWidgetGroupFile ={
+	groupid?: string
+}
+  const Widget = ({groupid = ''}: IWidgetGroupFile) => {
+	const intl = useIntl();//i18n
+	const [dataSource, setDataSource] = useState<DataItem[]>(defaultData);
+
+  return (
+			<Content>
+
+				<Space>{groupid}</Space>
+				<ProList<DataItem>
+					rowKey="id"
+					headerTitle={intl.formatMessage({ id: "group.files" })}
+					dataSource={dataSource}
+					showActions="hover"
+					onDataSourceChange={setDataSource}
+					metas={{
+						title: {
+						dataIndex: 'name',
+						},
+						avatar: {
+						dataIndex: 'image',
+						editable: false,
+						},
+						description: {
+						dataIndex: 'description',
+						},
+						content: {
+							dataIndex: 'content',
+							editable: false,
+						},
+						subTitle: {
+						render: (text, row, index, action) => {
+							const showtag = row.tag.map((item,key) => {
+								return <Tag color={item.color}>{item.title}</Tag>
+							});
+							return (
+							<Space size={0}>
+								{showtag}
+							</Space>
+							);
+						},
+						},
+						actions: {
+						render: (text, row, index, action) => [
+							<Button
+							onClick={() => {
+								action?.startEditable(row.id);
+							}}
+							key="link"
+							>
+							删除
+							</Button>,
+						],
+						},
+					}}
+					pagination={{
+						showQuickJumper: true,
+						showSizeChanger: true,
+					  }}
+				/>			
+			</Content>
+
+  );
+};
+
+export default Widget;

+ 101 - 0
dashboard/src/components/studio/group/GroupMember.tsx

@@ -0,0 +1,101 @@
+import { useIntl } from "react-intl";
+import { useState } from 'react';
+import { ProList } from '@ant-design/pro-components';
+import { Space, Tag, Button, Layout } from 'antd';
+const {  Content } = Layout;
+
+const defaultData = [
+	{
+	  id: '1',
+	  name: '小僧善巧',
+	  tag:[{title:"管理员",color:"success"}],
+	  image:'https://gw.alipayobjects.com/zos/antfincdn/efFD%24IOql2/weixintupian_20170331104822.jpg',
+	},
+	{
+	  id: '2',
+	  name: '无语',
+	  tag:[{title:"管理员",color:"success"}],
+	  image:'https://gw.alipayobjects.com/zos/antfincdn/efFD%24IOql2/weixintupian_20170331104822.jpg',
+	},
+	{
+	  id: '3',
+	  name: '慧欣',
+	  tag:[],
+	  image:'https://gw.alipayobjects.com/zos/antfincdn/efFD%24IOql2/weixintupian_20170331104822.jpg',
+	},
+	{
+	  id: '4',
+	  name: '谭博文',
+	  tag:[],
+	  image:'https://gw.alipayobjects.com/zos/antfincdn/efFD%24IOql2/weixintupian_20170331104822.jpg',
+	},
+	{
+		id: '4',
+		name: '豆沙猫',
+		tag:[],
+		image:'https://gw.alipayobjects.com/zos/antfincdn/efFD%24IOql2/weixintupian_20170331104822.jpg',
+	},  
+	{
+		id: '4',
+		name: 'visuddhinanda',
+		tag:[],
+		image:'https://gw.alipayobjects.com/zos/antfincdn/efFD%24IOql2/weixintupian_20170331104822.jpg',
+	}, 	
+	];
+  type DataItem = typeof defaultData[number];
+  type IWidgetGroupFile ={
+	groupid?: string
+}
+  const Widget = ({groupid = ''}: IWidgetGroupFile) => {
+	const intl = useIntl();//i18n
+	const [dataSource, setDataSource] = useState<DataItem[]>(defaultData);
+
+  return (
+			<Content>
+				<Space>{groupid}</Space>
+				<ProList<DataItem>
+					rowKey="id"
+					headerTitle={intl.formatMessage({ id: "group.member" })}
+					dataSource={dataSource}
+					showActions="hover"
+					onDataSourceChange={setDataSource}
+					metas={{
+						title: {
+						dataIndex: 'name',
+						},
+						avatar: {
+						dataIndex: 'image',
+						editable: false,
+						},
+						subTitle: {
+						render: (text, row, index, action) => {
+							const showtag = row.tag.map((item,key) => {
+								return <Tag color={item.color}>{item.title}</Tag>
+							});
+							return (
+							<Space size={0}>
+								{showtag}
+							</Space>
+							);
+						},
+						},
+						actions: {
+						render: (text, row, index, action) => [
+							<Button
+							onClick={() => {
+								action?.startEditable(row.id);
+							}}
+							key="link"
+							>
+							删除
+							</Button>,
+						],
+						},
+					}}
+				/>			
+			</Content>
+
+  );
+};
+
+export default Widget;

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

@@ -0,0 +1,8 @@
+const items = {
+  "group": "群组",
+  "group.fields.name.label": "群组名",
+  "group.files": "群文档",
+  "group.member": "群成员",
+};
+
+export default items;

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

@@ -4,6 +4,8 @@ import tables from "./tables";
 import nut from "./nut";
 import channel from "./channel";
 import dict from "./dict";
+import term from "./term";
+import group from "./group";
 
 const items = {
   "flashes.success": "操作成功",
@@ -15,8 +17,11 @@ const items = {
   "columns.library.dict.title": "字典",
   "columns.library.anthology.title": "文集",
   "columns.library.help.title": "帮助",
+  "columns.library.more.title": "更多",
+  "columns.library.calendar.title": "佛历",
   "columns.library.convertor.title": "巴利字符转换器",
   "columns.library.palihandbook.title": "语法手册",
+  "columns.library.statistics.title": "单词统计",
   "columns.studio.title": "译经楼",
   "columns.studio.palicanon.title": "圣典",
   "columns.studio.recent.title": "最近编辑",
@@ -27,12 +32,17 @@ const items = {
   "columns.studio.article.title": "文章",
   "columns.studio.anthology.title": "文集",
   "columns.studio.analysis.title": "分析",
+  "columns.studio.collaboration.title": "协作",
+  "columns.studio.basic.title": "常用",
+  "columns.studio.advance.title": "高级",
   ...buttons,
   ...forms,
   ...tables,
   ...nut,
   ...channel,
   ...dict,
+  ...term,
+  ...group,
 };
 
 export default items;

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

@@ -0,0 +1,13 @@
+const items = {
+  "term": "字典",
+  "term.fields.sn.label": "序号",
+  "term.fields.word.label": "词头",
+  "term.fields.description.label": "标签",
+  "term.fields.description.tooltip": "单词说明,可以用来区分相同拼写的不同单词。",
+  "term.fields.channel.label": "所属版本",
+  "term.fields.meaning.label": "意思",
+  "term.fields.meaning2.label": "其他意思",
+  "term.fields.note.label": "注释",
+};
+
+export default items;

+ 11 - 0
dashboard/src/pages/README.md

@@ -1,3 +1,14 @@
 # 前端页面
 
 nut目录为练习用途。里面的内容可能会被删除。**线上不要使用**。
+
+
+## 目录
+
+- `library` 藏经阁(用户前台)
+  - `community` 最新更新
+  - `palicanon` 选经
+  - `course` 课程
+  - `dict` 字典
+  - `term `  术语百科
+  - `anthology` 文集

+ 13 - 10
dashboard/src/pages/studio/analysis/index.tsx

@@ -1,25 +1,28 @@
 import { useParams ,Link} from "react-router-dom";
 import { useIntl } from "react-intl";
-import { Space } from "antd";
+import { Layout,Space } from "antd";
 import HeadBar from "../../../components/studio/HeadBar";
 import LeftSider from "../../../components/studio/LeftSider";
 import Footer from "../../../components/studio/Footer";
+const {  Content } = Layout;
 
 const Widget = () => {
 	const intl = useIntl();//i18n
 	const { studioname } = useParams();//url 参数
   return (
-    <div>
+    <Layout>
 		<HeadBar/>
-		<LeftSider/>
-      <h2>studio/{studioname}/{intl.formatMessage({ id: "columns.studio.analysis.title" })}/行为分析首页</h2>
-      <div>
-		<Space>
-			<Link to=""> </Link>
-		</Space>
-      </div>
+		<Layout>
+			<LeftSider selectedKeys="analysis"/>
+			<Content>
+				<h2>studio/{studioname}/{intl.formatMessage({ id: "columns.studio.analysis.title" })}/行为分析首页</h2>
+				<Space>
+					<Link to=""> </Link>
+				</Space>
+			</Content>
+		</Layout>
       <Footer/>
-    </div>
+    </Layout>
   );
 };
 

+ 14 - 8
dashboard/src/pages/studio/anthology/edit.tsx

@@ -1,22 +1,28 @@
 import { useParams } from "react-router-dom";
 import { useIntl } from "react-intl";
+import { Layout } from "antd";
+
 import HeadBar from "../../../components/studio/HeadBar";
 import LeftSider from "../../../components/studio/LeftSider";
 import Footer from "../../../components/studio/Footer";
 
+const {  Content } = Layout;
+
 const Widget = () => {
 	const intl = useIntl();
 	const { studioname,anthology_id } = useParams();//url 参数
   return (
-    <div>
+	<Layout>
 		<HeadBar/>
-		<LeftSider/>
-      <h2>studio/{studioname}/{intl.formatMessage({ id: "columns.studio.anthology.title" })}/anthology/{anthology_id}</h2>
-      <div>
-		
-      </div>
-      <Footer/>
-    </div>
+		<Layout>
+			<LeftSider selectedKeys="anthology"/>
+			<Content>
+				<h2>studio/{studioname}/{intl.formatMessage({ id: "columns.studio.anthology.title" })}/anthology/{anthology_id}</h2>
+			</Content>
+		</Layout>
+		<Footer/>
+    </Layout>
+
   );
 };
 

+ 16 - 10
dashboard/src/pages/studio/anthology/index.tsx

@@ -1,26 +1,32 @@
 import { useParams ,Link} from "react-router-dom";
 import { useIntl } from "react-intl";
-import { Space } from "antd";
+import { Layout,Space } from "antd";
 import HeadBar from "../../../components/studio/HeadBar";
 import LeftSider from "../../../components/studio/LeftSider";
 import Footer from "../../../components/studio/Footer";
 
+const {  Content } = Layout;
+
 const Widget = () => {
 	const intl = useIntl();//i18n
 	const { studioname } = useParams();//url 参数
 	const linkEdit = `/studio/${studioname}/anthology/edit/12345`;
   return (
-    <div>
+    <Layout>
 		<HeadBar/>
-		<LeftSider/>
-      <h2>studio/{studioname}/{intl.formatMessage({ id: "columns.studio.article.title" })}/文集列表</h2>
-      <div>
-		<Space>
-			<Link to={linkEdit}> anthology edit </Link>
-		</Space>
-      </div>
+		<Layout>
+			<LeftSider selectedKeys="anthology"/>
+			<Content>
+				<h2>studio/{studioname}/{intl.formatMessage({ id: "columns.studio.article.title" })}/文集列表</h2>
+				<div>
+					<Space>
+						<Link to={linkEdit}> anthology edit </Link>
+					</Space>
+				</div>
+	  		</Content>
+		</Layout>
       <Footer/>
-    </div>
+    </Layout>
   );
 };
 

+ 13 - 8
dashboard/src/pages/studio/article/edit.tsx

@@ -1,22 +1,27 @@
 import { useParams } from "react-router-dom";
 import { useIntl } from "react-intl";
+import { Layout } from "antd";
+
 import HeadBar from "../../../components/studio/HeadBar";
 import LeftSider from "../../../components/studio/LeftSider";
 import Footer from "../../../components/studio/Footer";
+const {  Content } = Layout;
 
 const Widget = () => {
 	const intl = useIntl();
 	const { studioname,articleid } = useParams();//url 参数
   return (
-    <div>
+	<Layout>
 		<HeadBar/>
-		<LeftSider/>
-      <h2>studio/{studioname}/{intl.formatMessage({ id: "columns.studio.article.title" })}/edit/{articleid}</h2>
-      <div>
-		
-      </div>
-      <Footer/>
-    </div>
+		<Layout>
+			<LeftSider selectedKeys="article"/>
+			<Content>
+			<h2>studio/{studioname}/{intl.formatMessage({ id: "columns.studio.article.title" })}/edit/{articleid}</h2>
+			</Content>
+		</Layout>
+		<Footer/>
+	</Layout>
+
   );
 };
 

+ 16 - 11
dashboard/src/pages/studio/article/index.tsx

@@ -1,26 +1,31 @@
 import { useParams ,Link} from "react-router-dom";
 import { useIntl } from "react-intl";
-import { Space } from "antd";
+import { Space,Layout } from "antd";
 import HeadBar from "../../../components/studio/HeadBar";
 import LeftSider from "../../../components/studio/LeftSider";
 import Footer from "../../../components/studio/Footer";
+const {  Content } = Layout;
 
 const Widget = () => {
 	const intl = useIntl();//i18n
 	const { studioname } = useParams();//url 参数
 	const linkEdit = `/studio/${studioname}/article/edit/12345`;
   return (
-    <div>
+	<Layout>
 		<HeadBar/>
-		<LeftSider/>
-      <h2>studio/{studioname}/{intl.formatMessage({ id: "columns.studio.article.title" })}/文章列表</h2>
-      <div>
-		<Space>
-			<Link to={linkEdit}> article edit </Link>
-		</Space>
-      </div>
-      <Footer/>
-    </div>
+		<Layout>
+			<LeftSider selectedKeys="article"/>
+			<Content>
+			<h2>studio/{studioname}/{intl.formatMessage({ id: "columns.studio.article.title" })}/文章列表</h2>
+			<div>
+				<Space>
+					<Link to={linkEdit}> article edit </Link>
+				</Space>
+			</div>
+			</Content>
+		</Layout>
+		<Footer/>
+	</Layout>
   );
 };
 

+ 14 - 13
dashboard/src/pages/studio/channel/edit.tsx

@@ -1,11 +1,13 @@
 import { useParams } from "react-router-dom";
 import { ProForm, ProFormText , ProFormSelect,ProFormTextArea } from "@ant-design/pro-components";
 import { useIntl } from "react-intl";
-import { message } from "antd";
+import { message,Layout,Space } from "antd";
 import HeadBar from "../../../components/studio/HeadBar";
 import LeftSider from "../../../components/studio/LeftSider";
 import Footer from "../../../components/studio/Footer";
 
+const {  Content } = Layout;
+
 interface IFormData {
 	name: string;
 	type: string;
@@ -15,15 +17,14 @@ interface IFormData {
   }
 const Widget = () => {
 	const intl = useIntl();
-	const { studioname,channelid } = useParams();//url 参数
+	const { channelid } = useParams();//url 参数
   return (
-    <div>
-		<HeadBar/>
-		<LeftSider/>
-      <h2>studio/{studioname}/{intl.formatMessage({ id: "columns.studio.channel.title" })}/edit/{channelid}</h2>
-      <div>
-		
-		<div>
+	<Layout>
+	<HeadBar/>
+	<Layout>
+		<LeftSider selectedKeys="channel"/>
+		<Content>
+		<Space>{channelid}</Space>
 		<ProForm<IFormData>
 		onFinish={async (values: IFormData) => {
 			// TODO
@@ -73,10 +74,10 @@ const Widget = () => {
 		</ProForm.Group>
 
 		</ProForm>
-		</div>
-      </div>
-      <Footer/>
-    </div>
+	  </Content>
+	</Layout>
+	<Footer/>
+</Layout>
   );
 };
 

+ 19 - 12
dashboard/src/pages/studio/channel/index.tsx

@@ -1,26 +1,33 @@
 import { useParams ,Link} from "react-router-dom";
 import { useIntl } from "react-intl";
-import { Space } from "antd";
+import { Space,Layout } from "antd";
 import HeadBar from "../../../components/studio/HeadBar";
 import LeftSider from "../../../components/studio/LeftSider";
 import Footer from "../../../components/studio/Footer";
 
+const {  Content } = Layout;
+
 const Widget = () => {
 	const intl = useIntl();//i18n
 	const { studioname } = useParams();//url 参数
 	const linkEdit = `/studio/${studioname}/channel/edit/12345`;
   return (
-    <div>
-		<HeadBar/>
-		<LeftSider/>
-      <h2>studio/{studioname}/{intl.formatMessage({ id: "columns.studio.channel.title" })}/版本列表</h2>
-      <div>
-		<Space>
-			<Link to={linkEdit}> channel1 edit </Link>
-		</Space>
-      </div>
-      <Footer/>
-    </div>
+	<Layout>
+	<HeadBar/>
+	<Layout>
+		<LeftSider selectedKeys="channel"/>
+		<Content>
+		<h2>studio/{studioname}/{intl.formatMessage({ id: "columns.studio.channel.title" })}/版本列表</h2>
+		<div>
+			<Space>
+				<Link to={linkEdit}> channel1 edit </Link>
+			</Space>
+		</div>		
+	  </Content>
+	</Layout>
+	<Footer/>
+</Layout>
+
   );
 };
 

+ 151 - 120
dashboard/src/pages/studio/dict/index.tsx

@@ -2,13 +2,15 @@ import { useParams } from "react-router-dom";
 import { ProTable } from "@ant-design/pro-components";
 import { useIntl } from "react-intl";
 import { Link } from "react-router-dom";
-import { Button,Layout } from "antd";
+import { Button,Layout,Space, Table  } from "antd";
 import {  PlusOutlined } from '@ant-design/icons';
 
 import HeadBar from "../../../components/studio/HeadBar";
 import LeftSider from "../../../components/studio/LeftSider";
 import Footer from "../../../components/studio/Footer";
 
+const {  Content } = Layout;
+
 interface IItem {
   id: number;
   word: string;
@@ -28,136 +30,165 @@ const valueEnum = {
 	3: 'ind',
   };
 
+
 const Widget = () => {
   const intl = useIntl();
   const { studioname } = useParams();
   return (
 	<Layout>
-		<HeadBar/>
-		<LeftSider/>
+	<HeadBar/>
+	<Layout>
+		<LeftSider selectedKeys="userdict"/>
+		<Content>
+
 		<Layout>{studioname}</Layout>
-    <ProTable<IItem>
-      columns={[
-        {
-          title: intl.formatMessage({ id: "dict.fields.sn.label" }),
-          dataIndex: "id",
-          key: "id",
-          width: 80,
-          search: false,
-        },
-        {
-          title: intl.formatMessage({ id: "dict.fields.word.label" }),
-          dataIndex: "word",
-          key: "word",
-		  render: (_) => <Link to="">{_}</Link>,
-		  tip: '单词过长会自动收缩',
-		  ellipsis: true,
-		  formItemProps: {
-			rules: [
-			  {
-				required: true,
-				message: '此项为必填项',
-			  },
-			],
-		  },
-        },
-		{
-			title: intl.formatMessage({ id: "dict.fields.type.label" }),
-			dataIndex: "type",
-			key: "type",
+		<ProTable<IItem>
+		columns={[
+			{
+			title: intl.formatMessage({ id: "dict.fields.sn.label" }),
+			dataIndex: "id",
+			key: "id",
+			width: 80,
 			search: false,
-			filters: true,
-			onFilter: true,
-			valueEnum: {
-			  all: { text: '全部', status: 'Default' },
-			  n: { text: '名词', status: 'Default' },
-			  ti: { text: '三性', status: 'Processing' },
-			  v: { text: '动词', status: 'Success' },
-			  ind: { text: '不变词', status: 'Success' },
 			},
-		},
-		{
-			title: intl.formatMessage({ id: "dict.fields.grammar.label" }),
-			dataIndex: "grammar",
-			key: "grammar",
-			search: false,
-		},
-		{
-			title: intl.formatMessage({ id: "dict.fields.parent.label" }),
-			dataIndex: "parent",
-			key: "parent",
-		},
-		{
-			title: intl.formatMessage({ id: "dict.fields.meaning.label" }),
-			dataIndex: "meaning",
-			key: "meaning",
-			tip: '意思过长会自动收缩',
+			{
+			title: intl.formatMessage({ id: "dict.fields.word.label" }),
+			dataIndex: "word",
+			key: "word",
+			render: (_) => <Link to="">{_}</Link>,
+			tip: '单词过长会自动收缩',
 			ellipsis: true,
-		},
-		{
-			title: intl.formatMessage({ id: "dict.fields.note.label" }),
-			dataIndex: "note",
-			key: "note",
-			search: false,
-			tip: '注释过长会自动收缩',
-			ellipsis: true,
-		},
-		{
-			title: intl.formatMessage({ id: "dict.fields.factors.label" }),
-			dataIndex: "factors",
-			key: "factors",
-			search: false,
-		},
-        {
-          title: intl.formatMessage({ id: "forms.fields.created-at.label" }),
-          key: "created-at",
-          width: 200,
+			formItemProps: {
+				rules: [
+				{
+					required: true,
+					message: '此项为必填项',
+				},
+				],
+			},
+			},
+			{
+				title: intl.formatMessage({ id: "dict.fields.type.label" }),
+				dataIndex: "type",
+				key: "type",
+				search: false,
+				filters: true,
+				onFilter: true,
+				valueEnum: {
+				all: { text: '全部', status: 'Default' },
+				n: { text: '名词', status: 'Default' },
+				ti: { text: '三性', status: 'Processing' },
+				v: { text: '动词', status: 'Success' },
+				ind: { text: '不变词', status: 'Success' },
+				},
+			},
+			{
+				title: intl.formatMessage({ id: "dict.fields.grammar.label" }),
+				dataIndex: "grammar",
+				key: "grammar",
+				search: false,
+			},
+			{
+				title: intl.formatMessage({ id: "dict.fields.parent.label" }),
+				dataIndex: "parent",
+				key: "parent",
+			},
+			{
+				title: intl.formatMessage({ id: "dict.fields.meaning.label" }),
+				dataIndex: "meaning",
+				key: "meaning",
+				tip: '意思过长会自动收缩',
+				ellipsis: true,
+			},
+			{
+				title: intl.formatMessage({ id: "dict.fields.note.label" }),
+				dataIndex: "note",
+				key: "note",
+				search: false,
+				tip: '注释过长会自动收缩',
+				ellipsis: true,
+			},
+			{
+				title: intl.formatMessage({ id: "dict.fields.factors.label" }),
+				dataIndex: "factors",
+				key: "factors",
+				search: false,
+			},
+			{
+			title: intl.formatMessage({ id: "forms.fields.created-at.label" }),
+			key: "created-at",
+			width: 200,
 
-          search: false,
-		  dataIndex: 'createdAt',
-		  valueType: 'date',
-    	sorter: (a, b) => a.createdAt - b.createdAt,
-        },
-      ]}
-      request={async (params = {}, sorter, filter) => {
-        // TODO
-        console.log(params, sorter, filter);
+			search: false,
+			dataIndex: 'createdAt',
+			valueType: 'date',
+			sorter: (a, b) => a.createdAt - b.createdAt,
+			},
+		]}
+		rowSelection={{
+			// 自定义选择项参考: https://ant.design/components/table-cn/#components-table-demo-row-selection-custom
+			// 注释该行则默认不显示下拉选项
+			selections: [Table.SELECTION_ALL, Table.SELECTION_INVERT],
+		}}
+		tableAlertRender={({ selectedRowKeys, selectedRows, onCleanSelected }) => (
+			<Space size={24}>
+			<span>
+				已选 {selectedRowKeys.length} 项
+				<Button type="link" style={{ marginInlineStart: 8 }} onClick={onCleanSelected}>
+				取消选择
+				</Button>
+			</span>
+			</Space>
+		)}
+		tableAlertOptionRender={() => {
+			return (
+			<Space size={16}>
+				<Button type="link">批量删除</Button>
+				<Button type="link">导出数据</Button>
+			</Space>
+			);
+		}}
+		request={async (params = {}, sorter, filter) => {
+			// TODO
+			console.log(params, sorter, filter);
 
-        const size = params.pageSize || 20;
-        return {
-          total: 1 << 12,
-          success: true,
-          data: Array.from(Array(size).keys()).map((x) => {
-            const id = ((params.current || 1) - 1) * size + x + 1;
-			
-            var it: IItem = {
-              id,
-              word: `word ${id}`,
-			  type: valueEnum[2],
-			  grammar: "阳-单-属",
-			  parent: `parent ${id}`,
-			  meaning: `meaning ${id}`,
-			  note: `note ${id}`,
-			  factors: `factors ${id}`,
-              createdAt: Date.now() - Math.floor(Math.random() * 200000),
-            };
-            return it;
-          }),
-        };
-      }}
-      rowKey="id"
-      bordered
-      pagination={{
-        showQuickJumper: true,
-        showSizeChanger: true,
-      }}
-      headerTitle={intl.formatMessage({ id: "dict" })}
-	  toolBarRender={() => [
-        <Button key="button" icon={<PlusOutlined />} type="primary">
-          新建
-        </Button>,
-      ]}
-    />
+			const size = params.pageSize || 20;
+			return {
+			total: 1 << 12,
+			success: true,
+			data: Array.from(Array(size).keys()).map((x) => {
+				const id = ((params.current || 1) - 1) * size + x + 1;
+				
+				var it: IItem = {
+				id,
+				word: `word ${id}`,
+				type: valueEnum[2],
+				grammar: "阳-单-属",
+				parent: `parent ${id}`,
+				meaning: `meaning ${id}`,
+				note: `note ${id}`,
+				factors: `factors ${id}`,
+				createdAt: Date.now() - Math.floor(Math.random() * 200000),
+				};
+				return it;
+			}),
+			};
+		}}
+		rowKey="id"
+		bordered
+		pagination={{
+			showQuickJumper: true,
+			showSizeChanger: true,
+		}}
+		headerTitle={intl.formatMessage({ id: "dict" })}
+		toolBarRender={() => [
+			<Button key="button" icon={<PlusOutlined />} type="primary">
+			新建
+			</Button>,
+		]}
+		/>
+	  	</Content>
+	</Layout>
 	<Footer/>
 	</Layout>
   );

+ 127 - 15
dashboard/src/pages/studio/group/index.tsx

@@ -1,28 +1,140 @@
-import { useParams ,Link} from "react-router-dom";
+import { useParams,Link } from "react-router-dom";
 import { useIntl } from "react-intl";
-import { Space } from "antd";
+import { useState } from 'react';
+import { ProList } from '@ant-design/pro-components';
+import { Space, Tag, Button, Layout,Breadcrumb } from 'antd';
+
+
 import HeadBar from "../../../components/studio/HeadBar";
 import LeftSider from "../../../components/studio/LeftSider";
 import Footer from "../../../components/studio/Footer";
+const {  Content } = Layout;
+
+
+const defaultData = [
+	{
+	  id: '1',
+	  name: 'IAPT巴利语学习营',
+	  tag:[{title:"巴利语",color:"blue"},{title:"大金塔",color:"yellow"},{title:"拥有者",color:"success"}],
+	  image:
+		'https://gw.alipayobjects.com/zos/antfincdn/efFD%24IOql2/weixintupian_20170331104822.jpg',
+		description: '我是一条测试的描述',
+	},
+	{
+	  id: '2',
+	  name: '初级巴利语入门',
+	  tag:[{title:"巴利语",color:"blue"},{title:"管理员",color:"processing"}],
+	  image:
+		'https://gw.alipayobjects.com/zos/antfincdn/efFD%24IOql2/weixintupian_20170331104822.jpg',
+		description: '我是一条测试的描述',
+	},
+	{
+	  id: '3',
+	  name: '大金塔寺学习小组',
+	  tag:[{title:"大金塔",color:"yellow"},{title:"成员",color:"default"}],
+	  image:
+		'https://gw.alipayobjects.com/zos/antfincdn/efFD%24IOql2/weixintupian_20170331104822.jpg',
+		description: '我是一条测试的描述',
+	},
+	{
+	  id: '4',
+	  name: '趣向涅槃之道第一册翻译组',
+	  tag:[{title:"大金塔",color:"yellow"},{title:"成员",color:"default"}],
+	  image:
+		'https://gw.alipayobjects.com/zos/antfincdn/efFD%24IOql2/weixintupian_20170331104822.jpg',
+		description: '我是一条测试的描述',
+	},
+  ];
+  type DataItem = typeof defaultData[number];
 
 const Widget = () => {
 	const intl = useIntl();//i18n
 	const { studioname } = useParams();//url 参数
-	const linkEdit = `/studio/${studioname}/group/edit/12345`;
-	const linkShow = `/studio/${studioname}/group/12345`;
+	const [dataSource, setDataSource] = useState<DataItem[]>(defaultData);
+	const linkStudio = `/studio/${studioname}`;
+	const linkGroup = `${linkStudio}/group`;
   return (
-    <div>
+    <Layout>
 		<HeadBar/>
-		<LeftSider/>
-      <h2>studio/{studioname}/{intl.formatMessage({ id: "columns.studio.channel.title" })}/版本列表</h2>
-      <div>
-		<Space>
-			<Link to={linkEdit}> group1 edit </Link>
-			<Link to={linkShow}> group1 show </Link>
-		</Space>
-      </div>
-      <Footer/>
-    </div>
+		<Layout>
+			<LeftSider selectedKeys="group"/>
+			<Content>
+				<Breadcrumb>
+					<Breadcrumb.Item>
+						<Link to={linkStudio}>{intl.formatMessage({ id: "columns.studio.title" })}</Link>
+					</Breadcrumb.Item>
+					<Breadcrumb.Item>
+						{intl.formatMessage({ id: "columns.studio.collaboration.title" })}
+					</Breadcrumb.Item>
+					<Breadcrumb.Item >
+						<Link to={linkGroup}>{intl.formatMessage({ id: "columns.studio.group.title" })}</Link>
+					</Breadcrumb.Item>
+					<Breadcrumb.Item>列表</Breadcrumb.Item>
+				</Breadcrumb>
+				<Layout>
+					<ProList<DataItem>
+					rowKey="id"
+					headerTitle="群组列表"
+					dataSource={dataSource}
+					showActions="hover"
+					editable={{
+						onSave: async (key, record, originRow) => {
+						console.log(key, record, originRow);
+						return true;
+						},
+					}}
+					onDataSourceChange={setDataSource}
+					metas={{
+						title: {
+						dataIndex: 'name',
+						render: (text, row, index, action) => {
+							return (
+								<Link to ={row.id}>{row.name}</Link>
+							);
+						},
+						},
+						avatar: {
+						dataIndex: 'image',
+						editable: false,
+						},
+						description: {
+						dataIndex: 'description',
+						},
+						content: {
+							dataIndex: 'content',
+							editable: false,
+						},
+						subTitle: {
+						render: (text, row, index, action) => {
+							const showtag = row.tag.map((item,key) => {
+								return <Tag color={item.color}>{item.title}</Tag>
+							});
+							return (
+							<Space size={0}>
+								{showtag}
+							</Space>
+							);
+						},
+						},
+						actions: {
+						render: (text, row, index, action) => [
+							<Button
+							onClick={() => {
+								action?.startEditable(row.id);
+							}}
+							key="link"
+							>
+							编辑
+							</Button>,
+						],
+						},
+					}}
+					/>
+				</Layout>				
+			</Content>
+		</Layout>
+        <Footer/>
+    </Layout>
   );
 };
 

+ 32 - 10
dashboard/src/pages/studio/group/show.tsx

@@ -1,24 +1,46 @@
-import { useParams } from "react-router-dom";
+import { useParams,Link } from "react-router-dom";
 import { useIntl } from "react-intl";
-
+import {  Layout,Breadcrumb } from 'antd';
+import { Col, Row} from 'antd';
 import HeadBar from "../../../components/studio/HeadBar";
 import LeftSider from "../../../components/studio/LeftSider";
 import Footer from "../../../components/studio/Footer";
+import GroupFile from "../../../components/studio/group/GroupFile";
+import GroupMember from "../../../components/studio/group/GroupMember";
 
+const {  Content } = Layout;
 
 const Widget = () => {
 	const intl = useIntl();
 	const { studioname,groupid } = useParams();//url 参数
+	const linkStudio = `/studio/${studioname}`;
+	const linkGroup = `${linkStudio}/group`;
   return (
-    <div>
+    <Layout>
 		<HeadBar/>
-		<LeftSider/>
-      <h2>studio/{studioname}/{intl.formatMessage({ id: "columns.studio.channel.title" })}/show/{groupid}</h2>
-      <div>
-		群组详情
-      </div>
-      <Footer/>
-    </div>
+		<Layout>
+			<LeftSider selectedKeys="group"/>
+			<Content>
+				<Breadcrumb>
+					<Breadcrumb.Item>
+						<Link to={linkStudio}>{intl.formatMessage({ id: "columns.studio.title" })}</Link>
+					</Breadcrumb.Item>
+					<Breadcrumb.Item>
+						{intl.formatMessage({ id: "columns.studio.collaboration.title" })}
+					</Breadcrumb.Item>
+					<Breadcrumb.Item >
+						<Link to={linkGroup}>{intl.formatMessage({ id: "columns.studio.group.title" })}</Link>
+					</Breadcrumb.Item>
+					<Breadcrumb.Item>{groupid}</Breadcrumb.Item>
+				</Breadcrumb>
+				<Row>
+					<Col flex="auto"><GroupFile groupid={groupid}/></Col>
+					<Col flex="400px"><GroupMember groupid={groupid}/></Col>
+				</Row>	
+			</Content>
+		</Layout>
+        <Footer/>
+    </Layout>
   );
 };
 

+ 147 - 131
dashboard/src/pages/studio/term/index.tsx

@@ -2,164 +2,180 @@ import { useParams } from "react-router-dom";
 import { ProTable } from "@ant-design/pro-components";
 import { useIntl } from "react-intl";
 import { Link } from "react-router-dom";
-import { Button,Layout } from "antd";
+import { Button,Layout,Space, Table  } from "antd";
 import {  PlusOutlined } from '@ant-design/icons';
 
 import HeadBar from "../../../components/studio/HeadBar";
 import LeftSider from "../../../components/studio/LeftSider";
 import Footer from "../../../components/studio/Footer";
 
+const {  Content } = Layout;
+
 interface IItem {
   id: number;
   word: string;
-  type: string;
-  grammar: string;
-  parent: string;
+  tag: string;
+  channel: string;
   meaning: string;  
+  meaning2: string;  
   note: string;
-  factors: string;
   createdAt: number;
 }
 
-const valueEnum = {
-	0: 'n',
-	1: 'ti',
-	2: 'v',
-	3: 'ind',
-  };
 
 const Widget = () => {
   const intl = useIntl();
   const { studioname } = useParams();
   return (
 	<Layout>
-		<HeadBar/>
-		<LeftSider/>
+	<HeadBar/>
+	<Layout>
+		<LeftSider selectedKeys="term"/>
+		<Content>
+
 		<Layout>{studioname}</Layout>
-    <ProTable<IItem>
-      columns={[
-        {
-          title: intl.formatMessage({ id: "dict.fields.sn.label" }),
-          dataIndex: "id",
-          key: "id",
-          width: 80,
-          search: false,
-        },
-        {
-          title: intl.formatMessage({ id: "dict.fields.word.label" }),
-          dataIndex: "word",
-          key: "word",
-		  render: (_) => <Link to="">{_}</Link>,
-		  tip: '单词过长会自动收缩',
-		  ellipsis: true,
-		  formItemProps: {
-			rules: [
-			  {
-				required: true,
-				message: '此项为必填项',
-			  },
-			],
-		  },
-        },
-		{
-			title: intl.formatMessage({ id: "dict.fields.type.label" }),
-			dataIndex: "type",
-			key: "type",
+		<ProTable<IItem>
+		columns={[
+			{
+			title: intl.formatMessage({ id: "term.fields.sn.label" }),
+			dataIndex: "id",
+			key: "id",
+			width: 80,
 			search: false,
-			filters: true,
-			onFilter: true,
-			valueEnum: {
-			  all: { text: '全部', status: 'Default' },
-			  n: { text: '名词', status: 'Default' },
-			  ti: { text: '三性', status: 'Processing' },
-			  v: { text: '动词', status: 'Success' },
-			  ind: { text: '不变词', status: 'Success' },
 			},
-		},
-		{
-			title: intl.formatMessage({ id: "dict.fields.grammar.label" }),
-			dataIndex: "grammar",
-			key: "grammar",
-			search: false,
-		},
-		{
-			title: intl.formatMessage({ id: "dict.fields.parent.label" }),
-			dataIndex: "parent",
-			key: "parent",
-		},
-		{
-			title: intl.formatMessage({ id: "dict.fields.meaning.label" }),
-			dataIndex: "meaning",
-			key: "meaning",
-			tip: '意思过长会自动收缩',
+			{
+			title: intl.formatMessage({ id: "term.fields.word.label" }),
+			dataIndex: "word",
+			key: "word",
+			render: (_) => <Link to="">{_}</Link>,
+			tip: '单词过长会自动收缩',
 			ellipsis: true,
-		},
-		{
-			title: intl.formatMessage({ id: "dict.fields.note.label" }),
-			dataIndex: "note",
-			key: "note",
-			search: false,
-			tip: '注释过长会自动收缩',
-			ellipsis: true,
-		},
-		{
-			title: intl.formatMessage({ id: "dict.fields.factors.label" }),
-			dataIndex: "factors",
-			key: "factors",
+			formItemProps: {
+				rules: [
+				{
+					required: true,
+					message: '此项为必填项',
+				},
+				],
+			},
+			},
+			{
+				title: intl.formatMessage({ id: "term.fields.description.label" }),
+				dataIndex: "tag",
+				key: "description",
+				search: false,
+			},
+			{
+				title: intl.formatMessage({ id: "term.fields.channel.label" }),
+				dataIndex: "channel",
+				valueType: 'select',
+				valueEnum: {
+				  all: { text: '全部' },
+				  1: { text: '中文' },
+				  2: { text: '中文草稿' },
+				  3: { text: '英文' },
+				  4: { text: '英文草稿' },
+				  5: { text: 'Visuddhinanda' },
+				},
+			},
+			{
+				title: intl.formatMessage({ id: "term.fields.meaning.label" }),
+				dataIndex: "meaning",
+				key: "meaning",
+			},
+			{
+				title: intl.formatMessage({ id: "term.fields.meaning2.label" }),
+				dataIndex: "meaning2",
+				key: "meaning2",
+				tip: '意思过长会自动收缩',
+				ellipsis: true,
+			},
+			{
+				title: intl.formatMessage({ id: "term.fields.note.label" }),
+				dataIndex: "note",
+				key: "note",
+				search: false,
+				tip: '注释过长会自动收缩',
+				ellipsis: true,
+			},
+			{
+			title: intl.formatMessage({ id: "forms.fields.created-at.label" }),
+			key: "created-at",
+			width: 200,
 			search: false,
-		},
-        {
-          title: intl.formatMessage({ id: "forms.fields.created-at.label" }),
-          key: "created-at",
-          width: 200,
-
-          search: false,
-		  dataIndex: 'createdAt',
-		  valueType: 'date',
-    	sorter: (a, b) => a.createdAt - b.createdAt,
-        },
-      ]}
-      request={async (params = {}, sorter, filter) => {
-        // TODO
-        console.log(params, sorter, filter);
+			dataIndex: 'createdAt',
+			valueType: 'date',
+			sorter: (a, b) => a.createdAt - b.createdAt,
+			},
+		]}
+		rowSelection={{
+			// 自定义选择项参考: https://ant.design/components/table-cn/#components-table-demo-row-selection-custom
+			// 注释该行则默认不显示下拉选项
+			selections: [Table.SELECTION_ALL, Table.SELECTION_INVERT],
+		}}
+		tableAlertRender={({ selectedRowKeys, selectedRows, onCleanSelected }) => (
+			<Space size={24}>
+			<span>
+				已选 {selectedRowKeys.length} 项
+				<Button type="link" style={{ marginInlineStart: 8 }} onClick={onCleanSelected}>
+				取消选择
+				</Button>
+			</span>
+			</Space>
+		)}
+		tableAlertOptionRender={() => {
+			return (
+			<Space size={16}>
+				<Button type="link">批量删除</Button>
+				<Button type="link">导出数据</Button>
+			</Space>
+			);
+		}}
+		request={async (params = {}, sorter, filter) => {
+			// TODO
+			console.log(params, sorter, filter);
 
-        const size = params.pageSize || 20;
-        return {
-          total: 1 << 12,
-          success: true,
-          data: Array.from(Array(size).keys()).map((x) => {
-            const id = ((params.current || 1) - 1) * size + x + 1;
-			
-            var it: IItem = {
-              id,
-              word: `word ${id}`,
-			  type: valueEnum[2],
-			  grammar: "阳-单-属",
-			  parent: `parent ${id}`,
-			  meaning: `meaning ${id}`,
-			  note: `note ${id}`,
-			  factors: `factors ${id}`,
-              createdAt: Date.now() - Math.floor(Math.random() * 200000),
-            };
-            return it;
-          }),
-        };
-      }}
-      rowKey="id"
-      bordered
-      pagination={{
-        showQuickJumper: true,
-        showSizeChanger: true,
-      }}
-      headerTitle={intl.formatMessage({ id: "dict" })}
-	  toolBarRender={() => [
-        <Button key="button" icon={<PlusOutlined />} type="primary">
-          新建
-        </Button>,
-      ]}
-    />
+			const size = params.pageSize || 20;
+			return {
+			total: 1 << 12,
+			success: true,
+			data: Array.from(Array(size).keys()).map((x) => {
+				const id = ((params.current || 1) - 1) * size + x + 1;
+				
+				var it: IItem = {
+				id,
+				word: `word ${id}`,
+				tag: "",
+				channel: "2",
+				meaning: `parent ${id}`,
+				meaning2: `meaning ${id}`,
+				note: `note ${id}`,
+				createdAt: Date.now() - Math.floor(Math.random() * 200000),
+				};
+				return it;
+			}),
+			};
+		}}
+		rowKey="id"
+		//bordered
+		pagination={{
+			showQuickJumper: true,
+			showSizeChanger: true,
+		}}
+		headerTitle={intl.formatMessage({ id: "dict" })}
+		toolBarRender={() => [
+			<Button key="button" icon={<PlusOutlined />} type="primary">
+			新建
+			</Button>,
+		]}
+		search={{
+			filterType: 'light',
+		  }}
+		dateFormatter="string"
+		/>
+	  	</Content>
+	</Layout>
 	<Footer/>
-
 	</Layout>
   );
 };

+ 45 - 0
documents/development/frontend/api-v1.md

@@ -0,0 +1,45 @@
+# API In V1
+
+```javascript
+/**
+ * add 添加一个点赞记录
+ * @api    /api/v2/like 
+ * @method POST
+ * @param {string} liketype 下列值之一 like,favorite,watch
+ * @param {string} restype 资源类型 下列值之一 chapter,article,course
+ * @param {string} resid   资源 uuid
+ *
+ * @response {json}
+ *          data{
+                ok: bool,
+                message: string,
+                data:{
+                    type: string, 资源类型 下列值之一 chapter,article,course
+                    target_id: string, 资源 uuid
+                    id: string 点赞记录的uuid
+                }
+            }
+ */
+function add(liketype, restype, resid) {
+    fetch('/api/v2/like', {
+        method: 'POST',
+        credentials: 'include',
+        headers: {
+            'Content-Type': 'application/json'
+        },
+        body: JSON.stringify({
+            type: liketype,
+            target_type: restype,
+            target_id: resid
+        })
+    })
+        .then(response => response.json())
+        .then(function (data) {
+            console.log(data);
+            let result = data.data;
+            if (data.ok == true) {
+                // TODO
+            }
+        });
+}
+```