Browse Source

Merge pull request #1033 from visuddhinanda/agile

:sparkles: create channelPicker
visuddhinanda 3 years ago
parent
commit
62bb3e12bc
100 changed files with 7240 additions and 1226 deletions
  1. 114 107
      dashboard/src/Router.tsx
  2. 22 0
      dashboard/src/assets/general/images/wikipali_login_page.svg
  3. 69 0
      dashboard/src/assets/icon/index.tsx
  4. 99 23
      dashboard/src/components/api/Article.ts
  5. 11 7
      dashboard/src/components/api/Auth.ts
  6. 42 6
      dashboard/src/components/api/Channel.ts
  7. 128 84
      dashboard/src/components/api/Corpus.ts
  8. 41 0
      dashboard/src/components/api/Dict.ts
  9. 23 0
      dashboard/src/components/api/Group.ts
  10. 29 0
      dashboard/src/components/api/Suggestion.ts
  11. 56 0
      dashboard/src/components/api/Term.ts
  12. 22 23
      dashboard/src/components/article/AnthologStudioList.tsx
  13. 6 2
      dashboard/src/components/article/AnthologyCard.tsx
  14. 12 11
      dashboard/src/components/article/AnthologyDetail.tsx
  15. 38 38
      dashboard/src/components/article/AnthologyList.tsx
  16. 66 0
      dashboard/src/components/article/Article.tsx
  17. 104 0
      dashboard/src/components/article/ArticleCard.tsx
  18. 73 0
      dashboard/src/components/article/ArticleCardMainMenu.tsx
  19. 105 0
      dashboard/src/components/article/ArticleTabs.tsx
  20. 63 0
      dashboard/src/components/article/ArticleView.tsx
  21. 56 0
      dashboard/src/components/article/Find.tsx
  22. 38 0
      dashboard/src/components/article/Nav.tsx
  23. 32 0
      dashboard/src/components/article/PaliTextToc.tsx
  24. 64 54
      dashboard/src/components/article/TocTree.tsx
  25. 73 25
      dashboard/src/components/auth/SignInAvatar.tsx
  26. 28 13
      dashboard/src/components/auth/StudioName.tsx
  27. 14 0
      dashboard/src/components/auth/ToLibaray.tsx
  28. 23 0
      dashboard/src/components/auth/ToStudio.tsx
  29. 17 0
      dashboard/src/components/auth/User.tsx
  30. 18 0
      dashboard/src/components/auth/setting/SettingArticle.tsx
  31. 129 0
      dashboard/src/components/auth/setting/SettingItem.tsx
  32. 190 0
      dashboard/src/components/auth/setting/default.ts
  33. 6 0
      dashboard/src/components/auth/setting/index.tsx
  34. 9 0
      dashboard/src/components/channel/Channel.tsx
  35. 43 44
      dashboard/src/components/channel/ChannelList.tsx
  36. 13 15
      dashboard/src/components/channel/ChannelListItem.tsx
  37. 50 0
      dashboard/src/components/channel/ChannelPicker.tsx
  38. 324 0
      dashboard/src/components/channel/ChannelPickerTable.tsx
  39. 104 0
      dashboard/src/components/channel/ProgressSvg.tsx
  40. 384 0
      dashboard/src/components/code/my.ts
  41. 337 0
      dashboard/src/components/code/si.ts
  42. 677 0
      dashboard/src/components/code/tai-tham.ts
  43. 135 0
      dashboard/src/components/code/thai.ts
  44. 10 5
      dashboard/src/components/corpus/BookTree.tsx
  45. 59 73
      dashboard/src/components/corpus/ChapterCard.tsx
  46. 22 13
      dashboard/src/components/corpus/ChapterHead.tsx
  47. 73 32
      dashboard/src/components/corpus/ChapterInChannel.tsx
  48. 2 2
      dashboard/src/components/corpus/ChapterTagList.tsx
  49. 34 34
      dashboard/src/components/corpus/PaliChapterChannelList.tsx
  50. 58 56
      dashboard/src/components/corpus/PaliChapterHead.tsx
  51. 35 35
      dashboard/src/components/corpus/PaliChapterListByPara.tsx
  52. 36 36
      dashboard/src/components/corpus/PaliChapterListByTag.tsx
  53. 56 43
      dashboard/src/components/corpus/TocPath.tsx
  54. 64 0
      dashboard/src/components/dict/DictComponent.tsx
  55. 7 5
      dashboard/src/components/dict/DictSearch.tsx
  56. 29 32
      dashboard/src/components/general/UiLangSelect.tsx
  57. 80 19
      dashboard/src/components/library/HeadBar.tsx
  58. 172 0
      dashboard/src/components/library/article/ProTabs.tsx
  59. 24 0
      dashboard/src/components/library/article/TermShell.tsx
  60. 37 0
      dashboard/src/components/nut/Home.tsx
  61. 38 0
      dashboard/src/components/nut/InnerDrawer.tsx
  62. 65 0
      dashboard/src/components/nut/users/ForgotPassword.tsx
  63. 80 36
      dashboard/src/components/nut/users/SignIn.tsx
  64. 9 5
      dashboard/src/components/studio/Confidence.tsx
  65. 55 9
      dashboard/src/components/studio/EditableTree.tsx
  66. 20 0
      dashboard/src/components/studio/GoBack.tsx
  67. 17 8
      dashboard/src/components/studio/HeadBar.tsx
  68. 42 0
      dashboard/src/components/studio/LangSelect.tsx
  69. 39 0
      dashboard/src/components/studio/PublicitySelect.tsx
  70. 234 86
      dashboard/src/components/studio/SelectCase.tsx
  71. 44 28
      dashboard/src/components/studio/SelectLang.tsx
  72. 32 21
      dashboard/src/components/studio/anthology/AnthologyCreate.tsx
  73. 24 21
      dashboard/src/components/studio/article/ArticleCreate.tsx
  74. 26 19
      dashboard/src/components/studio/channel/ChannelCreate.tsx
  75. 49 0
      dashboard/src/components/studio/channel/ChannelTypeSelect.tsx
  76. 17 70
      dashboard/src/components/studio/dict/DictCreate.tsx
  77. 78 0
      dashboard/src/components/studio/dict/DictEdit.tsx
  78. 121 0
      dashboard/src/components/studio/dict/DictEditInner.tsx
  79. 74 0
      dashboard/src/components/studio/table.ts
  80. 176 86
      dashboard/src/components/studio/term/TermCreate.tsx
  81. 122 0
      dashboard/src/components/studio/term/TermEditInner.tsx
  82. 31 0
      dashboard/src/components/template/MdTpl.tsx
  83. 14 0
      dashboard/src/components/template/MdView.tsx
  84. 31 0
      dashboard/src/components/template/Note.tsx
  85. 75 0
      dashboard/src/components/template/Quote.tsx
  86. 80 0
      dashboard/src/components/template/SentEdit.tsx
  87. 26 0
      dashboard/src/components/template/SentEdit/EditInfo.tsx
  88. 53 0
      dashboard/src/components/template/SentEdit/SentCell.tsx
  89. 111 0
      dashboard/src/components/template/SentEdit/SentCellEditable.tsx
  90. 29 0
      dashboard/src/components/template/SentEdit/SentContent.tsx
  91. 66 0
      dashboard/src/components/template/SentEdit/SentEditMenu.tsx
  92. 65 0
      dashboard/src/components/template/SentEdit/SentMenu.tsx
  93. 147 0
      dashboard/src/components/template/SentEdit/SentTab.tsx
  94. 80 0
      dashboard/src/components/template/SentEdit/SentTabButton.tsx
  95. 46 0
      dashboard/src/components/template/SentEdit/SuggestionAdd.tsx
  96. 51 0
      dashboard/src/components/template/SentEdit/SuggestionList.tsx
  97. 77 0
      dashboard/src/components/template/SentEdit/SuggestionTabs.tsx
  98. 105 0
      dashboard/src/components/template/SentRead.tsx
  99. 92 0
      dashboard/src/components/template/Term.tsx
  100. 14 0
      dashboard/src/components/template/Wbw/WbwCase.tsx

+ 114 - 107
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";
@@ -76,112 +77,118 @@ import StudioAnalysis from "./pages/studio/analysis";
 import StudioAnalysisList from "./pages/studio/analysis/list";
 
 const Widget = () => {
-	return (
-		<Routes>
-			<Route path="anonymous" element={<Anonymous />}>
-				<Route path="users">
-					<Route path="sign-in" element={<NutUsersSignIn />} />
-					<Route path="sign-up" element={<NutUsersSignUp />} />
-
-					<Route path="unlock">
-						<Route path="new" element={<NutUsersUnlockNew />} />
-						<Route path="verify/:token" element={<NutUsersUnlockVerify />} />
-					</Route>
-					<Route path="reset-password/:token" element={<NutUsersResetPassword />} />
-					<Route path="forgot-password" element={<NutUsersForgotPassword />} />
-				</Route>
-			</Route>
-
-			<Route path="dashboard" element={<Dashboard />}>
-				<Route path="users">
-					<Route path="change-password" element={<NutUsersChangePassword />} />
-					<Route path="logs" element={<NutUsersLogs />} />
-					<Route path="account-info" element={<NutUsersAccountInfo />} />
-				</Route>
-			</Route>
-			<Route path="switch-language" element={<NutSwitchLanguage />} />
-			<Route path="forbidden" element={<NutForbidden />} />
-			<Route path="" element={<NutHome />} />
-			<Route path="*" element={<NutNotFound />} />
-
-			<Route path="community" element={<LibraryCommunity />}>
-				<Route path="list" element={<LibraryCommunityList />} />
-				<Route path="recent" element={<LibraryCommunityRecent />} />
-			</Route>
-			<Route path="palicanon" element={<LibraryPalicanon />}>
-				<Route path="list" element={<LibraryPalicanonByPath />} />
-				<Route path="list/:root" element={<LibraryPalicanonByPath />} />
-				<Route path="list/:root/:path" element={<LibraryPalicanonByPath />} />
-				<Route path="list/:root/:path/:tag" element={<LibraryPalicanonByPath />} />
-				<Route path="chapter/:id" element={<LibraryPalicanonChapter />} />
-			</Route>
-			<Route path="course" element={<LibraryCourse />}>
-				<Route path="list" element={<LibraryCourseList />}></Route>
-				<Route path="show/:id" element={<LibraryCourseShow />}></Route>
-				<Route path="lesson/:id" element={<LibraryLessonShow />}></Route>
-			</Route>
-
-			<Route path="term/:word" element={<LibraryTerm />} />
-
-			<Route path="dict" element={<LibraryDict />}>
-				<Route path=":word" element={<LibraryDictShow />} />
-				<Route path="recent" element={<LibraryDictRecent />} />
-			</Route>
-
-			<Route path="anthology" element={<LibraryAnthology />}>
-				<Route path="list" element={<LibraryAnthologyList />} />
-				<Route path=":id" element={<LibraryAnthologyShow />} />
-				<Route path=":id/by_channel/:tags" element={<LibraryAnthologyShow />} />
-			</Route>
-
-			<Route path="article" element={<LibraryArticle />}>
-				<Route path=":type/:id" element={<LibraryArticle />} />
-				<Route path=":type/:id/param/:param" element={<LibraryArticle />} />
-				<Route path=":type/:id/tran" element={<LibraryArticle />} />
-				<Route path=":type/:id/tran/param/:param" element={<LibraryArticle />} />
-			</Route>
-
-			<Route path="blog/:studio" element={<LibraryBlog />}>
-				<Route path="overview" element={<LibraryBlogOverview />} />
-				<Route path="palicanon" element={<LibraryBlogTranslation />} />
-				<Route path="course" element={<LibraryBlogCourse />} />
-				<Route path="anthology" element={<LibraryBlogAnthology />} />
-				<Route path="term" element={<LibraryBlogTerm />} />
-			</Route>
-
-			<Route path="studio/:studioname" element={<Studio />}>
-				<Route path="home" element={<StudioHome />} />
-				<Route path="palicanon" element={<StudioPalicanon />}></Route>
-				<Route path="recent" element={<StudioRecent />}></Route>
-				<Route path="channel" element={<StudioChannel />}>
-					<Route path="list" element={<StudioChannelList />} />
-					<Route path=":channelid/edit" element={<StudioChannelEdit />} />
-				</Route>
-				<Route path="group" element={<StudioGroup />}>
-					<Route path="list" element={<StudioGroupList />} />
-					<Route path=":groupid" element={<StudioGroupShow />} />
-					<Route path=":groupid/edit" element={<StudioGroupEdit />} />
-				</Route>
-				<Route path="dict" element={<StudioDict />}>
-					<Route path="list" element={<StudioDictList />} />
-				</Route>
-				<Route path="term" element={<StudioTerm />}>
-					<Route path="list" element={<StudioTermList />} />
-				</Route>
-				<Route path="article" element={<StudioArticle />}>
-					<Route path="list" element={<StudioArticleList />} />
-					<Route path=":articleid/edit" element={<StudioArticleEdit />} />
-				</Route>
-				<Route path="anthology" element={<StudioAnthology />}>
-					<Route path="list" element={<StudioAnthologyList />}></Route>
-					<Route path=":anthology_id/edit" element={<StudioAnthologyEdit />} />
-				</Route>
-				<Route path="analysis" element={<StudioAnalysis />}>
-					<Route path="list" element={<StudioAnalysisList />} />
-				</Route>
-			</Route>
-		</Routes>
-	);
+  return (
+    <Routes>
+      <Route path="anonymous" element={<Anonymous />}>
+        <Route path="users">
+          <Route path="sign-in" element={<NutUsersSignIn />} />
+          <Route path="sign-up" element={<NutUsersSignUp />} />
+
+          <Route path="unlock">
+            <Route path="new" element={<NutUsersUnlockNew />} />
+            <Route path="verify/:token" element={<NutUsersUnlockVerify />} />
+          </Route>
+          <Route
+            path="reset-password/:token"
+            element={<NutUsersResetPassword />}
+          />
+          <Route path="forgot-password" element={<NutUsersForgotPassword />} />
+        </Route>
+      </Route>
+
+      <Route path="dashboard" element={<Dashboard />}>
+        <Route path="users">
+          <Route path="change-password" element={<NutUsersChangePassword />} />
+          <Route path="logs" element={<NutUsersLogs />} />
+          <Route path="account-info" element={<NutUsersAccountInfo />} />
+        </Route>
+      </Route>
+      <Route path="switch-language" element={<NutSwitchLanguage />} />
+      <Route path="forbidden" element={<NutForbidden />} />
+      <Route path="" element={<NutHome />} />
+      <Route path="*" element={<NutNotFound />} />
+
+      <Route path="community" element={<LibraryCommunity />}>
+        <Route path="list" element={<LibraryCommunityList />} />
+        <Route path="recent" element={<LibraryCommunityRecent />} />
+      </Route>
+      <Route path="palicanon" element={<LibraryPalicanon />}>
+        <Route path="list" element={<LibraryPalicanonByPath />} />
+        <Route path="list/:root" element={<LibraryPalicanonByPath />} />
+        <Route path="list/:root/:path" element={<LibraryPalicanonByPath />} />
+        <Route
+          path="list/:root/:path/:tag"
+          element={<LibraryPalicanonByPath />}
+        />
+        <Route path="chapter/:id" element={<LibraryPalicanonChapter />} />
+      </Route>
+      <Route path="course" element={<LibraryCourse />}>
+        <Route path="list" element={<LibraryCourseList />}></Route>
+        <Route path="show/:id" element={<LibraryCourseShow />}></Route>
+        <Route path="lesson/:id" element={<LibraryLessonShow />}></Route>
+      </Route>
+
+      <Route path="term/:word" element={<LibraryTerm />} />
+
+      <Route path="dict" element={<LibraryDict />}>
+        <Route path=":word" element={<LibraryDictShow />} />
+        <Route path="recent" element={<LibraryDictRecent />} />
+      </Route>
+
+      <Route path="anthology" element={<LibraryAnthology />}>
+        <Route path="list" element={<LibraryAnthologyList />} />
+        <Route path=":id" element={<LibraryAnthologyShow />} />
+        <Route path=":id/by_channel/:tags" element={<LibraryAnthologyShow />} />
+      </Route>
+
+      <Route path="article" element={<LibraryArticle />}>
+        <Route path=":type/:id" element={<LibraryArticleShow />} />
+        <Route path=":type/:id/:mode" element={<LibraryArticleShow />} />
+        <Route path=":type/:id/:mode/:param" element={<LibraryArticleShow />} />
+      </Route>
+
+      <Route path="blog/:studio" element={<LibraryBlog />}>
+        <Route path="overview" element={<LibraryBlogOverview />} />
+        <Route path="palicanon" element={<LibraryBlogTranslation />} />
+        <Route path="course" element={<LibraryBlogCourse />} />
+        <Route path="anthology" element={<LibraryBlogAnthology />} />
+        <Route path="term" element={<LibraryBlogTerm />} />
+      </Route>
+
+      <Route path="studio/:studioname" element={<Studio />}>
+        <Route path="home" element={<StudioHome />} />
+        <Route path="palicanon" element={<StudioPalicanon />}></Route>
+        <Route path="recent" element={<StudioRecent />}></Route>
+        <Route path="channel" element={<StudioChannel />}>
+          <Route path="list" element={<StudioChannelList />} />
+          <Route path=":channelid/edit" element={<StudioChannelEdit />} />
+        </Route>
+        <Route path="group" element={<StudioGroup />}>
+          <Route path="list" element={<StudioGroupList />} />
+          <Route path=":groupid" element={<StudioGroupShow />} />
+          <Route path=":groupid/edit" element={<StudioGroupEdit />} />
+          <Route path=":groupid/show" element={<StudioGroupShow />} />
+        </Route>
+        <Route path="dict" element={<StudioDict />}>
+          <Route path="list" element={<StudioDictList />} />
+        </Route>
+        <Route path="term" element={<StudioTerm />}>
+          <Route path="list" element={<StudioTermList />} />
+        </Route>
+        <Route path="article" element={<StudioArticle />}>
+          <Route path="list" element={<StudioArticleList />} />
+          <Route path=":articleid/edit" element={<StudioArticleEdit />} />
+        </Route>
+        <Route path="anthology" element={<StudioAnthology />}>
+          <Route path="list" element={<StudioAnthologyList />}></Route>
+          <Route path=":anthology_id/edit" element={<StudioAnthologyEdit />} />
+        </Route>
+        <Route path="analysis" element={<StudioAnalysis />}>
+          <Route path="list" element={<StudioAnalysisList />} />
+        </Route>
+      </Route>
+    </Routes>
+  );
 };
 
 export default Widget;

+ 22 - 0
dashboard/src/assets/general/images/wikipali_login_page.svg

@@ -0,0 +1,22 @@
+<svg id="logo_login" xmlns="http://www.w3.org/2000/svg"  viewBox="0 0 336.454 152">
+  <g id="Group_12" data-name="Group 12" transform="translate(-396 -319)">
+    <g id="Group_2" data-name="Group 2" transform="translate(396 319)">
+      <g id="Group_1" data-name="Group 1" transform="translate(96.697 30.301)">
+        <path id="Path_1" data-name="Path 1" d="M258.951,153.873a2.9,2.9,0,0,1-1.8-.6,2.8,2.8,0,0,1-1.039-1.543l-8.443-31.183a2.022,2.022,0,0,1-.062-.5,1.673,1.673,0,0,1,.379-1.008,1.6,1.6,0,0,1,1.32-.629h3.023a2.765,2.765,0,0,1,1.765.6,2.869,2.869,0,0,1,1.008,1.543l4.159,17.074q.064.252,1.953,9.954a.113.113,0,0,0,.129.125.112.112,0,0,0,.125-.125q2.267-9.7,2.331-9.954l4.413-17.074a2.908,2.908,0,0,1,2.773-2.14h2.456a2.908,2.908,0,0,1,2.769,2.14l4.534,17.074q.375,1.576,1.164,4.882t1.23,5.073a.112.112,0,0,0,.125.125.206.206,0,0,0,.191-.125q.187-1.136.883-4.569t1.133-5.385l4.034-17.074a2.908,2.908,0,0,1,2.773-2.14H294.8a1.618,1.618,0,0,1,1.324.629,1.772,1.772,0,0,1,.379,1.07,1.99,1.99,0,0,1-.062.441l-8,31.183a2.8,2.8,0,0,1-1.039,1.543,2.876,2.876,0,0,1-1.8.6h-4.413a2.765,2.765,0,0,1-1.765-.6,2.855,2.855,0,0,1-1.008-1.543L274.447,136.3q-.633-2.519-2.2-10.079a.232.232,0,0,0-.223-.125.222.222,0,0,0-.219.125q-1.259,6.678-2.206,10.146l-3.78,15.371a2.855,2.855,0,0,1-1.008,1.543,2.743,2.743,0,0,1-1.765.6h-4.093Z" transform="translate(-247.61 -102.469)" fill="#fff"/>
+        <path id="Path_2" data-name="Path 2" d="M399.972,86.4a4.869,4.869,0,0,1-3.4,1.23,4.758,4.758,0,0,1-3.37-1.23,4.069,4.069,0,0,1-1.32-3.12A4.145,4.145,0,0,1,393.2,80.1a4.752,4.752,0,0,1,3.37-1.23,4.845,4.845,0,0,1,3.4,1.23,4.1,4.1,0,0,1,1.355,3.179A4.027,4.027,0,0,1,399.972,86.4Zm-4.819,43.375a2.254,2.254,0,0,1-2.269-2.269V96.514a2.117,2.117,0,0,1,.66-1.543,2.186,2.186,0,0,1,1.609-.66h2.835a2.193,2.193,0,0,1,1.609.66,2.119,2.119,0,0,1,.664,1.543v30.992a2.26,2.26,0,0,1-2.273,2.269Z" transform="translate(-335.539 -78.37)" fill="#fff"/>
+        <path id="Path_3" data-name="Path 3" d="M445.639,128.994a2.253,2.253,0,0,1-2.269-2.269V79.793a2.13,2.13,0,0,1,.66-1.543,2.186,2.186,0,0,1,1.609-.66h2.71a2.214,2.214,0,0,1,1.609.66,2.115,2.115,0,0,1,.66,1.543v30.871c0,.043.031.062.094.062a.224.224,0,0,0,.16-.062L463.345,95.23a4.484,4.484,0,0,1,3.655-1.7h3.718a.947.947,0,0,1,.914.6.935.935,0,0,1-.156,1.1L461.018,107.7a.409.409,0,0,0,0,.441l12.094,18.96a1.2,1.2,0,0,1,.191.629,1.179,1.179,0,0,1-.191.629,1.1,1.1,0,0,1-1.07.629h-3.589a3.8,3.8,0,0,1-3.4-1.89l-8.318-13.922c-.082-.168-.187-.187-.316-.062l-5.6,6.49a.823.823,0,0,0-.191.5v6.615a2.254,2.254,0,0,1-2.269,2.269h-2.714Z" transform="translate(-366.921 -77.59)" fill="#fff"/>
+        <path id="Path_4" data-name="Path 4" d="M545.4,86.4a4.869,4.869,0,0,1-3.4,1.23,4.752,4.752,0,0,1-3.37-1.23,4.069,4.069,0,0,1-1.32-3.12,4.145,4.145,0,0,1,1.32-3.179A4.758,4.758,0,0,1,542,78.87a4.845,4.845,0,0,1,3.4,1.23,4.1,4.1,0,0,1,1.355,3.179A4.04,4.04,0,0,1,545.4,86.4Zm-4.819,43.375a2.26,2.26,0,0,1-2.273-2.269V96.514a2.106,2.106,0,0,1,.664-1.543,2.186,2.186,0,0,1,1.609-.66h2.835a2.193,2.193,0,0,1,1.609.66,2.115,2.115,0,0,1,.66,1.543v30.992a2.254,2.254,0,0,1-2.269,2.269Z" transform="translate(-424.176 -78.37)" fill="#fff"/>
+        <path id="Path_5" data-name="Path 5" d="M591.075,167.236a2.253,2.253,0,0,1-2.265-2.265V119.612a2.253,2.253,0,0,1,2.265-2.269h1.574a2.488,2.488,0,0,1,1.671.629,2.784,2.784,0,0,1,.914,1.574l.187,1.574c.043.086.094.129.156.129a.234.234,0,0,0,.16-.062q5.67-4.727,10.837-4.725a12.237,12.237,0,0,1,10.364,4.882q3.685,4.885,3.687,13.2a24.893,24.893,0,0,1-1.261,8.1,17.127,17.127,0,0,1-3.4,6.049,15.857,15.857,0,0,1-4.881,3.687,13.081,13.081,0,0,1-5.764,1.324q-4.534,0-9.134-3.843a.077.077,0,0,0-.125,0,.182.182,0,0,0-.062.129l.187,5.8v9.2a2.253,2.253,0,0,1-2.269,2.265h-2.839Zm12.852-19.655a7.724,7.724,0,0,0,6.491-3.433q2.519-3.433,2.519-9.482,0-12.032-8.314-12.032-3.843,0-8.252,4.159a.6.6,0,0,0-.191.441v16.82a.6.6,0,0,0,.191.441A11.6,11.6,0,0,0,603.927,147.581Z" transform="translate(-455.564 -101.28)" fill="#fff"/>
+        <path id="Path_6" data-name="Path 6" d="M697.7,134.522a10.445,10.445,0,0,1-7.529-2.8,10.46,10.46,0,0,1,2.081-16.191q5.008-3.116,15.968-4.378c.168,0,.25-.1.25-.316q-.252-7.428-6.74-7.432a17.534,17.534,0,0,0-8.377,2.456,2.118,2.118,0,0,1-1.64.219,1.987,1.987,0,0,1-1.32-1.039l-.629-1.133a2.339,2.339,0,0,1-.219-1.73,2.015,2.015,0,0,1,1.039-1.355A25.533,25.533,0,0,1,702.994,97.3q6.491,0,9.7,3.905t3.214,11.149v19.089a2.26,2.26,0,0,1-2.273,2.269h-1.574a2.471,2.471,0,0,1-1.668-.629,2.74,2.74,0,0,1-.914-1.574l-.25-1.765c-.043-.082-.094-.129-.16-.129s-.113.043-.156.129Q703.244,134.522,697.7,134.522ZM695.3,90.362a2.253,2.253,0,0,1-2.269-2.269v-.5A2.253,2.253,0,0,1,695.3,85.32h15.433A2.253,2.253,0,0,1,713,87.589v.5a2.253,2.253,0,0,1-2.265,2.269Zm4.663,38.306q3.907,0,8.318-3.909a.681.681,0,0,0,.187-.5v-8.127c0-.211-.082-.293-.25-.25q-7.5.943-10.646,2.866a5.723,5.723,0,0,0-3.152,5.01,4.458,4.458,0,0,0,1.511,3.718A6.324,6.324,0,0,0,699.967,128.668Z" transform="translate(-515.555 -82.301)" fill="#fff"/>
+        <path id="Path_7" data-name="Path 7" d="M796.443,129.811q-3.907,0-5.639-2.331t-1.734-6.807V79.793a2.119,2.119,0,0,1,.664-1.543,2.186,2.186,0,0,1,1.609-.66h2.835a2.208,2.208,0,0,1,1.609.66,2.119,2.119,0,0,1,.664,1.543v41.263a2.744,2.744,0,0,0,1.008,2.519c.082.043.269.148.566.316s.5.293.629.379.293.2.5.344a1.693,1.693,0,0,1,.473.473,1.072,1.072,0,0,1,.156.566l.25,1.386a1.808,1.808,0,0,1,.062.441,2.351,2.351,0,0,1-.379,1.324,1.94,1.94,0,0,1-1.449.945C797.681,129.791,797.072,129.811,796.443,129.811Z" transform="translate(-577.618 -77.59)" fill="#fff"/>
+        <path id="Path_8" data-name="Path 8" d="M845.452,86.4a4.869,4.869,0,0,1-3.4,1.23,4.752,4.752,0,0,1-3.37-1.23,4.069,4.069,0,0,1-1.32-3.12,4.145,4.145,0,0,1,1.32-3.179,4.752,4.752,0,0,1,3.37-1.23,4.845,4.845,0,0,1,3.4,1.23,4.1,4.1,0,0,1,1.355,3.179A4.04,4.04,0,0,1,845.452,86.4Zm-4.819,43.375a2.254,2.254,0,0,1-2.269-2.269V96.514a2.117,2.117,0,0,1,.66-1.543,2.186,2.186,0,0,1,1.609-.66h2.835a2.193,2.193,0,0,1,1.609.66,2.119,2.119,0,0,1,.664,1.543v30.992a2.26,2.26,0,0,1-2.273,2.269Z" transform="translate(-607.05 -78.37)" fill="#fff"/>
+      </g>
+      <path id="Path_9" data-name="Path 9" d="M127.853,155.309a3.752,3.752,0,0,1-3.753-3.753V138c0-21.139,10.126-33.265,27.786-33.265a3.753,3.753,0,0,1,0,7.506c-13.457,0-20.284,8.666-20.284,25.763V151.56A3.745,3.745,0,0,1,127.853,155.309Z" transform="translate(-75.636 -63.837)" fill="#f1ca23"/>
+      <path id="Path_10" data-name="Path 10" d="M146.943,223.7a3.753,3.753,0,1,1,0-7.506c7.318,0,12.434-9.8,12.434-23.83v-40.3a3.753,3.753,0,0,1,7.506,0v40.3C166.879,214.011,156.866,223.7,146.943,223.7Z" transform="translate(-87.271 -90.392)" fill="#f1ca23"/>
+      <path id="Path_11" data-name="Path 11" d="M86.483,91.472a3.752,3.752,0,0,1-3.753-3.753V3.753a3.753,3.753,0,0,1,7.506,0v83.97A3.751,3.751,0,0,1,86.483,91.472Z" transform="translate(-50.422)" fill="#f1ca23"/>
+      <path id="Path_12" data-name="Path 12" d="M45.113,91.472a3.752,3.752,0,0,1-3.753-3.753V3.753a3.753,3.753,0,0,1,7.506,0v83.97A3.751,3.751,0,0,1,45.113,91.472Z" transform="translate(-25.208)" fill="#f1ca23"/>
+      <path id="Path_13" data-name="Path 13" d="M3.753,91.472A3.752,3.752,0,0,1,0,87.719V3.753a3.753,3.753,0,0,1,7.506,0v83.97A3.756,3.756,0,0,1,3.753,91.472Z" fill="#f1ca23"/>
+    </g>
+    <text id="studio" transform="translate(639 461)" fill="#fff" font-size="33" font-family="NotoSans-ExtraLight, Noto Sans" font-weight="200"><tspan x="0" y="0">studio</tspan></text>
+  </g>
+</svg>

+ 69 - 0
dashboard/src/assets/icon/index.tsx

@@ -0,0 +1,69 @@
+import Icon from "@ant-design/icons";
+import type { CustomIconComponentProps } from "@ant-design/icons/lib/components/Icon";
+import { suggestion } from "../../reducers/suggestion";
+
+const DictSvg = () => (
+  <svg
+    xmlns="http://www.w3.org/2000/svg"
+    width="1em"
+    height="1em"
+    fill="currentColor"
+    viewBox="0 0 32 32"
+  >
+    <g transform="translate(-4 -4)">
+      <path
+        d="M24.4,2,17.9,7.85v14.3l6.5-5.85V2M8.15,5.9A12.09,12.09,0,0,0,1,7.85V26.908a.7.7,0,0,0,.65.65c.13,0,.195-.091.325-.091A15.85,15.85,0,0,1,8.15,26.05,12.09,12.09,0,0,1,15.3,28a15.659,15.659,0,0,1,7.15-1.95,13.241,13.241,0,0,1,6.175,1.378.565.565,0,0,0,.325.039.7.7,0,0,0,.65-.65V7.85A8.867,8.867,0,0,0,27,6.55V24.1a15.106,15.106,0,0,0-4.55-.65A15.659,15.659,0,0,0,15.3,25.4V7.85A12.09,12.09,0,0,0,8.15,5.9Z"
+        transform="translate(5 4)"
+      />
+    </g>
+  </svg>
+);
+
+const TermSvg = () => (
+  <svg
+    xmlns="http://www.w3.org/2000/svg"
+    width="1em"
+    height="1em"
+    fill="currentColor"
+    viewBox="0 0 24 24"
+  >
+    <path d="M5 13.18v4L12 21l7-3.82v-4L12 17l-7-3.82zM12 3L1 9l11 6 9-4.91V17h2V9L12 3z" />
+  </svg>
+);
+
+const SuggestionSvg = () => (
+  <svg width="1em" height="1em" fill="currentColor" viewBox="0 0 235 235">
+    <path d="M 159.635,82.894 C 148.467,72.784 133.286,65 117,65 100.715,65 85.656,72.783 74.487,82.894 62.844,93.434 56.431,106.871 56.431,123 c 0,27.668 15.389,43.912 20.445,48.839 1.361,1.326 4.104,4.766 7.428,9.145 -2.43,2.298 -3.934,5.412 -3.934,8.844 0,4.016 2.053,7.599 5.249,9.943 -0.298,0.974 -0.47,1.995 -0.47,3.055 0,3.836 2.096,7.228 5.292,9.293 -0.499,1.149 -0.781,2.602 -0.781,3.911 0,5.556 4.935,8.97 11,8.97 h 32.802 c 6.064,0 10.999,-3.414 10.999,-8.97 0,-1.309 -0.283,-2.66 -0.781,-3.808 3.196,-2.064 5.293,-5.508 5.293,-9.344 0,-1.06 -0.172,-2.107 -0.47,-3.081 3.196,-2.344 5.248,-5.94 5.248,-9.956 0,-3.36 -1.445,-6.419 -3.786,-8.702 3.46,-4.511 6.319,-8.068 7.721,-9.43 13.461,-13.067 20.005,-29.843 20.005,-48.709 0,-16.129 -6.413,-29.566 -18.056,-40.106 z M 117,80 l 43,55 h -29.273 v 75 h -28 V 135 H 74 Z" />
+    <path d="m 117,60.8955 c 6.56,0 16,-5.317 16,-11.877 V 19.688 c 0,-6.56 -9.44,-11.877 -16,-11.877 -6.56,0 -16,5.317 -16,11.877 v 29.3315 c 0,6.559 9.44,11.876 16,11.876 z" />
+    <path d="m 222.244,106 h -29.3305 c -6.56,0 -11.877,10.44 -11.877,17 0,6.56 5.317,17 11.877,17 h 29.3305 c 6.56,0 11.877,-10.44 11.877,-17 0,-6.56 -5.317,-17 -11.877,-17 z" />
+    <path d="M 41.2085,106 H 11.877 C 5.317,106 0,116.44 0,123 c 0,6.56 5.317,17 11.877,17 h 29.3315 c 6.56,0 11.877,-10.44 11.877,-17 0,-6.56 -5.317,-17 -11.877,-17 z" />
+    <path d="M 72.31325,55.08925 49.63875,33.98275 C 44.76375,29.59475 34.2525,32.991 29.8655,37.866 c -4.388,4.876 -6.99275,15.38625 -2.11675,19.77325 l 22.6745,21.1055 c 2.27,2.043 5.84469,1.464413 8.67569,1.464413 3.25,0 8.75206,-2.741663 11.09706,-5.347663 4.387,-4.875 6.99225,-15.38425 2.11725,-19.77225 z" />
+    <path d="m 204.2555,37.8645 c -4.39,-4.877 -13.898,-8.27125 -18.773,-3.88325 l -22.673,21.1075 c -4.876,4.389 -3.271,14.89825 1.117,19.77325 2.346,2.606 5.582,5.347663 8.832,5.347663 2.831,0 8.672,0.578587 10.941,-1.464413 l 22.673,-21.1065 c 4.876,-4.389 2.271,-14.89925 -2.117,-19.77425 z" />
+  </svg>
+);
+
+const LockSvg = () => (
+  <svg
+    xmlns="http://www.w3.org/2000/svg"
+    width="1em"
+    height="1em"
+    fill="currentColor"
+    viewBox="0 0 16 16"
+  >
+    <path d="M8 1a2 2 0 0 1 2 2v4H6V3a2 2 0 0 1 2-2zm3 6V3a3 3 0 0 0-6 0v4a2 2 0 0 0-2 2v5a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2zM5 8h6a1 1 0 0 1 1 1v5a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V9a1 1 0 0 1 1-1z" />
+  </svg>
+);
+export const DictIcon = (props: Partial<CustomIconComponentProps>) => (
+  <Icon component={DictSvg} {...props} />
+);
+export const TermIcon = (props: Partial<CustomIconComponentProps>) => (
+  <Icon component={TermSvg} {...props} />
+);
+
+export const SuggestionIcon = (props: Partial<CustomIconComponentProps>) => (
+  <Icon component={SuggestionSvg} {...props} />
+);
+
+export const LockIcon = (props: Partial<CustomIconComponentProps>) => (
+  <Icon component={LockSvg} {...props} />
+);

+ 99 - 23
dashboard/src/components/api/Article.ts

@@ -1,29 +1,105 @@
+import { ITocPathNode } from "../corpus/TocPath";
 import type { IStudioApiResponse } from "./Auth";
 
 export interface IArticleListApiResponse {
-	article: string;
-	title: string;
-	level: string;
-	children: number;
-}
-//'uid','title','subtitle','article_list','owner','lang','updated_at','created_at'
-export interface IAnthologyListApiResponse {
-	uid: string;
-	title: string;
-	subtitle: string;
-	summary: string;
-	article_list: IArticleListApiResponse[];
-	studio: IStudioApiResponse;
-	lang: string;
-	created_at: string;
-	updated_at: string;
-}
-export interface IAnthologyListApiResponse2 {
-	ok: boolean;
-	message: string;
-	data: IAnthologyListApiResponse;
+  article: string;
+  title: string;
+  level: string;
+  children: number;
 }
+export interface IAnthologyDataRequest {
+  title: string;
+  subtitle: string;
+  summary: string;
+  article_list: IArticleListApiResponse[];
+  lang: string;
+  status: number;
+}
+export interface IAnthologyDataResponse {
+  uid: string;
+  title: string;
+  subtitle: string;
+  summary: string;
+  article_list: IArticleListApiResponse[];
+  studio: IStudioApiResponse;
+  lang: string;
+  status: number;
+  childrenNumber: number;
+  created_at: string;
+  updated_at: string;
+}
+export interface IAnthologyResponse {
+  ok: boolean;
+  message: string;
+  data: IAnthologyDataResponse;
+}
+export interface IAnthologyListResponse {
+  ok: boolean;
+  message: string;
+  data: {
+    rows: IAnthologyDataResponse[];
+    count: number;
+  };
+}
+
 export interface IAnthologyStudioListApiResponse {
-	count: number;
-	studio: IStudioApiResponse;
+  ok: boolean;
+  message: string;
+  data: {
+    count: number;
+    rows: IAnthologyStudioListDataApiResponse[];
+  };
+}
+export interface IAnthologyStudioListDataApiResponse {
+  count: number;
+  studio: IStudioApiResponse;
+}
+
+export interface IArticleDataRequest {
+  uid: string;
+  title: string;
+  subtitle: string;
+  summary: string;
+  content: string;
+  content_type: string;
+  status: number;
+  lang: string;
+}
+export interface IArticleDataResponse {
+  uid: string;
+  title: string;
+  subtitle: string;
+  summary: string;
+  content: string;
+  content_type: string;
+  path?: ITocPathNode[];
+  status: number;
+  lang: string;
+  created_at: string;
+  updated_at: string;
+}
+export interface IArticleResponse {
+  ok: boolean;
+  message: string;
+  data: IArticleDataResponse;
+}
+export interface IArticleListResponse {
+  ok: boolean;
+  message: string;
+  data: {
+    rows: IArticleDataResponse[];
+    count: number;
+  };
+}
+
+export interface IArticleCreateRequest {
+  title: string;
+  lang: string;
+  studio: string;
+}
+
+export interface IAnthologyCreateRequest {
+  title: string;
+  lang: string;
+  studio: string;
 }

+ 11 - 7
dashboard/src/components/api/Auth.ts

@@ -1,12 +1,16 @@
+export type Role = "owner" | "manager" | "editor" | "member";
+
 export interface IUserApiResponse {
-	id: string;
-	name: string;
-	avatar: string;
+  id: string;
+  userName: string;
+  nickName: string;
+  avatar: string;
 }
 
 export interface IStudioApiResponse {
-	id: string;
-	name: string;
-	avatar: string;
-	owner: IUserApiResponse;
+  id: string;
+  nickName: string;
+  studioName: string;
+  avatar: string;
+  owner: IUserApiResponse;
 }

+ 42 - 6
dashboard/src/components/api/Channel.ts

@@ -1,8 +1,44 @@
+import { IStudioApiResponse, Role } from "./Auth";
+
+export interface IChannelApiData {
+  id: string;
+  name: string;
+  type: string;
+}
+
 export type ChannelInfoProps = {
-	ChannelName: string;
-	ChannelId: string;
-	ChannelType: string;
-	StudioName: string;
-	StudioId: string;
-	StudioType: string;
+  channelName: string;
+  channelId: string;
+  channelType: string;
+  studioName: string;
+  studioId: string;
+  studioType: string;
 };
+
+export type IFinal = [number, boolean];
+export interface IApiResponseChannelData {
+  uid: string;
+  name: string;
+  summary: string;
+  type: string;
+  studio: IStudioApiResponse;
+  lang: string;
+  status: number;
+  created_at: string;
+  updated_at: string;
+  role?: Role;
+  final?: IFinal[];
+}
+export interface IApiResponseChannel {
+  ok: boolean;
+  message: string;
+  data: IApiResponseChannelData;
+}
+export interface IApiResponseChannelList {
+  ok: boolean;
+  message: string;
+  data: {
+    rows: IApiResponseChannelData[];
+    count: number;
+  };
+}

+ 128 - 84
dashboard/src/components/api/Corpus.ts

@@ -1,110 +1,154 @@
+import { IUser } from "../auth/User";
+import { IChannel } from "../channel/Channel";
+
 export interface IApiPaliChapterList {
-	id: string;
-	book: number;
-	paragraph: number;
-	level: number;
-	toc: string;
-	title: string;
-	lenght: number;
-	chapter_len: number;
-	next_chapter: number;
-	prev_chapter: number;
-	parent: number;
-	chapter_strlen: number;
-	path: string;
+  id: string;
+  book: number;
+  paragraph: number;
+  level: number;
+  toc: string;
+  title: string;
+  lenght: number;
+  chapter_len: number;
+  next_chapter: number;
+  prev_chapter: number;
+  parent: number;
+  chapter_strlen: number;
+  path: string;
 }
 
-export interface IApiResponcePaliChapterList {
-	ok: boolean;
-	message: string;
-	data: { rows: IApiPaliChapterList[]; count: number };
+export interface IApiResponsePaliChapterList {
+  ok: boolean;
+  message: string;
+  data: { rows: IApiPaliChapterList[]; count: number };
 }
-export interface IApiResponcePaliChapter {
-	ok: boolean;
-	message: string;
-	data: IApiPaliChapterList;
+export interface IApiResponsePaliChapter {
+  ok: boolean;
+  message: string;
+  data: IApiPaliChapterList;
 }
 
-export interface IApiResponcePaliPara {
-	book: number;
-	paragraph: number;
-	level: number;
-	class: string;
-	toc: string;
-	text: string;
-	html: string;
-	lenght: number;
-	chapter_len: number;
-	next_chapter: number;
-	prev_chapter: number;
-	parent: number;
-	chapter_strlen: number;
-	path: string;
+export interface IApiResponsePaliPara {
+  book: number;
+  paragraph: number;
+  level: number;
+  class: string;
+  toc: string;
+  text: string;
+  html: string;
+  lenght: number;
+  chapter_len: number;
+  next_chapter: number;
+  prev_chapter: number;
+  parent: number;
+  chapter_strlen: number;
+  path: string;
 }
 
 /**
  * progress?view=chapter_channels&book=98&par=22
  */
 export interface IApiChapterChannels {
-	book: number;
-	para: number;
-	uid: string;
-	channel_id: string;
-	progress: number;
-	updated_at: string;
-	views: number;
-	likes: number[];
-	channel: {
-		type: string;
-		owner_uid: string;
-		editor_id: number;
-		name: string;
-		summary: string;
-		lang: string;
-		status: number;
-		created_at: string;
-		updated_at: string;
-		uid: string;
-	};
+  book: number;
+  para: number;
+  uid: string;
+  channel_id: string;
+  progress: number;
+  updated_at: string;
+  views: number;
+  likes: number[];
+  channel: {
+    type: string;
+    owner_uid: string;
+    editor_id: number;
+    name: string;
+    summary: string;
+    lang: string;
+    status: number;
+    created_at: string;
+    updated_at: string;
+    uid: string;
+  };
 }
 
 export interface IApiResponseChapterChannelList {
-	ok: boolean;
-	message: string;
-	data: { rows: IApiChapterChannels[]; count: number };
+  ok: boolean;
+  message: string;
+  data: { rows: IApiChapterChannels[]; count: number };
 }
 
 export interface IApiChapterTag {
-	id: string;
-	name: string;
-	count: number;
+  id: string;
+  name: string;
+  count: number;
 }
 export interface IApiResponseChapterTagList {
-	ok: boolean;
-	message: string;
-	data: { rows: IApiChapterTag[]; count: number };
+  ok: boolean;
+  message: string;
+  data: { rows: IApiChapterTag[]; count: number };
 }
 
 export interface IApiResponseChannelListData {
-	channel_id: string;
-	count: number;
-	channel: {
-		id: number;
-		type: string;
-		owner_uid: string;
-		editor_id: number;
-		name: string;
-		summary: string;
-		lang: string;
-		status: number;
-		setting: string;
-		created_at: string;
-		updated_at: string;
-		uid: string;
-	};
+  channel_id: string;
+  count: number;
+  channel: {
+    id: number;
+    type: string;
+    owner_uid: string;
+    editor_id: number;
+    name: string;
+    summary: string;
+    lang: string;
+    status: number;
+    setting: string;
+    created_at: string;
+    updated_at: string;
+    uid: string;
+  };
 }
 export interface IApiResponseChannelList {
-	ok: boolean;
-	message: string;
-	data: { rows: IApiResponseChannelListData[]; count: number };
+  ok: boolean;
+  message: string;
+  data: { rows: IApiResponseChannelListData[]; count: number };
+}
+
+export interface ISentenceRequest {
+  book: number;
+  para: number;
+  wordStart: number;
+  wordEnd: number;
+  channel: string;
+  content: string;
+}
+
+export interface ISentenceData {
+  book: number;
+  paragraph: number;
+  word_start: number;
+  word_end: number;
+  content: string;
+  html: string;
+  editor: IUser;
+  channel: IChannel;
+  updated_at: string;
+}
+
+export interface ISentenceResponse {
+  ok: boolean;
+  message: string;
+  data: ISentenceData;
+}
+
+export interface IPaliToc {
+  book: number;
+  paragraph: number;
+  level: string;
+  toc: string;
+  translation?: string;
+}
+
+export interface IPaliTocListResponse {
+  ok: boolean;
+  message: string;
+  data: { rows: IPaliToc[]; count: number };
 }

+ 41 - 0
dashboard/src/components/api/Dict.ts

@@ -0,0 +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 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;
+}
+export interface IApiResponseDict {
+	ok: boolean;
+	message: string;
+	data: IApiResponseDictlData;
+}
+export interface IApiResponseDictList {
+	ok: boolean;
+	message: string;
+	data: {
+		rows: IApiResponseDictlData[];
+		count: number;
+	};
+}

+ 23 - 0
dashboard/src/components/api/Group.ts

@@ -0,0 +1,23 @@
+import { IStudioApiResponse, Role } from "./Auth";
+
+export interface IGroupDataRequest {
+  uid: string;
+  name: string;
+  description: string;
+  studio: IStudioApiResponse;
+  role: Role;
+  created_at: string;
+}
+export interface IGroupResponse {
+  ok: boolean;
+  message: string;
+  data: IGroupDataRequest;
+}
+export interface IGroupListResponse {
+  ok: boolean;
+  message: string;
+  data: {
+    rows: IGroupDataRequest[];
+    count: number;
+  };
+}

+ 29 - 0
dashboard/src/components/api/Suggestion.ts

@@ -0,0 +1,29 @@
+import { IUserApiResponse } from "./Auth";
+import { IChannelApiData } from "./Channel";
+
+export interface ISuggestionData {
+  id: number;
+  book: number;
+  paragraph: number;
+  word_start: number;
+  word_end: number;
+  channel: IChannelApiData;
+  content: string;
+  html: string;
+  editor: IUserApiResponse;
+  created_at: string;
+  updated_at: string;
+}
+export interface ISuggestionResponse {
+  ok: boolean;
+  message: string;
+  data: ISuggestionData;
+}
+export interface ISuggestionListResponse {
+  ok: boolean;
+  message: string;
+  data: {
+    rows: ISuggestionData[];
+    count: number;
+  };
+}

+ 56 - 0
dashboard/src/components/api/Term.ts

@@ -0,0 +1,56 @@
+export interface ITermDataRequest {
+  id: number;
+  word: string;
+  tag: string;
+  meaning: string;
+  other_meaning: string;
+  note: string;
+  channal: string;
+  language: string;
+}
+export interface ITermDataResponse {
+  id: number;
+  guid: string;
+  word: string;
+  tag: string;
+  meaning: string;
+  other_meaning: string;
+  note: string;
+  channal: string;
+  language: string;
+  created_at: string;
+  updated_at: string;
+}
+export interface ITermResponse {
+  ok: boolean;
+  message: string;
+  data: ITermDataResponse;
+}
+export interface ITermListResponse {
+  ok: boolean;
+  message: string;
+  data: {
+    rows: ITermDataResponse[];
+    count: number;
+  };
+}
+
+interface IMeaningCount {
+  meaning: string;
+  count: number;
+}
+interface IStudioChannel {
+  name: string;
+  uid: string;
+}
+export interface ITermCreate {
+  word: string;
+  meaningCount: IMeaningCount[];
+  studioChannels: IStudioChannel[];
+  language: string;
+}
+export interface ITermCreateResponse {
+  ok: boolean;
+  message: string;
+  data: ITermCreate;
+}

+ 22 - 23
dashboard/src/components/article/AnthologStudioList.tsx

@@ -1,8 +1,10 @@
 import { useState, useEffect } from "react";
 import { List, Space, Card } from "antd";
-import StudioNmae from "../auth/StudioName";
+import StudioName from "../auth/StudioName";
 import type { IAnthologyStudioListApiResponse } from "../api/Article";
 import type { IStudioApiResponse } from "../api/Auth";
+import { get } from "../../request";
+import { Link, useNavigate } from "react-router-dom";
 
 const defaultData: IAnthologyStudioData[] = [];
 
@@ -17,30 +19,25 @@ interface IWidgetAnthologyList {
 */
 const Widget = () => {
 	const [tableData, setTableData] = useState(defaultData);
-
+	const navigate = useNavigate();
 	useEffect(() => {
 		console.log("useEffect");
 		fetchData();
-	}, [setTableData]);
+	}, []);
 
 	function fetchData() {
-		let url = `http://127.0.0.1:8000/api/v2/anthology?view=studio_list`;
-		fetch(url)
-			.then(function (response) {
-				console.log("ajex:", response);
-				return response.json();
-			})
-			.then(function (myJson) {
-				console.log("ajex", myJson);
-
-				let newTree: IAnthologyStudioData[] = myJson.data.rows.map((item: IAnthologyStudioListApiResponse) => {
-					return {
-						count: item.count,
-						studio: item.studio,
-					};
-				});
-				setTableData(newTree);
+		let url = `/v2/anthology?view=studio_list`;
+		get(url).then(function (myJson) {
+			console.log("ajex", myJson);
+			const json = myJson as unknown as IAnthologyStudioListApiResponse;
+			let newTree: IAnthologyStudioData[] = json.data.rows.map((item) => {
+				return {
+					count: item.count,
+					studio: item.studio,
+				};
 			});
+			setTableData(newTree);
+		});
 	}
 
 	return (
@@ -51,10 +48,12 @@ const Widget = () => {
 				dataSource={tableData}
 				renderItem={(item) => (
 					<List.Item>
-						<Space>
-							<StudioNmae data={item.studio} />
-							<span>({item.count})</span>
-						</Space>
+						<Link to={`/blog/${item.studio.studioName}/anthology`}>
+							<Space>
+								<StudioName data={item.studio} />
+								<span>({item.count})</span>
+							</Space>
+						</Link>
 					</List.Item>
 				)}
 			/>

+ 6 - 2
dashboard/src/components/article/AnthologyCard.tsx

@@ -40,7 +40,9 @@ const Widget = (prop: IWidgetAnthologyCard) => {
 		<>
 			<Card hoverable bordered={false} style={{ width: "100%" }}>
 				<Title level={4}>
-					<Link to={`/anthology/${prop.data.id}`}>{prop.data.title}</Link>
+					<Link to={`/anthology/${prop.data.id}`}>
+						{prop.data.title}
+					</Link>
 				</Title>
 				<div>
 					<Text type="secondary">{prop.data.subTitle}</Text>
@@ -48,7 +50,9 @@ const Widget = (prop: IWidgetAnthologyCard) => {
 				<div>
 					<Text>{prop.data.summary}</Text>
 				</div>
-				<StudioName data={prop.data.studio} />
+				<Link to={`/blog/${prop.data.studio.studioName}/anthology`}>
+					<StudioName data={prop.data.studio} />
+				</Link>
 				<Row>
 					<Col flex={"100px"}>Content</Col>
 					<Col flex={"auto"}>{articleList}</Col>

+ 12 - 11
dashboard/src/components/article/AnthologyDetail.tsx

@@ -1,11 +1,14 @@
 import { useState, useEffect } from "react";
 import { Typography } from "antd";
-import ReactMarkdown from "react-markdown";
 
 import type { IAnthologyData } from "./AnthologyCard";
-import type { IAnthologyListApiResponse, IAnthologyListApiResponse2 } from "../api/Article";
+import type {
+	IAnthologyDataResponse,
+	IAnthologyResponse,
+} from "../api/Article";
 import TocTree from "./TocTree";
-import { ApiFetch } from "../../utils";
+import { get } from "../../request";
+import MDEditor from "@uiw/react-md-editor";
 
 const { Title, Text } = Typography;
 
@@ -22,16 +25,16 @@ const defaultData: IAnthologyData = {
 	articles: [],
 	studio: {
 		id: "",
-		name: "",
+		studioName: "",
+		nickName: "",
 		avatar: "",
 	},
 	created_at: "",
 	updated_at: "",
 };
-//const defaultTreeData: ListNodeData[] = [];
+
 const Widget = (prop: IWidgetAnthologyDetail) => {
 	const [tableData, setTableData] = useState(defaultData);
-	//const [treeData, setTreeData] = useState(defaultTreeData);
 
 	useEffect(() => {
 		console.log("useEffect");
@@ -39,11 +42,9 @@ const Widget = (prop: IWidgetAnthologyDetail) => {
 	}, [prop.aid]);
 
 	function fetchData(id: string) {
-		ApiFetch(`/anthology/${id}`)
+		get<IAnthologyResponse>(`/v2/anthology/${id}`)
 			.then((response) => {
-				const json = response as unknown as IAnthologyListApiResponse2;
-
-				const item: IAnthologyListApiResponse = json.data;
+				const item: IAnthologyDataResponse = response.data;
 				let newTree: IAnthologyData = {
 					id: item.uid,
 					title: item.title,
@@ -74,7 +75,7 @@ const Widget = (prop: IWidgetAnthologyDetail) => {
 				<Text type="secondary">{tableData.subTitle}</Text>
 			</div>
 			<div>
-				<ReactMarkdown>{tableData.summary}</ReactMarkdown>
+				<MDEditor.Markdown source={tableData.summary} />
 			</div>
 			<Title level={5}>目录</Title>
 

+ 38 - 38
dashboard/src/components/article/AnthologyList.tsx

@@ -2,51 +2,51 @@ import { useState, useEffect } from "react";
 import { List } from "antd";
 import AnthologyCard from "./AnthologyCard";
 import type { IAnthologyData } from "./AnthologyCard";
-import type { IAnthologyListApiResponse } from "../api/Article";
+import type { IAnthologyListResponse } from "../api/Article";
+import { get } from "../../request";
 
 const defaultData: IAnthologyData[] = [];
-
-const Widget = () => {
+interface IWidgetAnthologyList {
+	view: string;
+	id?: string;
+}
+const Widget = (prop: IWidgetAnthologyList) => {
 	const [tableData, setTableData] = useState(defaultData);
 
 	useEffect(() => {
-		console.log("useEffect");
-		fetchData();
-	}, [setTableData]);
-
-	function fetchData() {
-		let url = `http://127.0.0.1:8000/api/v2/anthology?view=public`;
-		fetch(url)
-			.then(function (response) {
-				console.log("ajex:", response);
-				return response.json();
-			})
-			.then(function (myJson) {
-				console.log("ajex", myJson);
+		console.log("useEffect", prop);
+		if (typeof prop.id === "undefined") {
+			fetchData(prop.view);
+		} else {
+			fetchData(prop.view, prop.id);
+		}
+	}, [prop]);
 
-				let newTree: IAnthologyData[] = myJson.data.rows.map((item: IAnthologyListApiResponse) => {
-					return {
-						id: item.uid,
-						title: item.title,
-						subTitle: item.subtitle,
-						summary: item.summary,
-						articles: item.article_list.map((al) => {
-							return {
-								id: al.article,
-								title: al.title,
-								subTitle: "",
-								summary: "",
-								created_at: "",
-								updated_at: "",
-							};
-						}),
-						studio: item.studio,
-						created_at: item.created_at,
-						updated_at: item.updated_at,
-					};
-				});
-				setTableData(newTree);
+	function fetchData(view: string, id?: string) {
+		let url = `/v2/anthology?view=${view}` + (id ? `&studio=${id}` : "");
+		console.log("get-url", url);
+		get<IAnthologyListResponse>(url).then(function (response) {
+			console.log("ajex", response);
+			let newTree: IAnthologyData[] = response.data.rows.map((item) => {
+				return {
+					id: item.uid,
+					title: item.title,
+					subTitle: item.subtitle,
+					summary: item.summary,
+					articles: item.article_list.map((al) => {
+						return {
+							key: al.article,
+							title: al.title,
+							level: parseInt(al.level),
+						};
+					}),
+					studio: item.studio,
+					created_at: item.created_at,
+					updated_at: item.updated_at,
+				};
 			});
+			setTableData(newTree);
+		});
 	}
 
 	return (

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

@@ -0,0 +1,66 @@
+import { message } from "antd";
+import { useEffect, useState } from "react";
+import { get } from "../../request";
+import { IArticleDataResponse, IArticleResponse } from "../api/Article";
+import ArticleView from "./ArticleView";
+
+export type ArticleMode = "read" | "edit";
+export type ArticleType =
+  | "article"
+  | "chapter"
+  | "paragraph"
+  | "cs-paragraph"
+  | "sentence"
+  | "sim"
+  | "page";
+interface IWidgetArticle {
+  type?: string;
+  articleId?: string;
+  mode?: ArticleMode;
+  active?: boolean;
+}
+const Widget = ({
+  type,
+  articleId,
+  mode = "read",
+  active = false,
+}: IWidgetArticle) => {
+  const [articleData, setArticleData] = useState<IArticleDataResponse>();
+  let channels: string[] = [];
+  if (typeof articleId !== "undefined") {
+    const aId = articleId.split("_");
+    if (aId.length > 1) {
+      channels = aId.slice(1);
+    }
+  }
+
+  useEffect(() => {
+    if (!active) {
+      return;
+    }
+    if (typeof type !== "undefined" && typeof articleId !== "undefined") {
+      get<IArticleResponse>(`/v2/${type}/${articleId}/${mode}`).then((json) => {
+        if (json.ok) {
+          setArticleData(json.data);
+        } else {
+          message.error(json.message);
+        }
+      });
+    }
+  }, [active, type, articleId, mode]);
+  return (
+    <ArticleView
+      id={articleData?.uid}
+      title={articleData?.title}
+      subTitle={articleData?.subtitle}
+      summary={articleData?.summary}
+      content={articleData ? articleData.content : ""}
+      path={articleData?.path}
+      created_at={articleData?.created_at}
+      updated_at={articleData?.updated_at}
+      channels={channels}
+    />
+  );
+};
+
+export default Widget;

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

@@ -0,0 +1,104 @@
+import { Button, Card, Dropdown, Space, Segmented } from "antd";
+import { MoreOutlined, ReloadOutlined } from "@ant-design/icons";
+import type { MenuProps } from "antd";
+import { IWidgetArticleData } from "./ArticleView";
+import { useIntl } from "react-intl";
+import { useState } from "react";
+import ArticleCardMainMenu from "./ArticleCardMainMenu";
+
+interface IWidgetArticleCard {
+  type?: string;
+  articleId?: string;
+  data?: IWidgetArticleData;
+  children?: React.ReactNode;
+  onModeChange?: Function;
+  openInCol?: Function;
+  showCol?: Function;
+}
+const Widget = ({
+  type,
+  articleId,
+  data,
+  children,
+  onModeChange,
+  showCol,
+}: IWidgetArticleCard) => {
+  const intl = useIntl();
+  const [mode, setMode] = useState<string>("read");
+
+  const onClick: MenuProps["onClick"] = (e) => {
+    console.log("click ", e);
+    switch (e.key) {
+      case "showCol":
+        if (typeof showCol !== "undefined") {
+          showCol();
+        }
+        break;
+
+      default:
+        break;
+    }
+  };
+
+  const items: MenuProps["items"] = [
+    {
+      key: "showCol",
+      label: "显示分栏",
+    },
+  ];
+  const modeSwitch = (
+    <Segmented
+      size="middle"
+      options={[
+        {
+          label: intl.formatMessage({ id: "buttons.read" }),
+          value: "read",
+        },
+        {
+          label: intl.formatMessage({ id: "buttons.edit" }),
+          value: "edit",
+        },
+      ]}
+      value={mode}
+      onChange={(value) => {
+        if (typeof onModeChange !== "undefined") {
+          onModeChange(value.toString());
+        }
+        setMode(value.toString());
+      }}
+    />
+  );
+
+  const contextMenu = (
+    <Dropdown menu={{ items, onClick }} placement="bottomRight">
+      <Button shape="circle" size="small" icon={<MoreOutlined />}></Button>
+    </Dropdown>
+  );
+  return (
+    <Card
+      size="small"
+      title={
+        <Space>
+          {<ArticleCardMainMenu type={type} articleId={articleId} />}
+          {data?.title}
+        </Space>
+      }
+      extra={
+        <Space>
+          {modeSwitch}
+          <Button
+            shape="circle"
+            size="small"
+            icon={<ReloadOutlined />}
+          ></Button>
+          {contextMenu}
+        </Space>
+      }
+      bodyStyle={{ height: `calc(100vh - 94px)`, overflowY: "scroll" }}
+    >
+      {children}
+    </Card>
+  );
+};
+
+export default Widget;

+ 73 - 0
dashboard/src/components/article/ArticleCardMainMenu.tsx

@@ -0,0 +1,73 @@
+import { Tabs, Button, Popover } from "antd";
+import { MenuOutlined, PushpinOutlined } from "@ant-design/icons";
+import PaliTextToc from "./PaliTextToc";
+import Find from "./Find";
+import Nav from "./Nav";
+
+interface IWidget {
+  type?: string;
+  articleId?: string;
+}
+const Widget = ({ type, articleId }: IWidget) => {
+  const id = articleId?.split("_");
+  let tocWidget = <></>;
+  if (id && id.length > 0) {
+    const sentId = id[0].split("-");
+    if (sentId.length > 1) {
+      tocWidget = (
+        <PaliTextToc book={parseInt(sentId[0])} para={parseInt(sentId[1])} />
+      );
+    }
+  }
+  const styleTabBody: React.CSSProperties = {
+    width: 350,
+    height: "calc(100vh - 200px)",
+    overflowY: "scroll",
+  };
+  const mainMenuContent = (
+    <Tabs
+      size="small"
+      defaultActiveKey="1"
+      tabBarExtraContent={{
+        right: <Button type="text" size="small" icon={<PushpinOutlined />} />,
+      }}
+      items={[
+        {
+          label: `目录`,
+          key: "1",
+          children: <div style={styleTabBody}>{tocWidget}</div>,
+        },
+        {
+          label: `定位`,
+          key: "2",
+          children: (
+            <div style={styleTabBody}>
+              <Nav />
+            </div>
+          ),
+        },
+        {
+          label: `查找`,
+          key: "3",
+          children: (
+            <div style={styleTabBody}>
+              <Find />
+            </div>
+          ),
+        },
+      ]}
+    />
+  );
+  return (
+    <Popover
+      placement="bottomLeft"
+      arrowPointAtCenter
+      content={mainMenuContent}
+      trigger="click"
+    >
+      <Button size="small" icon={<MenuOutlined />} />
+    </Popover>
+  );
+};
+
+export default Widget;

+ 105 - 0
dashboard/src/components/article/ArticleTabs.tsx

@@ -0,0 +1,105 @@
+import { useState, useRef, useEffect } from "react";
+import { Tabs, Button } from "antd";
+import { CloseOutlined } from "@ant-design/icons";
+import { useAppSelector } from "../../hooks";
+import { siteInfo as _siteInfo } from "../../reducers/open-article";
+import Article from "./Article";
+
+const defaultPanes = [{ label: `Tab`, children: <></>, key: "1" }];
+
+const Test = () => {
+  console.log("new");
+  return <div>hello</div>;
+};
+interface IWidget {
+  onClose?: Function;
+}
+const Widget = ({ onClose }: IWidget) => {
+  const [activeKey, setActiveKey] = useState("1");
+  const [items, setItems] = useState(defaultPanes);
+  const newTabIndex = useRef(0);
+
+  const newArticle = useAppSelector(_siteInfo);
+
+  useEffect(() => {
+    console.log("open", newArticle);
+    if (typeof newArticle !== "undefined") {
+      add(newArticle?.title, newArticle?.url, newArticle?.id);
+    }
+  }, [newArticle]);
+
+  const onChange = (key: string) => {
+    setActiveKey(key);
+  };
+
+  const add = (title: string, url: string, id: string) => {
+    const newActiveKey = `newTab${newTabIndex.current++}`;
+    setItems([
+      ...items,
+      {
+        label: title,
+        children: (
+          <Article active={true} type={url} articleId={id} mode="edit" />
+        ),
+        key: newActiveKey,
+      },
+    ]);
+    setActiveKey(newActiveKey);
+  };
+
+  const remove = (
+    targetKey:
+      | React.MouseEvent<Element, MouseEvent>
+      | React.KeyboardEvent<Element>
+      | string
+  ) => {
+    const targetIndex = items.findIndex((pane) => pane.key === targetKey);
+    const newPanes = items.filter((pane) => pane.key !== targetKey);
+    if (newPanes.length && targetKey === activeKey) {
+      const { key } =
+        newPanes[
+          targetIndex === newPanes.length ? targetIndex - 1 : targetIndex
+        ];
+      setActiveKey(key);
+    }
+    setItems(newPanes);
+  };
+
+  const onEdit = (
+    targetKey:
+      | React.MouseEvent<Element, MouseEvent>
+      | React.KeyboardEvent<Element>
+      | string,
+    action: "add" | "remove"
+  ) => {
+    if (action === "add") {
+      add("new", "url", "id");
+    } else {
+      remove(targetKey);
+    }
+  };
+  const operations = (
+    <Button
+      icon={<CloseOutlined />}
+      shape="circle"
+      onClick={() => {
+        if (onClose) {
+          onClose(true);
+        }
+      }}
+    />
+  );
+  return (
+    <Tabs
+      hideAdd
+      onChange={onChange}
+      activeKey={activeKey}
+      type="editable-card"
+      onEdit={onEdit}
+      items={items}
+      tabBarExtraContent={operations}
+    />
+  );
+};
+
+export default Widget;

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

@@ -0,0 +1,63 @@
+import { Typography, Divider, Button } from "antd";
+import { ReloadOutlined } from "@ant-design/icons";
+
+import MdView from "../template/MdView";
+import TocPath, { ITocPathNode } from "../corpus/TocPath";
+
+const { Paragraph, Title } = Typography;
+
+export interface IWidgetArticleData {
+  id?: string;
+  title?: string;
+  subTitle?: string;
+  summary?: string;
+  content: string;
+  path?: ITocPathNode[];
+  created_at?: string;
+  updated_at?: string;
+  channels?: string[];
+}
+
+const Widget = ({
+  id,
+  title = "",
+  subTitle,
+  summary,
+  content,
+  path = [],
+  created_at,
+  updated_at,
+  channels,
+}: IWidgetArticleData) => {
+  console.log("path", path);
+  return (
+    <>
+      <Button shape="round" size="small" icon={<ReloadOutlined />}>
+        刷新
+      </Button>
+      <div>
+        <TocPath data={path} channel={channels} />
+        <Title type="secondary" level={5}>
+          {subTitle}
+        </Title>
+        <Title level={3}>
+          <div
+            dangerouslySetInnerHTML={{
+              __html: title ? title : "",
+            }}
+          ></div>
+        </Title>
+
+        <Paragraph ellipsis={{ rows: 2, expandable: true, symbol: "more" }}>
+          {summary}
+        </Paragraph>
+        <Divider />
+      </div>
+      <div>
+        <MdView html={content} />
+      </div>
+    </>
+  );
+};
+
+export default Widget;

+ 56 - 0
dashboard/src/components/article/Find.tsx

@@ -0,0 +1,56 @@
+import { Input, Space, Select } from "antd";
+import { useState } from "react";
+
+const { Search } = Input;
+
+const Widget = () => {
+  const [isLoading, setIsLoading] = useState(false);
+
+  const onSearch = (value: string) => {
+    setIsLoading(true);
+    console.log(value);
+  };
+  const onReplace = (value: string) => {
+    console.log(value);
+  };
+  return (
+    <div>
+      <Space direction="vertical">
+        <Search
+          placeholder="input search text"
+          allowClear
+          onSearch={onSearch}
+          style={{ width: "100%" }}
+          loading={isLoading}
+        />
+        <Search
+          placeholder="input search text"
+          allowClear
+          enterButton="替换"
+          style={{ width: "100%" }}
+          onSearch={onReplace}
+        />
+        <Select
+          defaultValue="current"
+          style={{ width: "100%" }}
+          onChange={(value: string) => {
+            console.log(`selected ${value}`);
+          }}
+          options={[
+            {
+              value: "current",
+              label: "当前文档",
+            },
+            {
+              value: "all",
+              label: "全部译文",
+            },
+          ]}
+        />
+        <div>搜索结果</div>
+      </Space>
+    </div>
+  );
+};
+
+export default Widget;

+ 38 - 0
dashboard/src/components/article/Nav.tsx

@@ -0,0 +1,38 @@
+import { Space, Select } from "antd";
+
+const Widget = () => {
+  return (
+    <div>
+      <Space direction="vertical">
+        <Select
+          defaultValue="current"
+          style={{ width: "100%" }}
+          onChange={(value: string) => {
+            console.log(`selected ${value}`);
+          }}
+          options={[
+            {
+              value: "book-mark",
+              label: "书签",
+            },
+            {
+              value: "tag",
+              label: "标签",
+            },
+            {
+              value: "suggestion",
+              label: "修改建议",
+            },
+            {
+              value: "qa",
+              label: "问答",
+            },
+          ]}
+        />
+        <div>搜索结果</div>
+      </Space>
+    </div>
+  );
+};
+
+export default Widget;

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

@@ -0,0 +1,32 @@
+import { parseInt } from "lodash";
+import { useState, useEffect } from "react";
+import { get } from "../../request";
+import { IPaliTocListResponse } from "../api/Corpus";
+import { ListNodeData } from "../studio/EditableTree";
+import TocTree from "./TocTree";
+
+interface IWidget {
+  book?: number;
+  para?: number;
+  channel?: string;
+}
+const Widget = ({ book, para, channel }: IWidget) => {
+  const [tocList, setTocList] = useState<ListNodeData[]>([]);
+  useEffect(() => {
+    get<IPaliTocListResponse>(
+      `/v2/palitext?view=book-toc&book=${book}&para=${para}`
+    ).then((json) => {
+      const toc = json.data.rows.map((item, id) => {
+        return {
+          key: `${item.book}-${item.paragraph}`,
+          title: item.toc,
+          level: parseInt(item.level),
+        };
+      });
+      setTocList(toc);
+    });
+  }, [book, para]);
+  return <TocTree treeData={tocList} />;
+};
+
+export default Widget;

+ 64 - 54
dashboard/src/components/article/TocTree.tsx

@@ -3,78 +3,88 @@ import type { TreeProps } from "antd/es/tree";
 import type { ListNodeData } from "../studio/EditableTree";
 
 type TreeNodeData = {
-	key: string;
-	title: string;
-	children: TreeNodeData[];
-	level: number;
+  key: string;
+  title: string;
+  children: TreeNodeData[];
+  level: number;
 };
 
 function tocGetTreeData(listData: ListNodeData[], active = "") {
-	let treeData = [];
-	let tocActivePath: TreeNodeData[] = [];
-	let treeParents = [];
-	let rootNode: TreeNodeData = { key: "0", title: "root", level: 0, children: [] };
-	treeData.push(rootNode);
-	let lastInsNode: TreeNodeData = rootNode;
+  let treeData = [];
+  let tocActivePath: TreeNodeData[] = [];
+  let treeParents = [];
+  let rootNode: TreeNodeData = {
+    key: "0",
+    title: "root",
+    level: 0,
+    children: [],
+  };
+  treeData.push(rootNode);
+  let lastInsNode: TreeNodeData = rootNode;
 
-	let iCurrLevel = 0;
-	for (let index = 0; index < listData.length; index++) {
-		const element = listData[index];
+  let iCurrLevel = 0;
+  for (let index = 0; index < listData.length; index++) {
+    const element = listData[index];
 
-		let newNode: TreeNodeData = { key: element.key, title: element.title, children: [], level: element.level };
-		/*
+    let newNode: TreeNodeData = {
+      key: element.key,
+      title: element.title,
+      children: [],
+      level: element.level,
+    };
+    /*
 		if (active == element.article) {
 			newNode["extraClasses"] = "active";
 		}
 */
-		if (newNode.level > iCurrLevel) {
-			//新的层级比较大,为上一个的子目录
-			treeParents.push(lastInsNode);
-			lastInsNode.children.push(newNode);
-		} else if (newNode.level === iCurrLevel) {
-			//目录层级相同,为平级
-			treeParents[treeParents.length - 1].children.push(newNode);
-		} else {
-			// 小于 挂在上一个层级
-			while (treeParents.length > 1) {
-				treeParents.pop();
-				if (treeParents[treeParents.length - 1].level < newNode.level) {
-					break;
-				}
-			}
-			treeParents[treeParents.length - 1].children.push(newNode);
-		}
-		lastInsNode = newNode;
-		iCurrLevel = newNode.level;
+    if (newNode.level > iCurrLevel) {
+      //新的层级比较大,为上一个的子目录
+      treeParents.push(lastInsNode);
+      lastInsNode.children.push(newNode);
+    } else if (newNode.level === iCurrLevel) {
+      //目录层级相同,为平级
+      treeParents[treeParents.length - 1].children.push(newNode);
+    } else {
+      // 小于 挂在上一个层级
+      while (treeParents.length > 1) {
+        treeParents.pop();
+        if (treeParents[treeParents.length - 1].level < newNode.level) {
+          break;
+        }
+      }
+      treeParents[treeParents.length - 1].children.push(newNode);
+    }
+    lastInsNode = newNode;
+    iCurrLevel = newNode.level;
 
-		if (active === element.key) {
-			tocActivePath = [];
-			for (let index = 1; index < treeParents.length; index++) {
-				//treeParents[index]["expanded"] = true;
-				tocActivePath.push(treeParents[index]);
-			}
-		}
-	}
-	return treeData[0].children;
+    if (active === element.key) {
+      tocActivePath = [];
+      for (let index = 1; index < treeParents.length; index++) {
+        //treeParents[index]["expanded"] = true;
+        tocActivePath.push(treeParents[index]);
+      }
+    }
+  }
+  return treeData[0].children;
 }
 
 type IWidgetTocTree = {
-	treeData: ListNodeData[];
+  treeData: ListNodeData[];
 };
 const onSelect: TreeProps["onSelect"] = (selectedKeys, info) => {
-	//let aaa: NewTree = info.node;
-	console.log("selected", selectedKeys);
+  //let aaa: NewTree = info.node;
+  console.log("selected", selectedKeys);
 };
-const Widget = (prop: IWidgetTocTree) => {
-	const data = tocGetTreeData(prop.treeData);
+const Widget = ({ treeData }: IWidgetTocTree) => {
+  const data = tocGetTreeData(treeData);
 
-	//const [expandedKeys] = useState(["0-0", "0-0-0", "0-0-0-0"]);
+  //const [expandedKeys] = useState(["0-0", "0-0-0", "0-0-0-0"]);
 
-	return (
-		<>
-			<Tree onSelect={onSelect} treeData={data} />
-		</>
-	);
+  return (
+    <>
+      <Tree onSelect={onSelect} treeData={data} />
+    </>
+  );
 };
 
 export default Widget;

+ 73 - 25
dashboard/src/components/auth/SignInAvatar.tsx

@@ -1,34 +1,82 @@
-import { Dropdown, Tooltip } from "antd";
+import { useEffect, useState } from "react";
+import { Link, useNavigate } from "react-router-dom";
+import { Tooltip } from "antd";
 import { Avatar } from "antd";
+import { Popover } from "antd";
 import { ProCard } from "@ant-design/pro-components";
-import { UserOutlined, HomeOutlined, LogoutOutlined, SettingOutlined } from "@ant-design/icons";
+import {
+	UserOutlined,
+	HomeOutlined,
+	LogoutOutlined,
+	SettingOutlined,
+} from "@ant-design/icons";
+
+import { useAppSelector } from "../../hooks";
+import { currentUser as _currentUser } from "../../reducers/current-user";
 
-const userCard = (
-	<>
-		<ProCard
-			style={{ maxWidth: 500, minWidth: 300 }}
-			actions={[
-				<SettingOutlined key="setting" />,
-				<HomeOutlined key="edit" />,
-				<Tooltip title="退出登录">
-					<LogoutOutlined key="ellipsis" />
-				</Tooltip>,
-			]}
-		>
-			<div>
-				<h2>kosalla</h2>
-				<div>遨游法的海洋</div>
-			</div>
-		</ProCard>
-	</>
-);
 const Widget = () => {
 	// TODO
-	return (
-		<Dropdown overlay={userCard} placement="bottomRight" arrow={{ pointAtCenter: true }}>
-			<Avatar style={{ backgroundColor: "#87d068" }} icon={<UserOutlined />} />
-		</Dropdown>
+	const navigate = useNavigate();
+	const [userName, setUserName] = useState("");
+	const [nickName, setNickName] = useState("");
+	const user = useAppSelector(_currentUser);
+	useEffect(() => {
+		setUserName(user ? user.realName : "");
+		setNickName(user ? user.nickName : "");
+	}, [user]);
+
+	const userCard = (
+		<>
+			<ProCard
+				style={{ maxWidth: 500, minWidth: 300 }}
+				actions={[
+					<Tooltip title="设置">
+						<SettingOutlined key="setting" />
+					</Tooltip>,
+					<Tooltip title="个人空间">
+						<Link to={`/blog/${userName}/overview`}>
+							<HomeOutlined key="home" />
+						</Link>
+					</Tooltip>,
+					<Tooltip title="退出登录">
+						<LogoutOutlined
+							key="logout"
+							onClick={() => {
+								sessionStorage.removeItem("token");
+								localStorage.removeItem("token");
+								navigate("/anonymous/users/sign-in");
+							}}
+						/>
+					</Tooltip>,
+				]}
+			>
+				<div>
+					<h2>{nickName}</h2>
+					<div>欢迎遨游法的海洋</div>
+				</div>
+			</ProCard>
+		</>
 	);
+
+	if (typeof user === "undefined") {
+		return <Link to="/anonymous/users/sign-in">登录</Link>;
+	} else {
+		return (
+			<>
+				<Popover content={userCard} placement="bottomRight">
+					<Avatar
+						style={{ backgroundColor: "#87d068" }}
+						icon={<UserOutlined />}
+					>
+						{nickName.slice(0, 1)}
+					</Avatar>
+				</Popover>
+			</>
+		);
+	}
 };
 
 export default Widget;
+function currentUser(currentUser: any) {
+	throw new Error("Function not implemented.");
+}

+ 28 - 13
dashboard/src/components/auth/StudioName.tsx

@@ -1,22 +1,37 @@
 import { Avatar, Space } from "antd";
 
 export interface IStudio {
-	id: string;
-	name: string;
-	avatar: string;
+  id: string;
+  nickName: string;
+  studioName: string;
+  avatar: string;
 }
 interface IWidghtStudio {
-	data: IStudio;
+  data: IStudio;
+  showAvatar?: boolean;
+  showName?: boolean;
+  onClick?: Function;
 }
-const Widget = (prop: IWidghtStudio) => {
-	// TODO
-	const name = prop.data.name.slice(0, 1);
-	return (
-		<Space>
-			<Avatar size="small">{name}</Avatar>
-			{prop.data.name}
-		</Space>
-	);
+const Widget = ({
+  data,
+  showAvatar = true,
+  showName = true,
+  onClick,
+}: IWidghtStudio) => {
+  // TODO
+  const avatar = <Avatar size="small">{data.nickName.slice(0, 1)}</Avatar>;
+  return (
+    <Space
+      onClick={() => {
+        if (typeof onClick !== "undefined") {
+          onClick(data.studioName);
+        }
+      }}
+    >
+      {showAvatar ? avatar : ""}
+      {showName ? data.nickName : ""}
+    </Space>
+  );
 };
 
 export default Widget;

+ 14 - 0
dashboard/src/components/auth/ToLibaray.tsx

@@ -0,0 +1,14 @@
+import { Button } from "antd";
+import { Link } from "react-router-dom";
+
+const Widget = () => {
+	return (
+		<>
+			<Link to="/palicanon/list">
+				<Button type="primary">藏经阁</Button>
+			</Link>
+		</>
+	);
+};
+
+export default Widget;

+ 23 - 0
dashboard/src/components/auth/ToStudio.tsx

@@ -0,0 +1,23 @@
+import { Button } from "antd";
+//import { useEffect, useState } from "react";
+import { Link } from "react-router-dom";
+import { useAppSelector } from "../../hooks";
+import { currentUser as _currentUser } from "../../reducers/current-user";
+
+const Widget = () => {
+	const user = useAppSelector(_currentUser);
+
+	if (typeof user !== "undefined") {
+		return (
+			<>
+				<Link to={`/studio/${user.realName}/home`}>
+					<Button type="primary">藏经阁</Button>
+				</Link>
+			</>
+		);
+	} else {
+		return <></>;
+	}
+};
+
+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;

+ 18 - 0
dashboard/src/components/auth/setting/SettingArticle.tsx

@@ -0,0 +1,18 @@
+import { Divider } from "antd";
+import { SettingFind } from "./default";
+import SettingItem from "./SettingItem";
+
+const Widget = () => {
+  return (
+    <div>
+      <Divider>翻译</Divider>
+      <SettingItem data={SettingFind("setting.display.original")} />
+      <SettingItem data={SettingFind("setting.layout.direction")} />
+      <SettingItem data={SettingFind("setting.layout.paragraph")} />
+      <SettingItem data={SettingFind("setting.pali.script1")} />
+      <SettingItem data={SettingFind("setting.pali.script2")} />
+    </div>
+  );
+};
+
+export default Widget;

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

@@ -0,0 +1,129 @@
+import { Switch, Typography, Radio, RadioChangeEvent, Select } from "antd";
+import {
+  onChange as onSettingChanged,
+  settingInfo,
+  ISettingItem,
+} from "../../../reducers/setting";
+import { useAppSelector } from "../../../hooks";
+import store from "../../../store";
+import { ISetting } from "./default";
+import { useEffect, useState } from "react";
+
+const { Title, Text } = Typography;
+
+interface IWidgetSettingItem {
+  data?: ISetting;
+  onChange?: Function;
+}
+const Widget = ({ data, onChange }: IWidgetSettingItem) => {
+  const settings: ISettingItem[] | undefined = useAppSelector(settingInfo);
+  const [value, setValue] = useState(data?.defaultValue);
+  const title = <Title level={5}>{data?.label}</Title>;
+  console.log(data);
+  useEffect(() => {
+    const currSetting = settings?.find((element) => element.key === data?.key);
+    if (typeof currSetting !== "undefined") {
+      setValue(currSetting.value);
+    }
+  }, [data?.key, settings]);
+  let content: JSX.Element = <></>;
+  if (typeof data === "undefined") {
+    return content;
+  } else {
+    switch (typeof data.defaultValue) {
+      case "number":
+        break;
+      case "string":
+        switch (data.widget) {
+          case "radio-button":
+            if (typeof data.options !== "undefined") {
+              return (
+                <>
+                  {title}
+                  <div>
+                    <Text>{data.description}</Text>
+                  </div>
+                  <Radio.Group
+                    value={value}
+                    buttonStyle="solid"
+                    onChange={(e: RadioChangeEvent) => {
+                      setValue(e.target.value);
+                      store.dispatch(
+                        onSettingChanged({
+                          key: data.key,
+                          value: e.target.value,
+                        })
+                      );
+                    }}
+                  >
+                    {data.options.map((item, id) => {
+                      return (
+                        <Radio.Button key={id} value={item.value}>
+                          {item.label}
+                        </Radio.Button>
+                      );
+                    })}
+                  </Radio.Group>
+                </>
+              );
+            }
+
+            break;
+          default:
+            if (typeof data.options !== "undefined") {
+              content = (
+                <div>
+                  <Select
+                    defaultValue={data.defaultValue}
+                    style={{ width: 120 }}
+                    onChange={(value: string) => {
+                      console.log(`selected ${value}`);
+                      store.dispatch(
+                        onSettingChanged({
+                          key: data.key,
+                          value: value,
+                        })
+                      );
+                    }}
+                    options={data.options}
+                  />
+                </div>
+              );
+            } else {
+            }
+            break;
+        }
+        break;
+      case "boolean":
+        content = (
+          <div>
+            <Switch
+              defaultChecked={value as boolean}
+              onChange={(checked) => {
+                if (typeof onChange !== "undefined") {
+                  onChange(checked);
+                }
+                console.log("setting changed", data.key, checked);
+                store.dispatch(
+                  onSettingChanged({ key: data.key, value: checked })
+                );
+              }}
+            />
+            <Text>{data.description}</Text>
+          </div>
+        );
+        break;
+      default:
+        break;
+    }
+
+    return (
+      <div>
+        <Title level={5}>{data.label}</Title>
+        {content}
+      </div>
+    );
+  }
+};
+
+export default Widget;

+ 190 - 0
dashboard/src/components/auth/setting/default.ts

@@ -0,0 +1,190 @@
+import { useIntl } from "react-intl";
+import { ISettingItem } from "../../../reducers/setting";
+
+export interface ISettingItemOption {
+  label: string;
+  value: string;
+}
+export interface ISetting {
+  key: string;
+  label: string;
+  description: string;
+  defaultValue: string | number | boolean;
+  value?: string | number | boolean;
+  widget?: "input" | "select" | "radio" | "radio-button";
+  options?: ISettingItemOption[];
+  max?: number;
+  min?: number;
+}
+
+export const GetUserSetting = (
+  key: string,
+  curr?: ISettingItem[]
+): string | number | boolean | undefined => {
+  const currSetting = curr?.find((element) => element.key === key);
+  if (typeof currSetting !== "undefined") {
+    return currSetting.value;
+  } else {
+    const defaultSetting = SettingFind(key);
+    if (typeof defaultSetting !== "undefined") {
+      return defaultSetting.defaultValue;
+    } else {
+      return undefined;
+    }
+  }
+};
+
+export const SettingFind = (key: string): ISetting | undefined => {
+  return Settings().find((element) => element.key === key);
+};
+export const Settings = (): ISetting[] => {
+  const intl = useIntl();
+
+  const defaultSetting: ISetting[] = [
+    {
+      /**
+       * 是否显示巴利原文
+       */
+      key: "setting.display.original",
+      label: intl.formatMessage({ id: "setting.display.original.label" }),
+      description: intl.formatMessage({
+        id: "setting.display.original.description",
+      }),
+      defaultValue: true,
+    },
+    {
+      /**
+       * 排版方向
+       */
+      key: "setting.layout.direction",
+      label: intl.formatMessage({ id: "setting.layout.direction.label" }),
+      description: intl.formatMessage({
+        id: "setting.layout.direction.description",
+      }),
+      defaultValue: "column",
+      options: [
+        {
+          value: "column",
+          label: intl.formatMessage({
+            id: "setting.layout.direction.col.label",
+          }),
+        },
+        {
+          value: "row",
+          label: intl.formatMessage({
+            id: "setting.layout.direction.row.label",
+          }),
+        },
+      ],
+      widget: "radio-button",
+    },
+    {
+      /**
+       * 段落或者逐句对读
+       */
+      key: "setting.layout.paragraph",
+      label: intl.formatMessage({ id: "setting.layout.paragraph.label" }),
+      description: intl.formatMessage({
+        id: "setting.layout.paragraph.description",
+      }),
+      defaultValue: "sentence",
+      options: [
+        {
+          value: "sentence",
+          label: intl.formatMessage({
+            id: "setting.layout.paragraph.sentence.label",
+          }),
+        },
+        {
+          value: "paragraph",
+          label: intl.formatMessage({
+            id: "setting.layout.paragraph.paragraph.label",
+          }),
+        },
+      ],
+      widget: "radio-button",
+    },
+    {
+      /**
+       * 第一巴利脚本
+       */
+      key: "setting.pali.script1",
+      label: intl.formatMessage({ id: "setting.pali.script1.label" }),
+      description: intl.formatMessage({
+        id: "setting.pali.script1.description",
+      }),
+      defaultValue: "roman",
+      options: [
+        {
+          value: "roman",
+          label: intl.formatMessage({
+            id: "setting.pali.script.rome.label",
+          }),
+        },
+        {
+          value: "roman_to_my",
+          label: intl.formatMessage({
+            id: "setting.pali.script.my.label",
+          }),
+        },
+        {
+          value: "roman_to_si",
+          label: intl.formatMessage({
+            id: "setting.pali.script.si.label",
+          }),
+        },
+        {
+          value: "roman_to_thai",
+          label: intl.formatMessage({
+            id: "setting.pali.script.thai.label",
+          }),
+        },
+        {
+          value: "roman_to_taitham",
+          label: intl.formatMessage({
+            id: "setting.pali.script.tai.label",
+          }),
+        },
+      ],
+    },
+    {
+      /**
+       * 第二巴利脚本
+       */
+      key: "setting.pali.script2",
+      label: intl.formatMessage({ id: "setting.pali.script2.label" }),
+      description: intl.formatMessage({
+        id: "setting.pali.script2.description",
+      }),
+      defaultValue: "none",
+      options: [
+        {
+          value: "none",
+          label: intl.formatMessage({
+            id: "setting.pali.script.none.label",
+          }),
+        },
+        {
+          value: "roman",
+          label: intl.formatMessage({
+            id: "setting.pali.script.rome.label",
+          }),
+        },
+        {
+          value: "roman_to_my",
+          label: intl.formatMessage({
+            id: "setting.pali.script.my.label",
+          }),
+        },
+        {
+          value: "roman_to_si",
+          label: intl.formatMessage({
+            id: "setting.pali.script.si.label",
+          }),
+        },
+      ],
+    },
+  ];
+
+  return defaultSetting;
+};

+ 6 - 0
dashboard/src/components/auth/setting/index.tsx

@@ -0,0 +1,6 @@
+const Widget = () => {
+	return <div>change password</div>;
+  };
+  
+  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;

+ 43 - 44
dashboard/src/components/channel/ChannelList.tsx

@@ -2,61 +2,60 @@ import { useState, useEffect } from "react";
 import { List } from "antd";
 import ChannelListItem from "./ChannelListItem";
 import type { ChannelInfoProps } from "../api/Channel";
-import { ApiFetch } from "../../utils";
 import { IApiResponseChannelList } from "../api/Corpus";
+import { get } from "../../request";
 
 export interface ChannelFilterProps {
-	chapterProgress: number;
-	lang: string;
-	channelType: string;
+  chapterProgress: number;
+  lang: string;
+  channelType: string;
 }
 interface IWidgetChannelList {
-	filter?: ChannelFilterProps;
+  filter?: ChannelFilterProps;
 }
 const defaultChannelFilterProps: ChannelFilterProps = {
-	chapterProgress: 0.9,
-	lang: "zh",
-	channelType: "translation",
+  chapterProgress: 0.9,
+  lang: "zh",
+  channelType: "translation",
 };
 
 const Widget = ({ filter = defaultChannelFilterProps }: IWidgetChannelList) => {
-	const defualt: ChannelInfoProps[] = [];
-	const [tableData, setTableData] = useState(defualt);
+  const [tableData, setTableData] = useState<ChannelInfoProps[]>([]);
 
-	useEffect(() => {
-		console.log("palichapterlist useEffect");
-		let url = `/progress?view=channel&channel_type=${filter.channelType}&lang=${filter.lang}&progress=${filter.chapterProgress}`;
-		ApiFetch(url).then(function (myJson) {
-			console.log("ajex", myJson);
-			const data = myJson as unknown as IApiResponseChannelList;
-			const newData: ChannelInfoProps[] = data.data.rows.map((item) => {
-				return {
-					ChannelName: item.channel.name,
-					ChannelId: item.channel.uid,
-					ChannelType: item.channel.type,
-					StudioName: "V",
-					StudioId: "123",
-					StudioType: "p",
-				};
-			});
-			setTableData(newData);
-		});
-	}, [filter]);
-	return (
-		<>
-			<h3>Channel</h3>
-			<List
-				itemLayout="vertical"
-				size="large"
-				dataSource={tableData}
-				renderItem={(item) => (
-					<List.Item>
-						<ChannelListItem data={item} />
-					</List.Item>
-				)}
-			/>
-		</>
-	);
+  useEffect(() => {
+    console.log("palichapterlist useEffect");
+    let url = `/v2/progress?view=channel&channel_type=${filter.channelType}&lang=${filter.lang}&progress=${filter.chapterProgress}`;
+    get(url).then(function (myJson) {
+      console.log("ajex", myJson);
+      const data = myJson as unknown as IApiResponseChannelList;
+      const newData: ChannelInfoProps[] = data.data.rows.map((item) => {
+        return {
+          channelName: item.channel.name,
+          channelId: item.channel.uid,
+          channelType: item.channel.type,
+          studioName: "V",
+          studioId: "123",
+          studioType: "p",
+        };
+      });
+      setTableData(newData);
+    });
+  }, [filter]);
+  return (
+    <>
+      <h3>Channel</h3>
+      <List
+        itemLayout="vertical"
+        size="large"
+        dataSource={tableData}
+        renderItem={(item) => (
+          <List.Item>
+            <ChannelListItem data={item} />
+          </List.Item>
+        )}
+      />
+    </>
+  );
 };
 
 export default Widget;

+ 13 - 15
dashboard/src/components/channel/ChannelListItem.tsx

@@ -3,23 +3,21 @@ import { Avatar } from "antd";
 import type { ChannelInfoProps } from "../api/Channel";
 
 type IWidgetChannelListItem = {
-	data: ChannelInfoProps;
-	showProgress?: boolean;
-	showLike?: boolean;
+  data: ChannelInfoProps;
+  showProgress?: boolean;
+  showLike?: boolean;
 };
 
-const Widget = (props: IWidgetChannelListItem) => {
-	const studioName = props.data.StudioName.slice(0, 2);
-	return (
-		<>
-			<Space>
-				<Avatar size="small">{studioName}</Avatar>
-				<span>
-					{props.data.ChannelName}@{props.data.StudioName}
-				</span>
-			</Space>
-		</>
-	);
+const Widget = ({ data, showProgress, showLike }: IWidgetChannelListItem) => {
+  const studioName = data.studioName.slice(0, 2);
+  return (
+    <>
+      <Space>
+        <Avatar size="small">{studioName}</Avatar>
+        {data.channelName}@{data.studioName}
+      </Space>
+    </>
+  );
 };
 
 export default Widget;

+ 50 - 0
dashboard/src/components/channel/ChannelPicker.tsx

@@ -0,0 +1,50 @@
+import { useState } from "react";
+import { Button, Modal } from "antd";
+import ChannelPickerTable from "./ChannelPickerTable";
+import { IChannel } from "./Channel";
+
+interface IWidget {
+  type: string;
+  articleId: string;
+}
+const Widget = ({ type, articleId }: IWidget) => {
+  const [isModalOpen, setIsModalOpen] = useState(false);
+
+  const showModal = () => {
+    setIsModalOpen(true);
+  };
+
+  const handleOk = () => {
+    setIsModalOpen(false);
+  };
+
+  const handleCancel = () => {
+    setIsModalOpen(false);
+  };
+
+  return (
+    <>
+      <Button type="primary" onClick={showModal}>
+        Select channel
+      </Button>
+      <Modal
+        width={"80%"}
+        title="选择版本风格"
+        open={isModalOpen}
+        onOk={handleOk}
+        onCancel={handleCancel}
+      >
+        <ChannelPickerTable
+          type={type}
+          articleId={articleId}
+          onSelect={(e: IChannel) => {
+            console.log(e);
+            handleCancel();
+          }}
+        />
+      </Modal>
+    </>
+  );
+};
+
+export default Widget;

+ 324 - 0
dashboard/src/components/channel/ChannelPickerTable.tsx

@@ -0,0 +1,324 @@
+import { ProTable } from "@ant-design/pro-components";
+import { useIntl } from "react-intl";
+import { Space, Table } from "antd";
+import { GlobalOutlined } from "@ant-design/icons";
+import type { MenuProps } from "antd";
+import { Button, Menu } from "antd";
+import { SearchOutlined } from "@ant-design/icons";
+import { PublicityValueEnum } from "../studio/table";
+import { IApiResponseChannelList, IFinal } from "../api/Channel";
+import { get } from "../../request";
+import { LockIcon } from "../../assets/icon";
+import StudioName, { IStudio } from "../auth/StudioName";
+import ProgressSvg from "./ProgressSvg";
+import { IChannel } from "./Channel";
+
+const onMenuClick: MenuProps["onClick"] = (e) => {
+  console.log("click", e);
+};
+
+const menu = (
+  <Menu
+    onClick={onMenuClick}
+    items={[
+      {
+        key: "1",
+        label: "在藏经阁中打开",
+        icon: <SearchOutlined />,
+      },
+      {
+        key: "2",
+        label: "分享",
+        icon: <SearchOutlined />,
+      },
+      {
+        key: "3",
+        label: "删除",
+        icon: <SearchOutlined />,
+      },
+    ]}
+  />
+);
+
+export interface IItem {
+  id: number;
+  uid: string;
+  title: string;
+  summary: string;
+  type: string;
+  studio: IStudio;
+  shareType: string;
+  role?: string;
+  publicity: number;
+  createdAt: number;
+  final?: IFinal[];
+}
+interface IWidget {
+  type: string;
+  articleId: string;
+  onSelect?: Function;
+}
+const Widget = ({ type, articleId, onSelect }: IWidget) => {
+  const intl = useIntl();
+
+  return (
+    <>
+      <ProTable<IItem>
+        columns={[
+          {
+            title: intl.formatMessage({
+              id: "forms.fields.name.label",
+            }),
+            dataIndex: "title",
+            key: "title",
+            search: false,
+            tip: "过长会自动收缩",
+            ellipsis: true,
+            valueType: "text",
+            render: (text, row, index, action) => {
+              let pIcon = <></>;
+              switch (row.publicity) {
+                case 10:
+                  pIcon = <LockIcon />;
+                  break;
+                case 30:
+                  pIcon = <GlobalOutlined />;
+                  break;
+              }
+              return (
+                <Button
+                  type="link"
+                  onClick={() => {
+                    if (typeof onSelect !== "undefined") {
+                      const e: IChannel = { name: row.title, id: row.uid };
+                      onSelect(e);
+                    }
+                  }}
+                >
+                  <Space>
+                    {pIcon}
+                    {row.title}
+                  </Space>
+                </Button>
+              );
+            },
+          },
+          {
+            title: intl.formatMessage({
+              id: "channel.title",
+            }),
+            render: (text, row, index, action) => {
+              return <StudioName data={row.studio} />;
+            },
+            key: "studio",
+            search: false,
+          },
+          {
+            title: intl.formatMessage({
+              id: "tables.progress.label",
+            }),
+            width: 210,
+            render: (text, row, index, action) => {
+              return <ProgressSvg data={row.final} width={200} />;
+            },
+            key: "progress",
+            search: false,
+          },
+          {
+            title: intl.formatMessage({
+              id: "forms.fields.type.label",
+            }),
+            dataIndex: "type",
+            key: "type",
+            width: 100,
+            search: false,
+            filters: true,
+            onFilter: true,
+            valueEnum: {
+              all: {
+                text: intl.formatMessage({
+                  id: "channel.type.all.title",
+                }),
+                status: "Default",
+              },
+              translation: {
+                text: intl.formatMessage({
+                  id: "channel.type.translation.label",
+                }),
+                status: "Success",
+              },
+              nissaya: {
+                text: intl.formatMessage({
+                  id: "channel.type.nissaya.label",
+                }),
+                status: "Processing",
+              },
+              commentary: {
+                text: intl.formatMessage({
+                  id: "channel.type.commentary.label",
+                }),
+                status: "Default",
+              },
+              original: {
+                text: intl.formatMessage({
+                  id: "channel.type.original.label",
+                }),
+                status: "Default",
+              },
+              general: {
+                text: intl.formatMessage({
+                  id: "channel.type.general.label",
+                }),
+                status: "Default",
+              },
+            },
+          },
+          {
+            title: intl.formatMessage({
+              id: "forms.fields.publicity.label",
+            }),
+            dataIndex: "publicity",
+            key: "publicity",
+            width: 100,
+            search: false,
+            filters: true,
+            hideInTable: true,
+            onFilter: true,
+            valueEnum: PublicityValueEnum(),
+          },
+          {
+            title: intl.formatMessage({ id: "buttons.option" }),
+            key: "option",
+            width: 120,
+            valueType: "option",
+            render: (text, row, index, action) => {
+              return [
+                intl.formatMessage({
+                  id: "buttons.edit",
+                }),
+              ];
+            },
+          },
+          {
+            title: "类型",
+            dataIndex: "shareType",
+            valueType: "select",
+            hideInTable: true,
+            width: 120,
+            valueEnum: {
+              all: { text: "全部" },
+              my: { text: "我的" },
+              share: { text: "协作" },
+              public: { text: "全网公开" },
+            },
+          },
+          {
+            title: intl.formatMessage({ id: "auth.role.label" }),
+            dataIndex: "role",
+            valueType: "select",
+            width: 120,
+            valueEnum: {
+              all: { text: "全部" },
+              owner: { text: intl.formatMessage({ id: "auth.role.owner" }) },
+              manager: {
+                text: intl.formatMessage({ id: "auth.role.manager" }),
+              },
+              editor: { text: intl.formatMessage({ id: "auth.role.editor" }) },
+              member: { text: intl.formatMessage({ id: "auth.role.member" }) },
+            },
+          },
+        ]}
+        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>
+              {intl.formatMessage({ id: "buttons.selected" })}
+              {selectedRowKeys.length}
+              <Button
+                type="link"
+                style={{ marginInlineStart: 8 }}
+                onClick={onCleanSelected}
+              >
+                {intl.formatMessage({ id: "buttons.unselect" })}
+              </Button>
+            </span>
+          </Space>
+        )}
+        tableAlertOptionRender={() => {
+          return (
+            <Space size={16}>
+              <Button type="link">
+                {intl.formatMessage({
+                  id: "buttons.select",
+                })}
+              </Button>
+            </Space>
+          );
+        }}
+        request={async (params = {}, sorter, filter) => {
+          // TODO
+          console.log(params, sorter, filter);
+          let url: string = "";
+          switch (type) {
+            case "chapter":
+              const [book, para] = articleId.split("-");
+              url = `/v2/channel?view=user-in-chapter&book=${book}&para=${para}&progress=sent`;
+              break;
+          }
+          const res: IApiResponseChannelList = await get(url);
+          console.log("data", res.data.rows);
+          const items: IItem[] = res.data.rows.map((item, id) => {
+            console.log("final", item.final);
+            const date = new Date(item.created_at);
+            return {
+              id: id,
+              uid: item.uid,
+              title: item.name,
+              summary: item.summary,
+              studio: {
+                id: item.studio.id,
+                nickName: item.studio.nickName,
+                studioName: item.studio.studioName,
+                avatar: item.studio.avatar,
+              },
+              shareType: "my",
+              role: item.role,
+              type: item.type,
+              publicity: item.status,
+              createdAt: date.getTime(),
+              final: item.final,
+            };
+          });
+
+          return {
+            total: res.data.count,
+            succcess: true,
+            data: items,
+          };
+        }}
+        rowKey="id"
+        bordered
+        pagination={{
+          showQuickJumper: true,
+          showSizeChanger: false,
+          pageSize: 5,
+        }}
+        options={false}
+        search={{
+          filterType: "light",
+        }}
+        toolBarRender={() => [<></>]}
+      />
+    </>
+  );
+};
+
+export default Widget;

+ 104 - 0
dashboard/src/components/channel/ProgressSvg.tsx

@@ -0,0 +1,104 @@
+import { IFinal } from "../api/Channel";
+
+interface IWidget {
+  data?: IFinal[];
+  width?: number;
+}
+const Widget = ({ data, width = 300 }: IWidget) => {
+  //绘制句子进度
+  if (typeof data === "undefined" || data.length === 0) {
+    return <></>;
+  }
+  //进度
+  let svg_width = 0;
+  if (data) {
+    for (const iterator of data) {
+      svg_width += iterator[0];
+    }
+  }
+
+  const svg_height = svg_width / 10;
+
+  let curr_x = 0;
+  let finished = 0;
+
+  const innerBar = data?.map((item, id) => {
+    const stroke_width = item[0];
+    curr_x += stroke_width;
+    finished += item[1] ? stroke_width : 0;
+    return (
+      <rect
+        x={curr_x - stroke_width}
+        y={0}
+        height={svg_height}
+        width={stroke_width}
+        fill={item[1] ? "url(#grad1)" : "url(#grad2)"}
+      />
+    );
+  });
+  const finishedBar = (
+    <rect
+      x={0}
+      y={svg_height / 2 - svg_height / 20}
+      width={finished}
+      height={svg_height / 10}
+      style={{ strokeWidth: 0, fill: "rgb(100, 100, 228)" }}
+    />
+  );
+  const progress = (
+    <svg viewBox={`0 0 ${svg_width} ${svg_height} `} width={"100%"}>
+      <defs>
+        <linearGradient id="grad1" x1="0%" y1="0%" x2="0%" y2="100%">
+          <stop
+            offset="0%"
+            style={{ stopColor: "rgb(0,180,0)", stopOpacity: 1 }}
+          />
+          <stop
+            offset="50%"
+            style={{ stopColor: "rgb(255,255,255)", stopOpacity: 0.5 }}
+          />
+          <stop
+            offset="100%"
+            style={{ stopColor: "rgb(0,180,0)", stopOpacity: 1 }}
+          />
+        </linearGradient>
+        <linearGradient id="grad2" x1="0%" y1="0%" x2="0%" y2="100%">
+          <stop
+            offset="0%"
+            style={{ stopColor: "rgb(180,180,180)", stopOpacity: 1 }}
+          />
+          <stop
+            offset="50%"
+            style={{ stopColor: "rgb(255,255,255)", stopOpacity: 0.5 }}
+          />
+          <stop
+            offset="100%"
+            style={{ stopColor: "rgb(180,180,180)", stopOpacity: 1 }}
+          />
+        </linearGradient>
+      </defs>
+      {innerBar}
+      {finishedBar}
+    </svg>
+  );
+  /*
+			output +=
+				"<rect  x='0' y='0'  width='" + svg_width + "' height='" + svg_height / 5 + "' class='progress_bar_bg' />";
+			output +=
+				"<rect  x='0' y='0'  width='" +
+				allFinal +
+				"' height='" +
+				svg_height / 5 +
+				"' class='progress_bar_percent' style='stroke-width: 0; fill: rgb(100, 228, 100);'/>";
+			output += '<text x="0" y="' + svg_height + '" font-size="' + svg_height * 0.8 + '">';
+			output += channalinfo["count"] + "/" + channalinfo["all"] + "@" + curr_x;
+			output += "</text>";
+			output += "<svg>";
+			output += "</div>";
+*/
+  //进度结束
+
+  return <div style={{ width: width }}>{progress}</div>;
+};
+
+export default Widget;

+ 384 - 0
dashboard/src/components/code/my.ts

@@ -0,0 +1,384 @@
+const char_roman_to_myn = [
+  { id: "ggho", value: "ဂ္ဃေါ" },
+  { id: "gghā", value: "ဂ္ဃါ" },
+
+  { id: "ddho", value: "ဒ္ဓေါ" },
+  { id: "ddhā", value: "ဒ္ဓါ" },
+
+  { id: "ndho", value: "န္ဓော" }, //
+  { id: "ndo", value: "န္ဒော" }, //
+  { id: "ndā", value: "န္ဒာ" }, //
+  { id: "ndhā", value: "န္ဓာ" }, //
+
+  { id: "kho", value: "ခေါ" }, //
+  { id: "khā", value: "ခါ" }, //
+  { id: "kkho", value: "က္ခော" }, //
+  { id: "kkhā", value: "က္ခာ" }, //
+  { id: "go", value: "ဂေါ" }, //
+  { id: "ṅo", value: "ငေါ" }, //
+  { id: "dho", value: "ဓေါ" }, //
+  { id: "do", value: "ဒေါ" }, //
+  { id: "po", value: "ပေါ" }, //
+  { id: "vo", value: "ဝေါ" }, //
+  { id: "gā", value: "ဂါ" }, //
+  { id: "ṅā", value: "ငါ" }, //
+  { id: "dā", value: "ဒါ" }, //
+  { id: "dhā", value: "ဓါ" }, //
+  { id: "pā", value: "ပါ" }, //
+  { id: "dvā", value: "ဒွါ" }, //
+  { id: "tvā", value: "တွာ" }, //
+  { id: "vā", value: "ဝါ" }, //
+
+  //{ id: "ppho", value: "ပ္ဖေါ" },
+  //{ id: "pphā", value: "ပ္ဖါ" },
+
+  { id: "ss", value: "ဿ္" },
+
+  { id: "vh", value: "ဝှ္" },
+  { id: "vy", value: "ဝျ္" },
+  { id: "vr", value: "ဝြ္" },
+
+  { id: "yh", value: "ယှ္" },
+  { id: "yy", value: "ယျ္" },
+  { id: "yr", value: "ယြ္" },
+  { id: "yv", value: "ယွ္" },
+
+  { id: "hy", value: "ဟျ္" },
+  { id: "hr", value: "ဟြ္" },
+  { id: "hv", value: "ဟွ္" },
+
+  { id: "rv", value: "ရွ္" },
+  { id: "rh", value: "ရှ္" },
+  { id: "ry", value: "ရျ္" },
+
+  { id: "kh", value: "ခ္" },
+  { id: "gh", value: "ဃ္" },
+  { id: "ch", value: "ဆ္" },
+  { id: "jh", value: "ဈ္" },
+  { id: "ññ", value: "ည္" },
+  { id: "ṭh", value: "ဌ္" },
+  { id: "ḍh", value: "ဎ္" },
+  { id: "th", value: "ထ္" },
+  { id: "dh", value: "ဓ္" },
+  { id: "ph", value: "ဖ္" },
+  { id: "bh", value: "ဘ္" },
+  { id: "k", value: "က္" },
+  { id: "g", value: "ဂ္" },
+  { id: "c", value: "စ္" },
+  { id: "j", value: "ဇ္" },
+  { id: "ñ", value: "ဉ္" },
+  { id: "ḷ", value: "ဠ္" },
+  { id: "ṭ", value: "ဋ္" },
+  { id: "ḍ", value: "ဍ္" },
+  { id: "ṇ", value: "ဏ္" },
+  { id: "t", value: "တ္" },
+  { id: "d", value: "ဒ္" },
+  { id: "n", value: "န္" },
+  { id: "p", value: "ပ္" },
+  { id: "b", value: "ဗ္" },
+  { id: "m", value: "မ္" },
+  { id: "l", value: "လ္" },
+  { id: "s", value: "သ္" },
+  { id: "ṅ", value: "င်္" },
+
+  { id: "္h", value: "ှ္" },
+  { id: "h", value: "ဟ္" },
+  { id: "္y", value: "ျ္" },
+  { id: "y", value: "ယ္" },
+  { id: "္r", value: "ြ္" },
+  { id: "r", value: "ရ္" },
+  { id: "္v", value: "ွ္" },
+  { id: "v", value: "ဝ္" },
+  { id: "္aṃ", value: "ံ" },
+  { id: "္iṃ", value: "ိံ" },
+  { id: "္uṃ", value: "ုံ" },
+  { id: "္ā", value: "ာ" },
+  { id: "္i", value: "ိ" },
+  { id: "္ī", value: "ီ" },
+  { id: "္u", value: "ု" },
+  { id: "္ū", value: "ူ" },
+  { id: "္e", value: "ေ" },
+  { id: "္o", value: "ော" },
+  { id: "aṃ", value: "အံ" },
+  { id: "iṃ", value: "ဣံ" },
+  { id: "uṃ", value: "ဥံ" },
+  { id: "a", value: "အ" },
+  { id: "ā", value: "အာ" },
+  { id: "i", value: "ဣ" },
+  { id: "ī", value: "ဤ" },
+  { id: "u", value: "ဥ" },
+  { id: "ū", value: "ဦ" },
+  { id: "e", value: "ဧ" },
+  { id: "o", value: "ဩ" },
+  { id: "်္အ", value: "" },
+  { id: "္အ", value: "" },
+  { id: "1", value: "၁" }, //新增数字
+  { id: "2", value: "၂" },
+  { id: "3", value: "၃" },
+  { id: "4", value: "၄" },
+  { id: "5", value: "၅" },
+  { id: "6", value: "၆" },
+  { id: "7", value: "၇" },
+  { id: "8", value: "၈" },
+  { id: "9", value: "၉" },
+  { id: "0", value: "၀" },
+  { id: "ခော", value: "ခေါ" }, //矫正缅文转码错误
+  { id: "ခာ", value: "ခါ" }, //kh
+  { id: "က္ခေါ", value: "က္ခော" }, //kkho
+  { id: "က္ခါ", value: "က္ခာ" }, //kkhā
+  { id: "ဂော", value: "ဂေါ" }, //go
+  { id: "ငော", value: "ငေါ" }, //ṅo
+  { id: "ဓော", value: "ဓေါ" }, //dho
+  { id: "ဒော", value: "ဒေါ" }, //do
+  { id: "ပော", value: "ပေါ" }, //po
+  { id: "ဝော", value: "ဝေါ" }, //vo
+  { id: "ဂာ", value: "ဂါ" }, //gā
+  { id: "ငာ", value: "ငါ" }, //ṅā
+  { id: "ဒာ", value: "ဒါ" }, //dā
+  { id: "ဓာ", value: "ဓါ" }, //dhā
+  { id: "ပာ", value: "ပါ" }, //pā
+  { id: "ဝာ", value: "ဝါ" }, //vā
+  { id: "ဒွာ", value: "ဒွါ" }, //dvā
+];
+
+const char_myn_to_roman_1 = [
+  { id: "ႁႏၵ", value: "ndra" }, //後加
+  { id: "ခ္", value: "kh" },
+  { id: "ဃ္", value: "gh" },
+  { id: "ဆ္", value: "ch" },
+  { id: "ဈ္", value: "jh" },
+  { id: "ည္", value: "ññ" },
+  { id: "ဌ္", value: "ṭh" },
+  { id: "ဎ္", value: "ḍh" },
+  { id: "ထ္", value: "th" },
+  { id: "ဓ္", value: "dh" },
+  { id: "ဖ္", value: "ph" },
+  { id: "ဘ္", value: "bh" },
+  { id: "က္", value: "k" },
+  { id: "ဂ္", value: "g" },
+  { id: "စ္", value: "c" },
+  { id: "ဇ္", value: "j" },
+  { id: "ဉ္", value: "ñ" },
+  { id: "ဠ္", value: "ḷ" },
+  { id: "ဋ္", value: "ṭ" },
+  { id: "ဍ္", value: "ḍ" },
+  { id: "ဏ္", value: "ṇ" },
+  { id: "တ္", value: "t" },
+  { id: "ဒ္", value: "d" },
+  { id: "န္", value: "n" },
+  { id: "ဟ္", value: "h" },
+  { id: "ပ္", value: "p" },
+  { id: "ဗ္", value: "b" },
+  { id: "မ္", value: "m" },
+  { id: "ယ္", value: "y" },
+  { id: "ရ္", value: "r" },
+  { id: "လ္", value: "l" },
+  { id: "ဝ္", value: "v" },
+  { id: "သ္", value: "s" },
+  { id: "င္", value: "ṅ" },
+  { id: "င်္", value: "ṅ" },
+  { id: "ဿ", value: "ssa" },
+  { id: "ခ", value: "kha" },
+  { id: "ဃ", value: "gha" },
+  { id: "ဆ", value: "cha" },
+  { id: "ဈ", value: "jha" },
+  { id: "စျ", value: "jha" },
+  { id: "ည", value: "ñña" },
+  { id: "ဌ", value: "ṭha" },
+  { id: "ဎ", value: "ḍha" },
+  { id: "ထ", value: "tha" },
+  { id: "ဓ", value: "dha" },
+  { id: "ဖ", value: "pha" },
+  { id: "ဘ", value: "bha" },
+  { id: "က", value: "ka" },
+  { id: "ဂ", value: "ga" },
+  { id: "စ", value: "ca" },
+  { id: "ဇ", value: "ja" },
+  { id: "ဉ", value: "ña" },
+  { id: "ဠ", value: "ḷa" },
+  { id: "ဋ", value: "ṭa" },
+  { id: "ဍ", value: "ḍa" },
+  { id: "ဏ", value: "ṇa" },
+  { id: "တ", value: "ta" },
+  { id: "ဒ", value: "da" },
+  { id: "န", value: "na" },
+  { id: "ဟ", value: "ha" },
+  { id: "ပ", value: "pa" },
+  { id: "ဗ", value: "ba" },
+  { id: "မ", value: "ma" },
+  { id: "ယ", value: "ya" },
+  { id: "ရ", value: "ra" },
+  { id: "႐", value: "ra" }, //后加
+  { id: "လ", value: "la" },
+  { id: "ဝ", value: "va" },
+  { id: "သ", value: "sa" },
+  { id: "aျ္", value: "ya" },
+  { id: "aွ္", value: "va" },
+  { id: "aြ္", value: "ra" },
+  { id: "aြ", value: "ra" },
+
+  { id: "ၱ", value: "္ta" }, //后加
+  { id: "ၳ", value: "္tha" }, //后加
+  { id: "ၵ", value: "္da" }, //后加
+  { id: "ၶ", value: "္dha" }, //后加
+
+  { id: "ၬ", value: "္ṭa" }, //后加
+  { id: "ၭ", value: "္ṭha" }, //后加
+
+  { id: "ၠ", value: "္ka" }, //后加
+  { id: "ၡ", value: "္kha" }, //后加
+  { id: "ၢ", value: "္ga" }, //后加
+  { id: "ၣ", value: "္gha" }, //后加
+
+  { id: "ၸ", value: "္pa" }, //后加
+  { id: "ၹ", value: "္pha" }, //后加
+  { id: "ၺ", value: "္ba" }, //后加
+  { id: "႓", value: "္bha" }, //后加
+
+  { id: "ၥ", value: "္ca" }, //后加
+  { id: "ၧ", value: "္cha" }, //后加
+  { id: "ၨ", value: "္ja" }, //后加
+  { id: "ၩ", value: "္jha" }, //后加
+
+  { id: "်", value: "္a" }, //后加
+  { id: "ျ", value: "္ya" }, //后加
+  { id: "ႅ", value: "္la" }, //后加
+  { id: "ၼ", value: "္ma" }, //后加
+  { id: "ွ", value: "္va" }, //后加
+  { id: "ႇ", value: "္ha" }, //后加
+  { id: "ႆ", value: "ssa" }, //后加
+  { id: "ၷ", value: "na" }, //后加
+  { id: "ၲ", value: "ta" }, //后加
+
+  { id: "႒", value: "ṭṭha" }, //后加
+  { id: "႗", value: "ṭṭa" }, //后加
+  { id: "ၯ", value: "ḍḍha" }, //后加
+  { id: "ၮ", value: "ḍḍa" }, //后加
+  { id: "႑", value: "ṇḍa" }, //后加
+
+  { id: "kaၤ", value: "ṅka" }, //后加
+  { id: "gaၤ", value: "ṅga" }, //后加
+  { id: "khaၤ", value: "ṅkha" }, //后加
+  { id: "ghaၤ", value: "ṅgha" }, //后加
+
+  { id: "aှ", value: "ha" },
+  { id: "aိံ", value: "iṃ" },
+  { id: "aုံ", value: "uṃ" },
+  { id: "aော", value: "o" },
+  { id: "aေါ", value: "o" },
+  { id: "aအံ", value: "aṃ" },
+  { id: "aဣံ", value: "iṃ" },
+  { id: "aဥံ", value: "uṃ" },
+  { id: "aံ", value: "aṃ" },
+  { id: "aာ", value: "ā" },
+  { id: "aါ", value: "ā" },
+  { id: "aိ", value: "i" },
+  { id: "aီ", value: "ī" },
+  { id: "aု", value: "u" },
+  { id: "aဳ", value: "u" }, //後加
+  { id: "aူ", value: "ū" },
+  { id: "aေ", value: "e" },
+  { id: "အါ", value: "ā" },
+  { id: "အာ", value: "ā" },
+  { id: "အ", value: "a" },
+  { id: "ဣ", value: "i" },
+  { id: "ဤ", value: "ī" },
+  { id: "ဥ", value: "u" },
+  { id: "ဦ", value: "ū" },
+  { id: "ဧ", value: "e" },
+  { id: "ဩ", value: "o" },
+  { id: "ႏ", value: "n" }, //後加
+  { id: "ၪ", value: "ñ" }, //後加
+  { id: "a္", value: "" }, //後加
+  { id: "္", value: "" }, //後加
+  { id: "aံ", value: "aṃ" },
+
+  { id: "ေss", value: "sse" }, //后加
+  { id: "ေkh", value: "khe" }, //后加
+  { id: "ေgh", value: "ghe" }, //后加
+  { id: "ေch", value: "che" }, //后加
+  { id: "ေjh", value: "jhe" }, //后加
+  { id: "ေññ", value: "ññe" }, //后加
+  { id: "ေṭh", value: "ṭhe" }, //后加
+  { id: "ေḍh", value: "ḍhe" }, //后加
+  { id: "ေth", value: "the" }, //后加
+  { id: "ေdh", value: "dhe" }, //后加
+  { id: "ေph", value: "phe" }, //后加
+  { id: "ေbh", value: "bhe" }, //后加
+  { id: "ေk", value: "ke" }, //后加
+  { id: "ေg", value: "ge" }, //后加
+  { id: "ေc", value: "ce" }, //后加
+  { id: "ေj", value: "je" }, //后加
+  { id: "ေñ", value: "ñe" }, //后加
+  { id: "ေḷ", value: "ḷe" }, //后加
+  { id: "ေṭ", value: "ṭe" }, //后加
+  { id: "ေḍ", value: "ḍe" }, //后加
+  { id: "ေṇ", value: "ṇe" }, //后加
+  { id: "ေt", value: "te" }, //后加
+  { id: "ေd", value: "de" }, //后加
+  { id: "ေn", value: "ne" }, //后加
+  { id: "ေh", value: "he" }, //后加
+  { id: "ေp", value: "pe" }, //后加
+  { id: "ေb", value: "be" }, //后加
+  { id: "ေm", value: "me" }, //后加
+  { id: "ေy", value: "ye" }, //后加
+  { id: "ေr", value: "re" }, //后加
+  { id: "ေl", value: "le" }, //后加
+  { id: "ေv", value: "ve" }, //后加
+  { id: "ေs", value: "se" }, //后加
+  { id: "ေy", value: "ye" }, //后加
+  { id: "ေv", value: "ve" }, //后加
+  { id: "ေr", value: "re" }, //后加
+
+  { id: "ea", value: "e" }, //后加
+  { id: "eā", value: "o" }, //后加
+
+  { id: "၁", value: "1" },
+  { id: "၂", value: "2" },
+  { id: "၃", value: "3" },
+  { id: "၄", value: "4" },
+  { id: "၅", value: "5" },
+  { id: "၆", value: "6" },
+  { id: "၇", value: "7" },
+  { id: "၈", value: "8" },
+  { id: "၉", value: "9" },
+  { id: "၀", value: "0" },
+  { id: "း", value: "”" },
+  { id: "့", value: "’" },
+  { id: "။", value: "." },
+  { id: "၊", value: "," },
+];
+
+export const roman_to_my = (input: string | null): string | null => {
+  if (input === null) {
+    return input;
+  }
+  let txt = input.toLowerCase();
+
+  try {
+    for (const iterator of char_roman_to_myn) {
+      txt = txt.replaceAll(iterator.id, iterator.value);
+    }
+  } catch (err) {
+    //error
+    console.error(err);
+  }
+  return txt;
+};
+
+export const my_to_roman = (input: string | null): string | null => {
+  if (input === null) {
+    return input;
+  }
+  let txt = input.toLowerCase();
+
+  try {
+    for (const iterator of char_myn_to_roman_1) {
+      txt = txt.replaceAll(iterator.id, iterator.value);
+    }
+  } catch (err) {
+    //error
+    console.error(err);
+  }
+  return txt;
+};

+ 337 - 0
dashboard/src/components/code/si.ts

@@ -0,0 +1,337 @@
+const char_unicode_to_si = [
+  { id: "bbhr", value: "බ්භ්ර්" },
+  { id: "bbhv", value: "බ්භ්ව්" },
+  { id: "bbhy", value: "බ්භ්ය්" },
+  { id: "cchr", value: "ච්ඡ්ර්" },
+  { id: "cchv", value: "ච්ඡ්ව්" },
+  { id: "cchy", value: "ච්ඡ්ය්" },
+  { id: "ddhr", value: "ද්ධ්ර්" },
+  { id: "ddhv", value: "ද්ධ්ව්" },
+  { id: "ddhy", value: "ද්ධ්ය්" },
+  { id: "ḍḍhr", value: "ඩ්ඪ්ර්" },
+  { id: "ḍḍhv", value: "ඩ්ඪ්ව්" },
+  { id: "ḍḍhy", value: "ඩ්ඪ්ය්" },
+  { id: "gghr", value: "ග්ඝ්ර්" },
+  { id: "gghv", value: "ග්ඝ්ව්" },
+  { id: "gghy", value: "ග්ඝ්ය්" },
+  { id: "ṅkhr", value: "ඞ්ඛ්ර්" },
+  { id: "ṅkhv", value: "ඞ්ඛ්ව්" },
+  { id: "ṅkhy", value: "ඞ්ඛ්ය්" },
+  { id: "ṅghr", value: "ඞ්ඝ්ර්" },
+  { id: "ṅghv", value: "ඞ්ඝ්ව්" },
+  { id: "ṅghy", value: "ඞ්ඝ්ය්" },
+  { id: "jjhr", value: "ජ්ඣ්ර්" },
+  { id: "jjhv", value: "ජ්ඣ්ව්" },
+  { id: "jjhy", value: "ජ්ඣ්ය්" },
+  { id: "kkhr", value: "ක්ඛ්ර්" },
+  { id: "kkhv", value: "ක්ඛ්ව්" },
+  { id: "kkhy", value: "ක්ඛ්ය්" },
+  { id: "ñchr", value: "ඤ්ඡ්ර්" },
+  { id: "ñchv", value: "ඤ්ඡ්ව්" },
+  { id: "ñchy", value: "ඤ්ඡ්ය්" },
+  { id: "ñjhr", value: "ඤ්ඣ්ර්" },
+  { id: "ñjhv", value: "ඤ්ඣ්ව්" },
+  { id: "ñjhy", value: "ඤ්ඣ්ය්" },
+  { id: "ṇṭhr", value: "ණ්ඨ්ර්" },
+  { id: "ṇṭhv", value: "ණ්ඨ්ව්" },
+  { id: "ṇṭhy", value: "ණ්ඨ්ය්" },
+  { id: "ṇḍhr", value: "ණ්ඪ්ර්" },
+  { id: "ṇḍhv", value: "ණ්ඪ්ව්" },
+  { id: "ṇḍhy", value: "ණ්ඪ්ය්" },
+  { id: "nthr", value: "න්ථ්ර්" },
+  { id: "nthv", value: "න්ථ්ව්" },
+  { id: "nthy", value: "න්ථ්ය්" },
+  { id: "ndhr", value: "න්ධ්ර්" },
+  { id: "ndhv", value: "න්ධ්ව්" },
+  { id: "ndhy", value: "න්ධ්ය්" },
+  { id: "pphr", value: "ප්ඵ්ර්" },
+  { id: "pphv", value: "ප්ඵ්ව්" },
+  { id: "pphy", value: "ප්ඵ්ය්" },
+  { id: "mphr", value: "ම්ඵ්ර්" },
+  { id: "mphv", value: "ම්ඵ්ව්" },
+  { id: "mphy", value: "ම්ඵ්ය්" },
+  { id: "mbhr", value: "ම්භ්ර්" },
+  { id: "mbhv", value: "ම්භ්ව්" },
+  { id: "mbhy", value: "ම්භ්ය්" },
+  { id: "tthr", value: "ත්ථ්ර්" },
+  { id: "tthv", value: "ත්ථ්ව්" },
+  { id: "tthy", value: "ත්ථ්ය්" },
+  { id: "ṭṭhr", value: "ට්ඨ්ර්" },
+  { id: "ṭṭhv", value: "ට්ඨ්ව්" },
+  { id: "ṭṭhy", value: "ට්ඨ්ය්" },
+  { id: "bbr", value: "බ්බ්ර්" },
+  { id: "bbv", value: "බ්බ්ව්" },
+  { id: "bby", value: "බ්බ්ය්" },
+  { id: "ccr", value: "ච්ච්ර්" },
+  { id: "ccv", value: "ච්ච්ව්" },
+  { id: "ccy", value: "ච්ච්ය්" },
+  { id: "ddr", value: "ද්ද්ර්" },
+  { id: "ddv", value: "ද්ද්ව්" },
+  { id: "ddy", value: "ද්ද්ය්" },
+  { id: "ḍḍr", value: "ඩ්ඩ්ර්" },
+  { id: "ḍḍv", value: "ඩ්ඩ්ව්" },
+  { id: "ḍḍy", value: "ඩ්ඩ්ය්" },
+  { id: "ggr", value: "ග්ග්ර්" },
+  { id: "ggv", value: "ග්ග්ව්" },
+  { id: "ggy", value: "ග්ග්ය්" },
+  { id: "jjr", value: "ජ්ජ්ර්" },
+  { id: "jjv", value: "ජ්ජ්ව්" },
+  { id: "jjy", value: "ජ්ජ්ය්" },
+  { id: "ṅkr", value: "ඞ්ක්ර්" },
+  { id: "ṅkv", value: "ඞ්ක්ව්" },
+  { id: "ṅky", value: "ඞ්ක්ය්" },
+  { id: "ṅgr", value: "ඞ්ග්ර්" },
+  { id: "ṅgv", value: "ඞ්ග්ව්" },
+  { id: "ṅgy", value: "ඞ්ග්ය්" },
+  { id: "kkr", value: "ක්ක්ර්" },
+  { id: "kkv", value: "ක්ක්ව්" },
+  { id: "kky", value: "ක්ක්ය්" },
+  { id: "ñcr", value: "ඤ්ච්ර්" },
+  { id: "ñcv", value: "ඤ්ච්ව්" },
+  { id: "ñcy", value: "ඤ්ච්ය්" },
+  { id: "ñjr", value: "ඤ්ජ්ර්" },
+  { id: "ñjv", value: "ඤ්ජ්ව්" },
+  { id: "ñjy", value: "ඤ්ජ්ය්" },
+  { id: "mmr", value: "ම්ම්ර්" },
+  { id: "mmv", value: "ම්ම්ව්" },
+  { id: "mmy", value: "ම්ම්ය්" },
+  { id: "nnr", value: "න්න්ර්" },
+  { id: "nnv", value: "න්න්ව්" },
+  { id: "nny", value: "න්න්ය්" },
+  { id: "ṇṭr", value: "ණ්ට්ර්" },
+  { id: "ṇṭv", value: "ණ්ට්ව්" },
+  { id: "ṇṭy", value: "ණ්ට්ය්" },
+  { id: "ṇḍr", value: "ණ්ඩ්ර්" },
+  { id: "ṇḍv", value: "ණ්ඩ්ව්" },
+  { id: "ṇḍy", value: "ණ්ඩ්ය්" },
+  { id: "ññr", value: "ඤ්ඤ්ර්" },
+  { id: "ññv", value: "ඤ්ඤ්ව්" },
+  { id: "ññy", value: "ඤ්ඤ්ය්" },
+  { id: "ṇṇr", value: "ණ්ණ්ර්" },
+  { id: "ṇṇv", value: "ණ්ණ්ව්" },
+  { id: "ṇṇy", value: "ණ්ණ්ය්" },
+  { id: "ppr", value: "ප්ප්ර්" },
+  { id: "ppv", value: "ප්ප්ව්" },
+  { id: "ppy", value: "ප්ප්ය්" },
+  { id: "ntr", value: "න්ත්ර්" },
+  { id: "ntv", value: "න්ත්ව්" },
+  { id: "nty", value: "න්ත්ය්" },
+  { id: "ndr", value: "න්ද්ර්" },
+  { id: "ndv", value: "න්ද්ව්" },
+  { id: "ndy", value: "න්ද්ය්" },
+  { id: "ttr", value: "ත්ත්ර්" },
+  { id: "ttv", value: "ත්ත්ව්" },
+  { id: "tty", value: "ත්ත්ය්" },
+  { id: "mpr", value: "ම්ප්ර්" },
+  { id: "mpv", value: "ම්ප්ව්" },
+  { id: "mpy", value: "ම්ප්ය්" },
+  { id: "mbr", value: "ම්බ්ර්" },
+  { id: "mbv", value: "ම්බ්ව්" },
+  { id: "mby", value: "ම්බ්ය්" },
+  { id: "ṭṭr", value: "ට්ට්ර්" },
+  { id: "ṭṭv", value: "ට්ට්ව්" },
+  { id: "ṭṭy", value: "ට්ට්ය්" },
+  { id: "llr", value: "ල්ල්ර්" },
+  { id: "llv", value: "ල්ල්ව්" },
+  { id: "lly", value: "ල්ල්ය්" },
+  { id: "ssr", value: "ස්ස්ර්" },
+  { id: "ssv", value: "ස්ස්ව්" },
+  { id: "ssy", value: "ස්ස්ය්" },
+  { id: "yyr", value: "ය්ය්ර්" },
+  { id: "yyv", value: "ය්ය්ව්" },
+  { id: "yyy", value: "ය්ය්ය්" },
+  { id: "bbh", value: "බ්භ්" },
+  { id: "cch", value: "ච්ඡ්" },
+  { id: "ddh", value: "ද්ධ්" },
+  { id: "ḍḍh", value: "ඩ්ඪ්" },
+  { id: "ggh", value: "ග්ඝ්" },
+  { id: "jjh", value: "ජ්ඣ්" },
+  { id: "kkh", value: "ක්ඛ්" },
+  { id: "mbh", value: "ම්භ්" },
+  { id: "mph", value: "ම්ඵ්" },
+  { id: "ñch", value: "ඤ්ඡ්" },
+  { id: "bhr", value: "භ්ර්" },
+  { id: "bhv", value: "භ්ව්" },
+  { id: "bhy", value: "භ්ය්" },
+  { id: "chr", value: "ඡ්ර්" },
+  { id: "chv", value: "ඡ්ව්" },
+  { id: "chy", value: "ඡ්ය්" },
+  { id: "dhr", value: "ධ්ර්" },
+  { id: "dhv", value: "ධ්ව්" },
+  { id: "dhy", value: "ධ්ය්" },
+  { id: "ḍhr", value: "ඪ්ර්" },
+  { id: "ḍhv", value: "ඪ්ව්" },
+  { id: "ḍhy", value: "ඪ්ය්" },
+  { id: "ghr", value: "ඝ්ර්" },
+  { id: "ghv", value: "ඝ්ව්" },
+  { id: "ghy", value: "ඝ්ය්" },
+  { id: "jhr", value: "ඣ්ර්" },
+  { id: "jhv", value: "ඣ්ව්" },
+  { id: "jhy", value: "ඣ්ය්" },
+  { id: "khr", value: "ඛ්ර්" },
+  { id: "khv", value: "ඛ්ව්" },
+  { id: "khy", value: "ඛ්ය්" },
+  { id: "phr", value: "ඵ්ර්" },
+  { id: "phv", value: "ඵ්ව්" },
+  { id: "phy", value: "ඵ්ය්" },
+  { id: "thr", value: "ථ්ර්" },
+  { id: "thv", value: "ථ්ව්" },
+  { id: "thy", value: "ථ්ය්" },
+  { id: "ṭhr", value: "ඨ්ර්" },
+  { id: "ṭhv", value: "ඨ්ව්" },
+  { id: "ṭhy", value: "ඨ්ය්" },
+  { id: "ndh", value: "න්ධ්" },
+  { id: "ṇḍh", value: "ණ්ඪ්" },
+  { id: "ṅgh", value: "ඞ්ඝ්" },
+  { id: "ñjh", value: "ඤ්ඣ්" },
+  { id: "ṅkh", value: "ඞ්ඛ්" },
+  { id: "nth", value: "න්ථ්" },
+  { id: "ṇṭh", value: "ණ්ඨ්" },
+  { id: "pph", value: "ප්ඵ්" },
+  { id: "tth", value: "ත්ථ්" },
+  { id: "ṭṭh", value: "ට්ඨ්" },
+  { id: "bb", value: "බ්බ්" },
+  { id: "bh", value: "භ්" },
+  { id: "cc", value: "ච්ච්" },
+  { id: "ch", value: "ඡ්" },
+  { id: "dd", value: "ද්ද්" },
+  { id: "ḍḍ", value: "ඩ්ඩ්" },
+  { id: "dh", value: "ධ්" },
+  { id: "ḍh", value: "ඪ්" },
+  { id: "gg", value: "ග්ග්" },
+  { id: "gh", value: "ඝ්" },
+  { id: "jh", value: "ඣ්" },
+  { id: "jj", value: "ජ්ජ්" },
+  { id: "kh", value: "ඛ්" },
+  { id: "kk", value: "ක්ක්" },
+  { id: "ll", value: "ල්ල්" },
+  { id: "mb", value: "ම්බ්" },
+  { id: "mm", value: "ම්ම්" },
+  { id: "mp", value: "ම්ප්" },
+  { id: "ñc", value: "ඤ්ච්" },
+  { id: "nd", value: "න්ද්" },
+  { id: "ṇḍ", value: "ණ්ඩ්" },
+  { id: "ṅg", value: "ඞ්ග්" },
+  { id: "ñj", value: "ඤ්ජ්" },
+  { id: "ṅk", value: "ඞ්ක්" },
+  { id: "nn", value: "න්න්" },
+  { id: "ññ", value: "ඤ්ඤ්" },
+  { id: "ṇṇ", value: "ණ්ණ්" },
+  { id: "nt", value: "න්ත්" },
+  { id: "br", value: "බ්ර්" },
+  { id: "bv", value: "බ්ව්" },
+  { id: "by", value: "බ්ය්" },
+  { id: "cr", value: "ච්ර්" },
+  { id: "cv", value: "ච්ව්" },
+  { id: "cy", value: "ච්ය්" },
+  { id: "dr", value: "ද්ර්" },
+  { id: "dv", value: "ද්ව්" },
+  { id: "dy", value: "ද්ය්" },
+  { id: "ḍr", value: "ඩ්ර්" },
+  { id: "ḍv", value: "ඩ්ව්" },
+  { id: "ḍy", value: "ඩ්ය්" },
+  { id: "gr", value: "ග්ර්" },
+  { id: "gv", value: "ග්ව්" },
+  { id: "gy", value: "ග්ය්" },
+  { id: "jr", value: "ජ්ර්" },
+  { id: "jv", value: "ජ්ව්" },
+  { id: "jy", value: "ජ්ය්" },
+  { id: "kr", value: "ක්ර්" },
+  { id: "kv", value: "ක්ව්" },
+  { id: "ky", value: "ක්ය්" },
+  { id: "pr", value: "ප්ර්" },
+  { id: "pv", value: "ප්ව්" },
+  { id: "py", value: "ප්ය්" },
+  { id: "tr", value: "ත්ර්" },
+  { id: "tv", value: "ත්ව්" },
+  { id: "ty", value: "ත්ය්" },
+  { id: "ṭr", value: "ට්ර්" },
+  { id: "ṭv", value: "ට්ව්" },
+  { id: "ṭy", value: "ට්ය්" },
+  { id: "ñh", value: "ඤ්හ්" },
+  { id: "ṇh", value: "ණ්හ්" },
+  { id: "nh", value: "න්හ්" },
+  { id: "mh", value: "ම්හ්" },
+  { id: "yh", value: "ය්හ්" },
+  { id: "ly", value: "ල්ය්" },
+  { id: "lh", value: "ල්හ්" },
+  { id: "vh", value: "ව්හ්" },
+  { id: "sm", value: "ස්ම්" },
+  { id: "sv", value: "ස්ව්" },
+  { id: "hm", value: "හ්ම්" },
+  { id: "hv", value: "හ්ව්" },
+  { id: "ḷh", value: "ළ්හ්" },
+  { id: "ṇṭ", value: "ණ්ට්" },
+  { id: "ph", value: "ඵ්" },
+  { id: "pp", value: "ප්ප්" },
+  { id: "ss", value: "ස්ස්" },
+  { id: "th", value: "ථ්" },
+  { id: "ṭh", value: "ඨ්" },
+  { id: "tt", value: "ත්ත්" },
+  { id: "ṭṭ", value: "ට්ට්" },
+  { id: "yy", value: "ය්ය්" },
+  { id: "b", value: "බ්" },
+  { id: "c", value: "ච්" },
+  { id: "d", value: "ද්" },
+  { id: "ḍ", value: "ඩ්" },
+  { id: "g", value: "ග්" },
+  { id: "h", value: "හ්" },
+  { id: "j", value: "ජ්" },
+  { id: "k", value: "ක්" },
+  { id: "l", value: "ල්" },
+  { id: "ḷ", value: "ළ්" },
+  { id: "m", value: "ම්" },
+  { id: "n", value: "න්" },
+  { id: "ṅ", value: "ඞ්" },
+  { id: "ñ", value: "ඤ්" },
+  { id: "ṇ", value: "ණ්" },
+  { id: "p", value: "ප්" },
+  { id: "r", value: "‍ර්" },
+  { id: "s", value: "ස්" },
+  { id: "t", value: "ත්" },
+  { id: "ṭ", value: "ට්" },
+  { id: "v", value: "ව්" },
+  { id: "y", value: "‍ය්" },
+  { id: "්iṃ", value: "ිං" },
+  { id: "්uṃ", value: "ුං" },
+  { id: "්aṃ", value: "ං" },
+  { id: "්ā", value: "ා" },
+  { id: "්i", value: "ි" },
+  { id: "්ī", value: "ී" },
+  { id: "්u", value: "ු" },
+  { id: "්ū", value: "ූ" },
+  { id: "්e", value: "ෙ" },
+  { id: "්ē", value: "ේ" },
+  { id: "්o", value: "ො" },
+  { id: "්ō", value: "ෝ" },
+  { id: "්", value: "්" },
+  { id: "aṃ", value: "අං" },
+  { id: "iṃ", value: "ඉං" },
+  { id: "uṃ", value: "උං" },
+  { id: "්a", value: "" },
+  { id: "a", value: "අ" },
+  { id: "ā", value: "ආ" },
+  { id: "i", value: "ඉ" },
+  { id: "ī", value: "ඊ" },
+  { id: "u", value: "උ" },
+  { id: "ū", value: "ඌ" },
+  { id: "e", value: "එ" },
+  { id: "o", value: "ඔ" },
+];
+
+export const roman_to_si = (input: string | null): string | null => {
+  if (input === null) {
+    return input;
+  }
+  let txt = input.toLowerCase();
+
+  try {
+    for (const iterator of char_unicode_to_si) {
+      txt = txt.replaceAll(iterator.id, iterator.value);
+    }
+  } catch (err) {
+    //error
+    console.error(err);
+  }
+  return txt;
+};

+ 677 - 0
dashboard/src/components/code/tai-tham.ts

@@ -0,0 +1,677 @@
+const char_roman_to_tai = [
+  //{ id: "n’</w>ti<w>", value: "nti" },
+  { id: "ndr", value: "nrd" },
+  { id: "ntr", value: "nrt" },
+  { id: "bbho", value: "ᨻᩮ᩠ᨽᩣ" },
+  { id: "ccho", value: "ᨧᩮ᩠ᨨᩣ" },
+  { id: "ddho", value: "ᨴᩮ᩠ᨵᩣ" },
+  { id: "ḍḍho", value: "ᨯᩮ᩠ᨰᩣ" },
+  { id: "ggho", value: "ᨣᩮ᩠ᨥᩣ" },
+  { id: "jjho", value: "ᨩᩮ᩠ᨫᩣ" },
+  { id: "kkho", value: "ᨠᩮ᩠ᨡᩣ" },
+  { id: "mbho", value: "ᨾᩮ᩠ᨽᩣ" },
+  { id: "mpho", value: "ᨾᩮ᩠ᨹᩣ" },
+  { id: "ndho", value: "ᨶᩮᩣ᩠ᨵ" },
+  { id: "ntho", value: "ᨶᩮᩣ᩠ᨳ" },
+  { id: "ndhā", value: "ᨶᩣ᩠ᨵ" },
+  { id: "nthā", value: "ᨶᩣ᩠ᨳ" },
+  { id: "ṅgho", value: "ᩘᨥᩮᩣ" }, //  "ᩘᩮ᩠ᨿᩣ
+  { id: "ṅkho", value: "ᩘᨡᩮᩣ" }, // "ᩘᩮ᩠ᨡᩣ"
+  { id: "ñcho", value: "ᨬᩮ᩠ᨨᩣ" },
+  { id: "ñjho", value: "ᨬᩮ᩠ᨫᩣ" },
+  { id: "ṇḍho", value: "ᨱᩮ᩠ᨰᩣ" },
+  { id: "ṇṭho", value: "ᨱᩮ᩠ᨮᩣ" },
+  { id: "ppho", value: "ᨷᩮ᩠ᨹᩣ" },
+  { id: "ttho", value: "ᨲᩮ᩠ᨳᩣ" },
+  { id: "ṭṭho", value: "ᨭᩛᩮᩣ" },
+  { id: "bbhe", value: "ᨻᩮ᩠ᨽ" },
+  { id: "mbhe", value: "ᨾᩮ᩠ᨽ" },
+  { id: "cche", value: "ᨧᩮ᩠ᨨ" },
+  { id: "ñche", value: "ᨬᩮ᩠ᨨ" },
+  { id: "ddhe", value: "ᨴᩮ᩠ᨵ" },
+  { id: "ndhe", value: "ᨶᩮ᩠ᨵ" },
+  { id: "ḍḍhe", value: "ᨯᩮ᩠ᨰ" },
+  { id: "ṇḍhe", value: "ᨱᩮ᩠ᨰ" },
+  { id: "gghe", value: "ᨣᩮ᩠ᨥ" },
+  { id: "ṅghe", value: "ᩘᨥᩮ" }, //  "ᩘᩮ᩠ᨿ
+  { id: "ṅkhe", value: "ᩘᨡᩮ" }, // "ᩘᩮ᩠ᨡ
+  { id: "jjhe", value: "ᨩᩮ᩠ᨫ" },
+  { id: "ñjhe", value: "ᨬᩮ᩠ᨫ" },
+  { id: "kkhe", value: "ᨠᩮ᩠ᨡ" },
+  { id: "mphe", value: "ᨾᩮ᩠ᨹ" },
+  { id: "pphe", value: "ᨷᩮ᩠ᨹ" },
+  { id: "nthe", value: "ᨶᩮ᩠ᨳ" },
+  { id: "tthe", value: "ᨲᩮ᩠ᨳ" },
+  { id: "ṇṭhe", value: "ᨱᩮ᩠ᨮ" },
+  { id: "ṭṭhe", value: "ᨭᩛᩮ" },
+  { id: "bbo", value: "ᨻᩮ᩠ᨻᩣ" },
+  { id: "cco", value: "ᨧᩮ᩠ᨧᩣ" },
+  { id: "ddo", value: "ᨴᩮ᩠ᨴᩣ" },
+  { id: "dvo", value: "ᨴᩮ᩠ᩅᩣ" },
+  { id: "ḍḍo", value: "ᨯᩮ᩠ᨯᩣ" },
+  { id: "ggo", value: "ᨣᩮ᩠ᨣᩣ" },
+  { id: "hro", value: "ᩉᩮ᩠ᩕᩣ" },
+  { id: "hvo", value: "ᩉᩮ᩠ᩅᩣ" },
+  { id: "hyo", value: "ᩉᩮ᩠ᨿᩣ" },
+  { id: "jjo", value: "ᨩᩮ᩠ᨩᩣ" },
+  { id: "kko", value: "ᨠᩮ᩠ᨠᩣ" },
+  { id: "kro", value: "ᨠᩮ᩠ᩕᩣ" },
+  { id: "mbo", value: "ᨾᩮ᩠ᨻᩣ" },
+  { id: "llo", value: "ᩃᩮ᩠ᩃᩣ" },
+  { id: "mmo", value: "ᨾᩮᩜᩣ" },
+  { id: "mpo", value: "ᨾᩮ᩠ᨷᩣ" },
+  { id: "ndo", value: "ᨶᩮᩣ᩠ᨴ" },
+  { id: "nno", value: "ᨶᩮᩣ᩠ᨶ" },
+  { id: "nto", value: "ᨶᩮᩣ᩠ᨲ" },
+  { id: "ndā", value: "ᨶᩣ᩠ᨴ" },
+  { id: "nnā", value: "ᨶᩣ᩠ᨶ" },
+  { id: "ntā", value: "ᨶᩣ᩠ᨲ" },
+  { id: "ṅgo", value: "ᩘ ᨣᩮᩤ" }, //  ᩘᩮ᩠ᨣᩣ
+  { id: "ṅko", value: "ᩘᨠᩮᩣ" }, //  ᩘᩮ᩠ᨠᩣ
+  { id: "ñco", value: "ᨬᩮ᩠ᨧᩣ" },
+  { id: "ñjo", value: "ᨬᩮ᩠ᨩᩣ" },
+  { id: "ñño", value: "ᨬᩮ᩠ᨬᩣ" },
+  { id: "ṇḍo", value: "ᨱᩮ᩠ᨯᩣ" },
+  { id: "ṇṇo", value: "ᨱᩮ᩠ᨱᩣ" },
+  { id: "ṇṭo", value: "ᨱᩮ᩠ᨭᩣ" },
+  { id: "ppo", value: "ᨷᩮ᩠ᨷᩣ" },
+  { id: "rho", value: "ᩁᩮ᩠ᩉᩣ" },
+  { id: "rvo", value: "ᩁᩮ᩠ᩅᩣ" },
+  { id: "ryo", value: "ᩁᩮ᩠ᨿᩣ" },
+  { id: "tto", value: "ᨲᩮ᩠ᨲᩣ" },
+  { id: "tvo", value: "ᨲᩮ᩠ᩅᩣ" },
+  { id: "ṭṭo", value: "ᨭᩮ᩠ᨭᩣ" },
+  { id: "vho", value: "ᩅᩮ᩠ᩉᩣ" },
+  { id: "vro", value: "ᩅᩮ᩠ᩕᩣ" },
+  { id: "vyo", value: "ᩅᩮ᩠ᨿᩣ" },
+  { id: "yho", value: "ᨿᩮ᩠ᩉᩣ" },
+  { id: "yro", value: "ᨿᩮ᩠ᩕᩣ" },
+  { id: "yvo", value: "ᨿᩮ᩠ᩅᩣ" },
+  { id: "yyo", value: "ᨿᩮ᩠ᨿᩣ" },
+  { id: "bbe", value: "ᨻᩮ᩠ᨻ" },
+  { id: "mbe", value: "ᨾᩮ᩠ᨻ" },
+  { id: "cce", value: "ᨧᩮ᩠ᨧ" },
+  { id: "ñce", value: "ᨬᩮ᩠ᨧ" },
+  { id: "dde", value: "ᨴᩮ᩠ᨴ" },
+  { id: "nde", value: "ᨶᩮ᩠ᨴ" },
+  { id: "ḍḍe", value: "ᨯᩮ᩠ᨯ" },
+  { id: "ṇḍe", value: "ᨱᩮ᩠ᨯ" },
+  { id: "gge", value: "ᨣᩮ᩠ᨣ" },
+  { id: "ṅge", value: "ᩘᨣᩮ" }, // "ᩘᩮ᩠ᨣ
+  { id: "rhe", value: "ᩁᩮ᩠ᩉ" },
+  { id: "vhe", value: "ᩅᩮ᩠ᩉ" },
+  { id: "yhe", value: "ᨿᩮ᩠ᩉ" },
+  { id: "jje", value: "ᨩᩮ᩠ᨩ" },
+  { id: "ñje", value: "ᨬᩮ᩠ᨩ" },
+  { id: "kke", value: "ᨠᩮ᩠ᨠ" },
+  { id: "ṅke", value: "ᩘᨠᩮ" }, //  ᩘᩮ᩠ᨠ
+  { id: "mme", value: "ᨾᩮᩜ" },
+  { id: "lle", value: "ᩃᩮ᩠ᩃ" },
+  { id: "nne", value: "ᨶᩮ᩠ᨶ" },
+  { id: "ññe", value: "ᨬᩮ᩠ᨬ" },
+  { id: "ṇṇe", value: "ᨱᩮ᩠ᨱ" },
+  { id: "mpe", value: "ᨾᩮ᩠ᨷ" },
+  { id: "ppe", value: "ᨷᩮ᩠ᨷ" },
+  { id: "hre", value: "ᩉᩮ᩠ᩕ" },
+  { id: "kre", value: "ᨠᩮ᩠ᩕ" },
+  { id: "vre", value: "ᩅᩮ᩠ᩕ" },
+  { id: "yre", value: "ᨿᩮ᩠ᩕ" },
+  { id: "nte", value: "ᨶᩮ᩠ᨲ" },
+  { id: "tte", value: "ᨲᩮ᩠ᨲ" },
+  { id: "ṇṭe", value: "ᨱᩮ᩠ᨭ" },
+  { id: "ṭṭe", value: "ᨭᩮ᩠ᨭ" },
+  { id: "dve", value: "ᨴᩮ᩠ᩅ" },
+  { id: "hve", value: "ᩉᩮ᩠ᩅ" },
+  { id: "rve", value: "ᩁᩮ᩠ᩅ" },
+  { id: "tve", value: "ᨲᩮ᩠ᩅ" },
+  { id: "yve", value: "ᨿᩮ᩠ᩅ" },
+  { id: "hye", value: "ᩉᩮ᩠ᨿ" },
+  { id: "rye", value: "ᩁᩮ᩠ᨿ" },
+  { id: "vye", value: "ᩅᩮ᩠ᨿ" },
+  { id: "yye", value: "ᨿᩮ᩠ᨿ" },
+  //{ id: "mmā", value: "ᨾᩜᩣ" },
+  //{ id: "mma", value: "ᨾᩜ" },
+  { id: "by", value: "ᨻ᩠ᨿ᩠" },
+  { id: "ṭṭh", value: "ᨭᩛ᩠" },
+
+  { id: "ss", value: "ᩔ᩠" },
+  { id: "vh", value: "ᩅ᩠ᩉ᩠" },
+  { id: "vy", value: "ᩅ᩠ᨿ᩠" },
+  { id: "vr", value: "ᩅᩕ᩠" },
+  { id: "yh", value: "ᨿ᩠ᩉ᩠" },
+  { id: "yy", value: "ᨿ᩠ᨿ᩠" },
+  { id: "yr", value: "ᨿᩕ᩠" },
+  { id: "yv", value: "ᨿ᩠ᩅ᩠" },
+  { id: "hy", value: "ᩉ᩠ᨿ᩠" },
+  { id: "hr", value: "ᩉᩕ᩠" },
+  { id: "hv", value: "ᩉ᩠ᩅ᩠" },
+  { id: "rv", value: "ᩁ᩠ᩅ᩠" },
+  { id: "rh", value: "ᩁ᩠ᩉ᩠" },
+  { id: "ry", value: "ᩁ᩠ᨿ᩠" },
+  { id: "kh", value: "ᨡ᩠" },
+  { id: "gh", value: "ᨥ᩠" },
+  { id: "ch", value: "ᨨ᩠" },
+  { id: "jh", value: "ᨫ᩠" },
+  { id: "ññ", value: "ᨬ᩠ᨬ᩠" },
+  { id: "ṭh", value: "ᨮ᩠" },
+  { id: "ḍh", value: "ᨰ᩠" },
+  { id: "th", value: "ᨳ᩠" },
+  { id: "dh", value: "ᨵ᩠" },
+  { id: "ph", value: "ᨹ᩠" },
+  { id: "bh", value: "ᨽ᩠" },
+  { id: "k", value: "ᨠ᩠" },
+  { id: "g", value: "ᨣ᩠" },
+  { id: "c", value: "ᨧ᩠" },
+  { id: "j", value: "ᨩ᩠" },
+  { id: "ñ", value: "ᨬ᩠" },
+  { id: "ḷ", value: "ᩊ᩠" },
+  { id: "ṭ", value: "ᨭ᩠" },
+  { id: "ḍ", value: "ᨯ᩠" },
+  { id: "ṇ", value: "ᨱ᩠" },
+  { id: "t", value: "ᨲ᩠" },
+  { id: "d", value: "ᨴ᩠" },
+  { id: "n", value: "ᨶ᩠" },
+  { id: "p", value: "ᨷ᩠" },
+  { id: "b", value: "ᨻ᩠" },
+  { id: "m", value: "ᨾ᩠" },
+  { id: "l", value: "ᩃ᩠" },
+  { id: "s", value: "ᩈ᩠" },
+  { id: "ṅ", value: "ᩘ" },
+  { id: "᩠h", value: "᩠ᩉ᩠" },
+  { id: "h", value: "ᩉ᩠" },
+  { id: "᩠y", value: "᩠ᨿ" },
+  { id: "y", value: "ᨿ᩠" },
+  { id: "᩠r", value: "ᩕ᩠" },
+  { id: "r", value: "ᩁ᩠" },
+  { id: "᩠v", value: "᩠ᩅ᩠" },
+  { id: "v", value: "ᩅ᩠" },
+  { id: "᩠ᨾ", value: "ᩜ" },
+  { id: "᩠ai", value: "ᩱ" },
+  { id: "᩠aṃ", value: "ᩴ" },
+  { id: "᩠iṃ", value: "ᩥᩴ" },
+  { id: "᩠uṃ", value: "ᩩᩴ" },
+  { id: "᩠ā", value: "ᩣ" },
+  { id: "᩠i", value: "ᩥ" },
+  { id: "᩠ī", value: "ᩦ" },
+  { id: "᩠u", value: "ᩩ" },
+  { id: "᩠ū", value: "ᩪ" },
+  { id: "᩠e", value: "ᩮ" },
+  { id: "᩠o", value: "ᩮᩣ" },
+  { id: "aṃ", value: "ᩋᩴ" },
+  { id: "iṃ", value: "ᨠ᩠ᨠᩴ" },
+  { id: "uṃ", value: "ᩏᩴ" },
+  { id: "a", value: "ᩋ" },
+  { id: "ā", value: "ᩋᩣ" },
+  { id: "i", value: "ᩍ" },
+  { id: "ī", value: "ᩎ" },
+  { id: "u", value: "ᩏ" },
+  { id: "ū", value: "ᩐ" },
+  { id: "e", value: "ᩑ" },
+  { id: "o", value: "ᩒ" },
+  { id: "᩠᩼ᩋ", value: "" },
+  { id: "᩠ᩋ", value: "" },
+  //{ id: "ᨡᩮᩣ", value: "ᨡᩮᩤ" },
+  //{ id: "ᨡᩣ", value: "ᨡᩤ" },
+  { id: "ᨠ᩠ᨡᩮᩤ", value: "ᨠᩮ᩠ᨡᩣ" },
+  { id: "က᩠ခါ", value: "ᨠ᩠ᨡᩣ" },
+  { id: "ဂော", value: "ᨣᩮᩤ" },
+  //{ id: "ᨦᩮᩣ", value: "ᨦᩮᩤ" },
+  { id: "ᨴᩮᩣ", value: "ᨴᩮᩤ" },
+  { id: "ᨷᩮᩣ", value: "ᨷᩮᩤ" },
+  { id: "ᩅᩮᩣ", value: "ᩅᩮᩤ" },
+  { id: "ᨣᩣ", value: "ᨣᩤ" },
+  //{ id: "ᨦᩣ", value: "ᨦᩤ" },
+  { id: "ᨴᩣ", value: "ᨴᩤ" },
+  { id: "ᨵᩣ", value: "ᨵᩤ" },
+  { id: "ᨷᩣ", value: "ᨷᩤ" },
+  { id: "ᩅᩣ", value: "ᩅᩤ" },
+  { id: "ᨴ᩠ᩅᩣ", value: "ᨴ᩠ᩅᩤ" },
+  { id: "ᩘ ", value: "ᩘ" },
+  { id: "ᨷ᩠ᨷᩤ", value: "ᨷ᩠ᨷᩣ" },
+  { id: "ᨲ᩠ᩅᩤ", value: "ᨲ᩠ᩅᩣ" },
+  { id: "ᩈ᩠ᩅᩤ", value: "ᩈ᩠ᩅᩣ" },
+  { id: "ᩮ᩠ᨷᩤ", value: "ᩮ᩠ᨷᩣ" },
+];
+
+/*
+const char_tai_to_roman = [
+  { id: "ᨻᩮ᩠ᨽᩣ", value: "bbho" },
+  { id: "ᨧᩮ᩠ᨨᩣ", value: "ccho" },
+  { id: "ᨴᩮ᩠ᨵᩣ", value: "ddho" },
+  { id: "ᨯᩮ᩠ᨰᩣ", value: "ḍḍho" },
+  { id: "ᨣᩮ᩠ᨥᩣ", value: "ggho" },
+  { id: "ᨩᩮ᩠ᨫᩣ", value: "jjho" },
+  { id: "ᨠᩮ᩠ᨡᩣ", value: "kkho" },
+  { id: "ᨾᩮ᩠ᨽᩣ", value: "mbho" },
+  { id: "ᨾᩮ᩠ᨹᩣ", value: "mpho" },
+  { id: "ᨶᩮᩣ᩠ᨵ", value: "ndho" },
+  { id: "ᨶᩮᩣ᩠ᨳ", value: "ntho" },
+  { id: "ᨶᩣ᩠ᨵ", value: "ndhā" },
+  { id: "ᨶᩣ᩠ᨳ", value: "nthā" },
+  { id: "ᩘᨥᩮᩣ", value: "ṅgho" },
+  { id: "ᩘᨡᩮᩣ", value: "ṅkho" },
+  { id: "ᨬᩮ᩠ᨨᩣ", value: "ñcho" },
+  { id: "ᨬᩮ᩠ᨫᩣ", value: "ñjho" },
+  { id: "ᨱᩮ᩠ᨰᩣ", value: "ṇḍho" },
+  { id: "ᨱᩮ᩠ᨮᩣ", value: "ṇṭho" },
+  { id: "ᨷᩮ᩠ᨹᩣ", value: "ppho" },
+  { id: "ᨲᩮ᩠ᨳᩣ", value: "ttho" },
+  { id: "ᨭᩛᩮᩣ", value: "ṭṭho" },
+  { id: "ᨻᩮ᩠ᨽ", value: "bbhe" },
+  { id: "ᨾᩮ᩠ᨽ", value: "mbhe" },
+  { id: "ᨧᩮ᩠ᨨ", value: "cche" },
+  { id: "ᨬᩮ᩠ᨨ", value: "ñche" },
+  { id: "ᨴᩮ᩠ᨵ", value: "ddhe" },
+  { id: "ᨶᩮ᩠ᨵ", value: "ndhe" },
+  { id: "ᨯᩮ᩠ᨰ", value: "ḍḍhe" },
+  { id: "ᨱᩮ᩠ᨰ", value: "ṇḍhe" },
+  { id: "ᨣᩮ᩠ᨥ", value: "gghe" },
+  { id: "ᩘᨥᩮ", value: "ṅghe" },
+  { id: "ᩘᨡᩮ", value: "ṅkhe" },
+  { id: "ᨩᩮ᩠ᨫ", value: "jjhe" },
+  { id: "ᨬᩮ᩠ᨫ", value: "ñjhe" },
+  { id: "ᨠᩮ᩠ᨡ", value: "kkhe" },
+  { id: "ᨾᩮ᩠ᨹ", value: "mphe" },
+  { id: "ᨷᩮ᩠ᨹ", value: "pphe" },
+  { id: "ᨶᩮ᩠ᨳ", value: "nthe" },
+  { id: "ᨲᩮ᩠ᨳ", value: "tthe" },
+  { id: "ᨱᩮ᩠ᨮ", value: "ṇṭhe" },
+  { id: "ᨭᩛᩮ", value: "ṭṭhe" },
+  { id: "ᨻᩮ᩠ᨻᩣ", value: "bbo" },
+  { id: "ᨧᩮ᩠ᨧᩣ", value: "cco" },
+  { id: "ᨴᩮ᩠ᨴᩣ", value: "ddo" },
+  { id: "ᨴᩮ᩠ᩅᩣ", value: "dvo" },
+  { id: "ᨯᩮ᩠ᨯᩣ", value: "ḍḍo" },
+  { id: "ᨣᩮ᩠ᨣᩣ", value: "ggo" },
+  { id: "ᩉᩮ᩠ᩕᩣ", value: "hro" },
+  { id: "ᩉᩮ᩠ᩅᩣ", value: "hvo" },
+  { id: "ᩉᩮ᩠ᨿᩣ", value: "hyo" },
+  { id: "ᨩᩮ᩠ᨩᩣ", value: "jjo" },
+  { id: "ᨠᩮ᩠ᨠᩣ", value: "kko" },
+  { id: "ᨠᩮ᩠ᩕᩣ", value: "kro" },
+  { id: "ᨾᩮ᩠ᨻᩣ", value: "mbo" },
+  { id: "ᨾᩮᩜᩣ", value: "mmo" },
+  { id: "ᨾᩮ᩠ᨾᩣ", value: "mmo" },
+  { id: "ᨾᩮ᩠ᨷᩣ", value: "mpo" },
+  { id: "ᨶᩮᩣ᩠ᨴ", value: "ndo" },
+  { id: "ᨶᩮᩣ᩠ᨶ", value: "nno" },
+  { id: "ᨶᩮᩣ᩠ᨲ", value: "nto" },
+  { id: "ᨶᩣ᩠ᨴ", value: "ndā" },
+  { id: "ᨶᩣ᩠ᨶ", value: "nnā" },
+  { id: "ᨶᩣ᩠ᨲ", value: "ntā" },
+  { id: "ᩘ ᨣᩮᩤ", value: "ṅgo" },
+  { id: "ᩘᨠᩮᩣ", value: "ṅko" },
+  { id: "ᨬᩮ᩠ᨧᩣ", value: "ñco" },
+  { id: "ᨬᩮ᩠ᨩᩣ", value: "ñjo" },
+  { id: "ᨬᩮ᩠ᨬᩣ", value: "ñño" },
+  { id: "ᨱᩮ᩠ᨯᩣ", value: "ṇḍo" },
+  { id: "ᨱᩮ᩠ᨱᩣ", value: "ṇṇo" },
+  { id: "ᨱᩮ᩠ᨭᩣ", value: "ṇṭo" },
+  { id: "ᨷᩮ᩠ᨷᩣ", value: "ppo" },
+  { id: "ᩁᩮ᩠ᩉᩣ", value: "rho" },
+  { id: "ᩁᩮ᩠ᩅᩣ", value: "rvo" },
+  { id: "ᩁᩮ᩠ᨿᩣ", value: "ryo" },
+  { id: "ᨲᩮ᩠ᨲᩣ", value: "tto" },
+  { id: "ᨲᩮ᩠ᩅᩣ", value: "tvo" },
+  { id: "ᨭᩮ᩠ᨭᩣ", value: "ṭṭo" },
+  { id: "ᩅᩮ᩠ᩉᩣ", value: "vho" },
+  { id: "ᩅᩮ᩠ᩕᩣ", value: "vro" },
+  { id: "ᩅᩮ᩠ᨿᩣ", value: "vyo" },
+  { id: "ᨿᩮ᩠ᩉᩣ", value: "yho" },
+  { id: "ᨿᩮ᩠ᩕᩣ", value: "yro" },
+  { id: "ᨿᩮ᩠ᩅᩣ", value: "yvo" },
+  { id: "ᨿᩮ᩠ᨿᩣ", value: "yyo" },
+  { id: "ᨻᩮ᩠ᨻ", value: "bbe" },
+  { id: "ᨾᩮ᩠ᨻ", value: "mbe" },
+  { id: "ᨧᩮ᩠ᨧ", value: "cce" },
+  { id: "ᨬᩮ᩠ᨧ", value: "ñce" },
+  { id: "ᨴᩮ᩠ᨴ", value: "dde" },
+  { id: "ᨶᩮ᩠ᨴ", value: "nde" },
+  { id: "ᨯᩮ᩠ᨯ", value: "ḍḍe" },
+  { id: "ᨱᩮ᩠ᨯ", value: "ṇḍe" },
+  { id: "ᨣᩮ᩠ᨣ", value: "gge" },
+  { id: "ᩘᨣᩮ", value: "ṅge" },
+  { id: "ᩁᩮ᩠ᩉ", value: "rhe" },
+  { id: "ᩅᩮ᩠ᩉ", value: "vhe" },
+  { id: "ᨿᩮ᩠ᩉ", value: "yhe" },
+  { id: "ᨩᩮ᩠ᨩ", value: "jje" },
+  { id: "ᨬᩮ᩠ᨩ", value: "ñje" },
+  { id: "ᨠᩮ᩠ᨠ", value: "kke" },
+  { id: "ᩘᨠᩮ", value: "ṅke" },
+  { id: "ᨾᩮᩜ", value: "mmo" },
+  { id: "ᨾᩮ᩠ᨾ", value: "mme" },
+  { id: "ᨶᩮ᩠ᨶ", value: "nne" },
+  { id: "ᨬᩮ᩠ᨬ", value: "ññe" },
+  { id: "ᨱᩮ᩠ᨱ", value: "ṇṇe" },
+  { id: "ᨾᩮ᩠ᨷ", value: "mpe" },
+  { id: "ᨷᩮ᩠ᨷ", value: "ppe" },
+  { id: "ᩉᩮ᩠ᩕ", value: "hre" },
+  { id: "ᨠᩮ᩠ᩕ", value: "kre" },
+  { id: "ᩅᩮ᩠ᩕ", value: "vre" },
+  { id: "ᨿᩮ᩠ᩕ", value: "yre" },
+  { id: "ᨶᩮ᩠ᨲ", value: "nte" },
+  { id: "ᨲᩮ᩠ᨲ", value: "tte" },
+  { id: "ᨱᩮ᩠ᨭ", value: "ṇṭe" },
+  { id: "ᨭᩮ᩠ᨭ", value: "ṭṭe" },
+  { id: "ᨴᩮ᩠ᩅ", value: "dve" },
+  { id: "ᩉᩮ᩠ᩅ", value: "hve" },
+  { id: "ᩁᩮ᩠ᩅ", value: "rve" },
+  { id: "ᨲᩮ᩠ᩅ", value: "tve" },
+  { id: "ᨿᩮ᩠ᩅ", value: "yve" },
+  { id: "ᩉᩮ᩠ᨿ", value: "hye" },
+  { id: "ᩁᩮ᩠ᨿ", value: "rye" },
+  { id: "ᩅᩮ᩠ᨿ", value: "vye" },
+  { id: "ᨿᩮ᩠ᨿ", value: "yye" },
+  { id: "ᨾᩜᩣ", value: "mmā" },
+
+  { id: "ᩜ", value: "᩠ma" },
+  { id: "ᩱ", value: "᩠ai" },
+  { id: "ᩴ", value: "᩠aṃ" },
+  { id: "ᩥᩴ", value: "᩠iṃ" },
+  { id: "ᩩᩴ", value: "᩠uṃ" },
+  { id: "ᩣ", value: "᩠ā" },
+  { id: "ᩤ", value: "᩠ā" },
+  { id: "ᩥ", value: "᩠i" },
+  { id: "ᩦ", value: "᩠ī" },
+  { id: "ᩩ", value: "᩠u" },
+  { id: "ᩪ", value: "᩠ū" },
+  { id: "ᩮ", value: "᩠e" },
+  { id: "ᩮᩣ", value: "᩠o" },
+
+  { id: "ᨾᩜ", value: "mma" },
+  { id: "ᨻ᩠ᨿ᩠", value: "by" },
+  { id: "ᨭᩛ᩠", value: "ṭṭh" },
+  { id: "ᩔ᩠", value: "ss" },
+  { id: "ᩅ᩠ᩉ᩠", value: "vh" },
+  { id: "ᩅ᩠ᨿ᩠", value: "vy" },
+  { id: "ᩅᩕ᩠", value: "vr" },
+  { id: "ᨿ᩠ᩉ᩠", value: "yh" },
+  { id: "ᨿ᩠ᨿ᩠", value: "yy" },
+  { id: "ᨿᩕ᩠", value: "yr" },
+  { id: "ᨿ᩠ᩅ᩠", value: "yv" },
+  { id: "ᩉ᩠ᨿ᩠", value: "hy" },
+  { id: "ᩉᩕ᩠", value: "hr" },
+  { id: "ᩉ᩠ᩅ᩠", value: "hv" },
+  { id: "ᩁ᩠ᩅ᩠", value: "rv" },
+  { id: "ᩁ᩠ᩉ᩠", value: "rh" },
+  { id: "ᩁ᩠ᨿ᩠", value: "ry" },
+  { id: "ᨡ᩠", value: "kh" },
+  { id: "ᨥ᩠", value: "gh" },
+  { id: "ᨨ᩠", value: "ch" },
+  { id: "ᨫ᩠", value: "jh" },
+  { id: "ᨬ᩠ᨬ᩠", value: "ññ" },
+  { id: "ᨮ᩠", value: "ṭh" },
+  { id: "ᨰ᩠", value: "ḍh" },
+  { id: "ᨳ᩠", value: "th" },
+  { id: "ᨵ᩠", value: "dh" },
+  { id: "ᨹ᩠", value: "ph" },
+  { id: "ᨽ᩠", value: "bh" },
+  { id: "ᨠ᩠", value: "k" },
+  { id: "ᨣ᩠", value: "g" },
+  { id: "ᨧ᩠", value: "c" },
+  { id: "ᨩ᩠", value: "j" },
+  { id: "ᨬ᩠", value: "ñ" },
+  { id: "ᩊ᩠", value: "ḷ" },
+  { id: "ᨭ᩠", value: "ṭ" },
+  { id: "ᨯ᩠", value: "ḍ" },
+  { id: "ᨱ᩠", value: "ṇ" },
+  { id: "ᨲ᩠", value: "t" },
+  { id: "ᨴ᩠", value: "d" },
+  { id: "ᨶ᩠", value: "n" },
+  { id: "ᨷ᩠", value: "p" },
+  { id: "ᨻ᩠", value: "b" },
+  { id: "ᨾ᩠", value: "m" },
+  { id: "ᩃ᩠", value: "l" },
+  { id: "ᩈ᩠", value: "s" },
+  { id: "ᩘ", value: "ṅ" },
+
+  { id: "ᨻ᩠ᨿ", value: "bya" },
+  { id: "ᨭᩛ", value: "ṭṭha" },
+  { id: "ᩔ", value: "ssa" },
+  { id: "ᩅ᩠ᩉ", value: "vha" },
+  { id: "ᩅ᩠ᨿ", value: "vya" },
+  { id: "ᩅᩕ", value: "vra" },
+  { id: "ᨿ᩠ᩉ", value: "yha" },
+  { id: "ᨿ᩠ᨿ", value: "yya" },
+  { id: "ᨿᩕ", value: "yra" },
+  { id: "ᨿ᩠ᩅ", value: "yva" },
+  { id: "ᩉ᩠ᨿ", value: "hya" },
+  { id: "ᩉᩕ", value: "hra" },
+  { id: "ᩉ᩠ᩅ", value: "hva" },
+  { id: "ᩁ᩠ᩅ", value: "rva" },
+  { id: "ᩁ᩠ᩉ", value: "rha" },
+  { id: "ᩁ᩠ᨿ", value: "rya" },
+  { id: "ᨡ", value: "kha" },
+  { id: "ᨥ", value: "gha" },
+  { id: "ᨨ", value: "cha" },
+  { id: "ᨫ", value: "jha" },
+  { id: "ᨬ᩠ᨬ", value: "ñña" },
+  { id: "ᨮ", value: "ṭha" },
+  { id: "ᨰ", value: "ḍha" },
+  { id: "ᨳ", value: "tha" },
+  { id: "ᨵ", value: "dha" },
+  { id: "ᨹ", value: "pha" },
+  { id: "ᨽ", value: "bha" },
+  { id: "ᨠ", value: "ka" },
+  { id: "ᨣ", value: "ga" },
+  { id: "ᨧ", value: "ca" },
+  { id: "ᨩ", value: "ja" },
+  { id: "ᨬ", value: "ña" },
+  { id: "ᩊ", value: "ḷa" },
+  { id: "ᨭ", value: "ṭa" },
+  { id: "ᨯ", value: "ḍa" },
+  { id: "ᨱ", value: "ṇa" },
+  { id: "ᨲ", value: "ta" },
+  { id: "ᨴ", value: "da" },
+  { id: "ᨶ", value: "na" },
+  { id: "ᨷ", value: "pa" },
+  { id: "ᨻ", value: "ba" },
+  { id: "ᨾ", value: "ma" },
+  { id: "ᩃ", value: "la" },
+  { id: "ᩈ", value: "sa" },
+  { id: "ᩘ", value: "ṅa" },
+
+  { id: "᩠ᩉ᩠", value: "᩠h" },
+  { id: "ᩉ᩠", value: "h" },
+  { id: "ᩉ", value: "ha" },
+  { id: "᩠ᨿ", value: "᩠y" },
+  { id: "ᨿ᩠", value: "y" },
+  { id: "ᨿ", value: "ya" },
+  { id: "ᩕ᩠", value: "᩠r" },
+  { id: "ᩕ᩠", value: "᩠r" },
+  { id: "aᩕ", value: "ra" },
+  { id: "ᩕ", value: "r" },
+  { id: "ᩁ᩠", value: "r" },
+  { id: "ᩁ", value: "ra" },
+  { id: "᩠ᩅ᩠", value: "᩠v" },
+  { id: "ᩅ᩠", value: "v" },
+  { id: "ᩅ", value: "va" },
+  { id: "ᩋᩴ", value: "aṃ" },
+  { id: "ᨠ᩠ᨠᩴ", value: "iṃ" },
+  { id: "ᩏᩴ", value: "uṃ" },
+  { id: "ᩋ", value: "a" },
+  { id: "ᩋᩣ", value: "ā" },
+  { id: "ᩍ", value: "i" },
+  { id: "ᩎ", value: "ī" },
+  { id: "ᩏ", value: "u" },
+  { id: "ᩐ", value: "ū" },
+  { id: "ᩑ", value: "e" },
+  { id: "ᩒ", value: "o" },
+  { id: "e᩠ā", value: "o" },
+  { id: "a᩠", value: "" },
+  { id: "᩠a", value: "" },
+  { id: "nrd", value: "ndr" },
+  { id: "nrt", value: "ntr" },
+];
+
+const char_tai_old_to_r = [
+  { id: "ํ", value: "ฺaṃ" },
+  { id: "ิํ", value: "ฺiṃ" },
+  { id: "ุํ", value: "ฺuṃ" },
+  { id: "า", value: "ฺā" },
+  { id: "ิ", value: "ฺi" },
+  { id: "ี", value: "ฺī" },
+  { id: "ุ", value: "ฺu" },
+  { id: "ู", value: "ฺū" },
+
+  { id: "เข", value: "khe" },
+  { id: "เฃ", value: "ghe" },
+  { id: "เฉ", value: "che" },
+  { id: "เณ", value: "jhe" },
+  { id: "เฐ", value: "ṭhe" },
+  { id: "เฒ", value: "ḍhe" },
+  { id: "เถ", value: "the" },
+  { id: "เธ", value: "dhe" },
+  { id: "เผ", value: "phe" },
+  { id: "เภ", value: "bhe" },
+  { id: "โข", value: "kho" },
+  { id: "โฃ", value: "gho" },
+  { id: "โฉ", value: "cho" },
+  { id: "โณ", value: "jho" },
+  { id: "โฐ", value: "ṭho" },
+  { id: "โฒ", value: "ḍho" },
+  { id: "โถ", value: "tho" },
+  { id: "โธ", value: "dho" },
+  { id: "โผ", value: "pho" },
+  { id: "โภ", value: "bho" },
+  { id: "เก", value: "ke" },
+  { id: "เค", value: "ge" },
+  { id: "เจ", value: "ce" },
+  { id: "เช", value: "je" },
+  { id: "เญ", value: "ñe" },
+  { id: "เฬ", value: "ḷe" },
+  { id: "เฏ", value: "ṭe" },
+  { id: "เฑ", value: "ḍe" },
+  { id: "เฌ", value: "ṇe" },
+  { id: "เต", value: "te" },
+  { id: "เท", value: "de" },
+  { id: "เน", value: "ne" },
+  { id: "เบ", value: "pe" },
+  { id: "เพ", value: "be" },
+  { id: "เม", value: "me" },
+  { id: "เล", value: "le" },
+  { id: "เส", value: "se" },
+  { id: "เง", value: "ṅe" },
+  { id: "เห", value: "he" },
+  { id: "เย", value: "ye" },
+  { id: "เร", value: "re" },
+  { id: "เว", value: "ve" },
+  { id: "โก", value: "ko" },
+  { id: "โค", value: "go" },
+  { id: "โจ", value: "co" },
+  { id: "โช", value: "jo" },
+  { id: "โญ", value: "ño" },
+  { id: "โฬ", value: "ḷo" },
+  { id: "โฏ", value: "ṭo" },
+  { id: "โฑ", value: "ḍo" },
+  { id: "โฌ", value: "ṇo" },
+  { id: "โต", value: "to" },
+  { id: "โท", value: "do" },
+  { id: "โน", value: "no" },
+  { id: "โบ", value: "po" },
+  { id: "โพ", value: "bo" },
+  { id: "โม", value: "mo" },
+  { id: "โล", value: "lo" },
+  { id: "โส", value: "so" },
+  { id: "โง", value: "ṅo" },
+  { id: "โห", value: "ho" },
+  { id: "โย", value: "yo" },
+  { id: "โร", value: "ro" },
+  { id: "โว", value: "vo" },
+  { id: "ขฺ", value: "kh" },
+  { id: "ฃฺ", value: "gh" },
+  { id: "ฉฺ", value: "ch" },
+  { id: "ณฺ", value: "jh" },
+  { id: "ฐฺ", value: "ṭh" },
+  { id: "ฒฺ", value: "ḍh" },
+  { id: "ถฺ", value: "th" },
+  { id: "ธฺ", value: "dh" },
+  { id: "ผฺ", value: "ph" },
+  { id: "ภฺ", value: "bh" },
+  { id: "กฺ", value: "k" },
+  { id: "คฺ", value: "g" },
+  { id: "จฺ", value: "c" },
+  { id: "ชฺ", value: "j" },
+  { id: "ญฺ", value: "ñ" },
+  { id: "ฬฺ", value: "ḷ" },
+  { id: "ฏฺ", value: "ṭ" },
+  { id: "ฑฺ", value: "ḍ" },
+  { id: "ฌฺ", value: "ṇ" },
+  { id: "ตฺ", value: "t" },
+  { id: "ทฺ", value: "d" },
+  { id: "นฺ", value: "n" },
+  { id: "บฺ", value: "p" },
+  { id: "พฺ", value: "b" },
+  { id: "มฺ", value: "m" },
+  { id: "ลฺ", value: "l" },
+  { id: "สฺ", value: "s" },
+  { id: "งฺ", value: "ṅ" },
+  { id: "หฺ", value: "h" },
+  { id: "ยฺ", value: "y" },
+  { id: "รฺ", value: "r" },
+  { id: "วฺ", value: "v" },
+
+  { id: "ข", value: "kha" },
+  { id: "ฃ", value: "gha" },
+  { id: "ฉ", value: "cha" },
+  { id: "ณ", value: "jha" },
+  { id: "ฐ", value: "ṭha" },
+  { id: "ฒ", value: "ḍha" },
+  { id: "ถ", value: "tha" },
+  { id: "ธ", value: "dha" },
+  { id: "ผ", value: "pha" },
+  { id: "ภ", value: "bha" },
+  { id: "ก", value: "ka" },
+  { id: "ค", value: "ga" },
+  { id: "จ", value: "ca" },
+  { id: "ช", value: "ja" },
+  { id: "ญ", value: "ña" },
+  { id: "ฬ", value: "ḷa" },
+  { id: "ฏ", value: "ṭa" },
+  { id: "ฑ", value: "ḍa" },
+  { id: "ฌ", value: "ṇa" },
+  { id: "ต", value: "ta" },
+  { id: "ท", value: "da" },
+  { id: "น", value: "na" },
+  { id: "บ", value: "pa" },
+  { id: "พ", value: "ba" },
+  { id: "ม", value: "ma" },
+  { id: "ล", value: "la" },
+  { id: "ส", value: "sa" },
+  { id: "ง", value: "ṅa" },
+  { id: "ห", value: "ha" },
+  { id: "ย", value: "ya" },
+  { id: "ร", value: "ra" },
+  { id: "ว", value: "va" },
+
+  { id: "อํ", value: "aṃ" },
+  { id: "อิํ", value: "iṃ" },
+  { id: "อุํ", value: "uṃ" },
+  { id: "อ", value: "a" },
+  { id: "อา", value: "ā" },
+  { id: "อิ", value: "i" },
+  { id: "อี", value: "ī" },
+  { id: "อุ", value: "u" },
+  { id: "อู", value: "ū" },
+  { id: "เอ", value: "e" },
+  { id: "โอ", value: "o" },
+  { id: "eฺā", value: "o" },
+  { id: "aฺ", value: "" },
+];
+*/
+export const roman_to_taitham = (input: string | null): string | null => {
+  if (input === null) {
+    return input;
+  }
+  let txt = input.toLowerCase();
+
+  try {
+    for (const iterator of char_roman_to_tai) {
+      txt = txt.replaceAll(iterator.id, iterator.value);
+    }
+  } catch (err) {
+    //error
+    console.error(err);
+  }
+  return txt;
+};

+ 135 - 0
dashboard/src/components/code/thai.ts

@@ -0,0 +1,135 @@
+const char_roman_to_thai = [
+  { id: "khe", value: "เข" },
+  { id: "ghe", value: "เฃ" },
+  { id: "che", value: "เฉ" },
+  { id: "jhe", value: "เณ" },
+  { id: "ṭhe", value: "เฐ" },
+  { id: "ḍhe", value: "เฒ" },
+  { id: "the", value: "เถ" },
+  { id: "dhe", value: "เธ" },
+  { id: "phe", value: "เผ" },
+  { id: "bhe", value: "เภ" },
+  { id: "kho", value: "โข" },
+  { id: "gho", value: "โฃ" },
+  { id: "cho", value: "โฉ" },
+  { id: "jho", value: "โณ" },
+  { id: "ṭho", value: "โฐ" },
+  { id: "ḍho", value: "โฒ" },
+  { id: "tho", value: "โถ" },
+  { id: "dho", value: "โธ" },
+  { id: "pho", value: "โผ" },
+  { id: "bho", value: "โภ" },
+  { id: "ke", value: "เก" },
+  { id: "ge", value: "เค" },
+  { id: "ce", value: "เจ" },
+  { id: "je", value: "เช" },
+  { id: "ñe", value: "เญ" },
+  { id: "ḷe", value: "เฬ" },
+  { id: "ṭe", value: "เฏ" },
+  { id: "ḍe", value: "เฑ" },
+  { id: "ṇe", value: "เฌ" },
+  { id: "te", value: "เต" },
+  { id: "de", value: "เท" },
+  { id: "ne", value: "เน" },
+  { id: "pe", value: "เป" },
+  { id: "be", value: "เพ" },
+  { id: "me", value: "เม" },
+  { id: "le", value: "เล" },
+  { id: "se", value: "เส" },
+  { id: "ṅe", value: "เง" },
+  { id: "he", value: "เห" },
+  { id: "ye", value: "เย" },
+  { id: "re", value: "เร" },
+  { id: "ve", value: "เว" },
+  { id: "ko", value: "โก" },
+  { id: "go", value: "โค" },
+  { id: "co", value: "โจ" },
+  { id: "jo", value: "โช" },
+  { id: "ño", value: "โญ" },
+  { id: "ḷo", value: "โฬ" },
+  { id: "ṭo", value: "โฏ" },
+  { id: "ḍo", value: "โฑ" },
+  { id: "ṇo", value: "โฌ" },
+  { id: "to", value: "โต" },
+  { id: "do", value: "โท" },
+  { id: "no", value: "โน" },
+  { id: "po", value: "โป" },
+  { id: "bo", value: "โพ" },
+  { id: "mo", value: "โม" },
+  { id: "lo", value: "โล" },
+  { id: "so", value: "โส" },
+  { id: "ṅo", value: "โง" },
+  { id: "ho", value: "โห" },
+  { id: "yo", value: "โย" },
+  { id: "ro", value: "โร" },
+  { id: "vo", value: "โว" },
+  { id: "kh", value: "ขฺ" },
+  { id: "gh", value: "ฃฺ" },
+  { id: "ch", value: "ฉฺ" },
+  { id: "jh", value: "ณฺ" },
+  { id: "ṭh", value: "ฐฺ" },
+  { id: "ḍh", value: "ฒฺ" },
+  { id: "th", value: "ถฺ" },
+  { id: "dh", value: "ธฺ" },
+  { id: "ph", value: "ผฺ" },
+  { id: "bh", value: "ภฺ" },
+  { id: "k", value: "กฺ" },
+  { id: "g", value: "คฺ" },
+  { id: "c", value: "จฺ" },
+  { id: "j", value: "ชฺ" },
+  { id: "ñ", value: "ญฺ" },
+  { id: "ḷ", value: "ฬฺ" },
+  { id: "ṭ", value: "ฏฺ" },
+  { id: "ḍ", value: "ฑฺ" },
+  { id: "ṇ", value: "ฌฺ" },
+  { id: "t", value: "ตฺ" },
+  { id: "d", value: "ทฺ" },
+  { id: "n", value: "นฺ" },
+  { id: "p", value: "ปฺ" },
+  { id: "b", value: "พฺ" },
+  { id: "m", value: "มฺ" },
+  { id: "l", value: "ลฺ" },
+  { id: "s", value: "สฺ" },
+  { id: "ṅ", value: "งฺ" },
+  { id: "h", value: "หฺ" },
+  { id: "y", value: "ยฺ" },
+  { id: "r", value: "รฺ" },
+  { id: "v", value: "วฺ" },
+  { id: "ฺaṃ", value: "ํ" },
+  { id: "ฺiṃ", value: "ิํ" },
+  { id: "ฺuṃ", value: "ุํ" },
+  { id: "ฺā", value: "า" },
+  { id: "ฺi", value: "ิ" },
+  { id: "ฺī", value: "ี" },
+  { id: "ฺu", value: "ุ" },
+  { id: "ฺū", value: "ู" },
+  { id: "aṃ", value: "อํ" },
+  { id: "iṃ", value: "อิํ" },
+  { id: "uṃ", value: "อุํ" },
+  { id: "a", value: "อ" },
+  { id: "ā", value: "อา" },
+  { id: "i", value: "อิ" },
+  { id: "ī", value: "อี" },
+  { id: "u", value: "อุ" },
+  { id: "ū", value: "อู" },
+  { id: "e", value: "เอ" },
+  { id: "o", value: "โอ" },
+  { id: "ฺอ", value: "" },
+];
+
+export const roman_to_thai = (input: string | null): string | null => {
+  if (input === null) {
+    return input;
+  }
+  let txt = input.toLowerCase();
+
+  try {
+    for (const iterator of char_roman_to_thai) {
+      txt = txt.replaceAll(iterator.id, iterator.value);
+    }
+  } catch (err) {
+    //error
+    console.error(err);
+  }
+  return txt;
+};

+ 10 - 5
dashboard/src/components/corpus/BookTree.tsx

@@ -5,7 +5,7 @@ import { Layout, Space, Tree } from "antd";
 import { Select } from "antd";
 import { Typography } from "antd";
 import type { TreeProps } from "antd/es/tree";
-import { ApiFetch } from "../../utils";
+import { get } from "../../request";
 
 const { Text } = Typography;
 
@@ -46,11 +46,12 @@ const Widget = (prop: IWidgetBookTree) => {
 				title: params.name,
 				key: params.tag.join(),
 				tag: params.tag,
-				children: Array.isArray(params.children) ? params.children.map(treeMap) : [],
+				children: Array.isArray(params.children)
+					? params.children.map(treeMap)
+					: [],
 			};
 		}
-		let url = `/palibook/${value}`;
-		ApiFetch(url).then((response) => {
+		get(`/v2/palibook/${value}`).then((response) => {
 			const myJson = response as unknown as OrgTree[];
 			let newTree = myJson.map(treeMap);
 			setTreeData(newTree);
@@ -66,7 +67,11 @@ const Widget = (prop: IWidgetBookTree) => {
 		<Layout>
 			<Space>
 				<Text>目录风格</Text>
-				<Select defaultValue={prop.root} loading={false} onChange={handleChange}>
+				<Select
+					defaultValue={prop.root}
+					loading={false}
+					onChange={handleChange}
+				>
 					<Option value="defualt">Defualt</Option>
 					<Option value="cscd">CSCD</Option>
 				</Select>

+ 59 - 73
dashboard/src/components/corpus/ChapterCard.tsx

@@ -7,86 +7,72 @@ 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;
-	PaliTitle: string;
-	Path: string;
-	Book: number;
-	Paragraph: number;
-	Summary: string;
-	Tag: TagNode[];
-	Channel: ChannelInfoProps;
-	CreatedAt: string;
-	UpdatedAt: string;
-	Hit: number;
-	Like: number;
+  Title: string;
+  PaliTitle: string;
+  Path: string;
+  Book: number;
+  Paragraph: number;
+  Summary: string;
+  Tag: TagNode[];
+  Channel: ChannelInfoProps;
+  CreatedAt: string;
+  UpdatedAt: string;
+  Hit: number;
+  Like: number;
 }
 
 interface IWidgetChapterCard {
-	data: ChapterData;
+  data: ChapterData;
 }
 
-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}>
-					<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>
-						</Col>
-						<Col span={8}>进度条</Col>
-					</Row>
-					<Row>
-						<Col>
-							<Paragraph>
-								<div style={aa}>{prop.data.Summary}</div>
-							</Paragraph>
-						</Col>
-					</Row>
-					<Row>
-						<Col span={16}>
-							<TagArea data={tags} />
-						</Col>
-						<Col span={5}>
-							<ChannelListItem data={prop.data.Channel} />
-						</Col>
-						<Col span={3}>
-							<TimeShow time={prop.data.UpdatedAt} title="UpdatedAt" />
-						</Col>
-					</Row>
-				</Col>
-			</Row>
-		</>
-	);
+const Widget = ({ data }: IWidgetChapterCard) => {
+  const path = JSON.parse(data.Path);
+  const tags = data.Tag;
+  return (
+    <>
+      <Row>
+        <Col>
+          <Row>
+            <Col span={16}>
+              <Title level={5}>
+                <Link>{data.Title}</Link>
+              </Title>
+              <Text type="secondary">{data.PaliTitle}</Text>
+              <TocPath data={path} />
+            </Col>
+            <Col span={8}>进度条</Col>
+          </Row>
+          <Row>
+            <Col>
+              <Paragraph
+                ellipsis={{
+                  rows: 2,
+                  expandable: false,
+                  symbol: "more",
+                }}
+              >
+                {data.Summary}
+              </Paragraph>
+            </Col>
+          </Row>
+          <Row>
+            <Col span={16}>
+              <TagArea data={tags} />
+            </Col>
+            <Col span={5}>
+              <ChannelListItem data={data.Channel} />
+            </Col>
+            <Col span={3}>
+              <TimeShow time={data.UpdatedAt} title="UpdatedAt" />
+            </Col>
+          </Row>
+        </Col>
+      </Row>
+    </>
+  );
 };
 
 export default Widget;

+ 22 - 13
dashboard/src/components/corpus/ChapterHead.tsx

@@ -1,25 +1,34 @@
 import { Typography } from "antd";
+import { Link } from "react-router-dom";
 
 const { Title, Text } = Typography;
 
 export interface IChapterInfo {
-	title: string;
-	subTitle?: string;
-	summary?: string;
-	cover?: string;
+  title: string;
+  subTitle?: string;
+  summary?: string;
+  cover?: string;
+  book?: number;
+  para?: number;
 }
 interface IWidgetPaliChapterHeading {
-	data: IChapterInfo;
+  data: IChapterInfo;
 }
 const Widget = (prop: IWidgetPaliChapterHeading) => {
-	return (
-		<>
-			<Title level={4}>{prop.data.title}</Title>
-			<div>
-				<Text type="secondary">{prop.data.subTitle ? prop.data.subTitle : ""}</Text>
-			</div>
-		</>
-	);
+  return (
+    <>
+      <Title level={4}>
+        <Link to={`/article/chapter/${prop.data.book}-${prop.data.para}`}>
+          {prop.data.title}
+        </Link>
+      </Title>
+      <div>
+        <Text type="secondary">
+          {prop.data.subTitle ? prop.data.subTitle : ""}
+        </Text>
+      </div>
+    </>
+  );
 };
 
 export default Widget;

+ 73 - 32
dashboard/src/components/corpus/ChapterInChannel.tsx

@@ -1,48 +1,89 @@
-import { Col, Layout, Progress, Row, Space } from "antd";
+import { Col, Layout, Progress, Row, Space, Tabs } from "antd";
 import { Typography } from "antd";
 import { LikeOutlined, EyeOutlined } from "@ant-design/icons";
 import { ChannelInfoProps } from "../api/Channel";
 import ChannelListItem from "../channel/ChannelListItem";
 import TimeShow from "../utilities/TimeShow";
+import { useIntl } from "react-intl";
+import { Link } from "react-router-dom";
 
 const { Text } = Typography;
 
 export interface IChapterChannelData {
-	channel: ChannelInfoProps;
-	progress: number;
-	hit: number;
-	like: number;
-	updatedAt: string;
+  channel: ChannelInfoProps;
+
+  progress: number;
+  hit: number;
+  like: number;
+  updatedAt: string;
 }
 interface IWidgetChapterInChannel {
-	data: IChapterChannelData[];
+  data: IChapterChannelData[];
+  book: number;
+  para: number;
 }
-const Widget = (prop: IWidgetChapterInChannel) => {
-	const view = prop.data.map((item, id) => {
-		return (
-			<Layout key={id}>
-				<Row>
-					<Col span={5}>
-						<ChannelListItem data={item.channel} />
-					</Col>
-					<Col span={5}>
-						<Progress percent={item.progress} size="small" />
-					</Col>
-					<Col span={8}></Col>
-				</Row>
+const Widget = ({ data, book, para }: IWidgetChapterInChannel) => {
+  const intl = useIntl(); //i18n
+  function getTab(type: string): JSX.Element[] {
+    const output = data.map((item, id) => {
+      if (item.channel.channelType === type) {
+        return (
+          <div key={id}>
+            <Row>
+              <Col span={5}>
+                <Link
+                  to={`/article/chapter/${book}-${para}_${item.channel.channelId}`}
+                  target="_blank"
+                >
+                  <ChannelListItem data={item.channel} />
+                </Link>
+              </Col>
+              <Col span={5}>
+                <Progress percent={item.progress} size="small" />
+              </Col>
+              <Col span={8}></Col>
+            </Row>
+
+            <Text type="secondary">
+              <Space style={{ paddingLeft: "2em" }}>
+                <EyeOutlined />
+                {item.hit} | <LikeOutlined />
+                {item.like} |
+                <TimeShow time={item.updatedAt} title={item.updatedAt} />
+              </Space>
+            </Text>
+          </div>
+        );
+      } else {
+        return <></>;
+      }
+    });
+    return output;
+  }
 
-				<Text type="secondary">
-					<Space style={{ paddingLeft: "2em" }}>
-						<EyeOutlined />
-						{item.hit} | <LikeOutlined />
-						{item.like} |
-						<TimeShow time={item.updatedAt} title={item.updatedAt} />
-					</Space>
-				</Text>
-			</Layout>
-		);
-	});
-	return <>{view}</>;
+  const items = [
+    {
+      label: intl.formatMessage({ id: "channel.type.translation.label" }),
+      key: "translation",
+      children: getTab("translation"),
+    },
+    {
+      label: intl.formatMessage({ id: "channel.type.nissaya.label" }),
+      key: "nissaya",
+      children: getTab("nissaya"),
+    },
+    {
+      label: intl.formatMessage({ id: "channel.type.commentary.label" }),
+      key: "commentary",
+      children: getTab("commentary"),
+    },
+    {
+      label: intl.formatMessage({ id: "channel.type.original.label" }),
+      key: "original",
+      children: getTab("original"),
+    },
+  ];
+  return <Tabs items={items} />;
 };
 
 export default Widget;

+ 2 - 2
dashboard/src/components/corpus/ChapterTagList.tsx

@@ -1,6 +1,6 @@
 import { message, Tag, Button } from "antd";
 import { useState, useEffect } from "react";
-import { ApiFetch } from "../../utils";
+import { get } from "../../request";
 import { IApiChapterTag, IApiResponseChapterTagList } from "../api/Corpus";
 
 interface ITagData {
@@ -21,7 +21,7 @@ const Widget = (prop: IWidgetChapterTagList) => {
 	}, []);
 
 	function fetchData() {
-		ApiFetch(`/progress?view=chapter-tag`)
+		get(`/v2/progress?view=chapter-tag`)
 			.then((response) => {
 				const json = response as unknown as IApiResponseChapterTagList;
 				const tags: IApiChapterTag[] = json.data.rows;

+ 34 - 34
dashboard/src/components/corpus/PaliChapterChannelList.tsx

@@ -1,47 +1,47 @@
 import { useState, useEffect } from "react";
-import { ApiFetch } from "../../utils";
+import { get } from "../../request";
 import { IApiResponseChapterChannelList } from "../api/Corpus";
 import { IParagraph } from "./BookViewer";
 import ChapterInChannel, { IChapterChannelData } from "./ChapterInChannel";
 
 interface IWidgetPaliChapterChannelList {
-	para: IParagraph;
+  para: IParagraph;
 }
 const defaultData: IChapterChannelData[] = [];
-const Widget = (prop: IWidgetPaliChapterChannelList) => {
-	const [tableData, setTableData] = useState(defaultData);
+const Widget = ({ para }: IWidgetPaliChapterChannelList) => {
+  const [tableData, setTableData] = useState(defaultData);
 
-	useEffect(() => {
-		console.log("palichapterlist useEffect");
-		let url = `/progress?view=chapter_channels&book=${prop.para.book}&par=${prop.para.para}`;
-		ApiFetch(url).then(function (myJson) {
-			console.log("ajex", myJson);
-			const data = myJson as unknown as IApiResponseChapterChannelList;
-			const newData: IChapterChannelData[] = data.data.rows.map((item) => {
-				return {
-					channel: {
-						ChannelName: item.channel.name,
-						ChannelId: item.channel.uid,
-						ChannelType: item.channel.type,
-						StudioName: "V",
-						StudioId: "123",
-						StudioType: "p",
-					},
-					progress: Math.ceil(item.progress * 100),
-					hit: item.views,
-					like: 0,
-					updatedAt: item.updated_at,
-				};
-			});
-			setTableData(newData);
-		});
-	}, [prop.para]);
+  useEffect(() => {
+    console.log("palichapterlist useEffect");
+    let url = `/v2/progress?view=chapter_channels&book=${para.book}&par=${para.para}`;
+    get(url).then(function (myJson) {
+      console.log("ajex", myJson);
+      const data = myJson as unknown as IApiResponseChapterChannelList;
+      const newData: IChapterChannelData[] = data.data.rows.map((item) => {
+        return {
+          channel: {
+            channelName: item.channel.name,
+            channelId: item.channel.uid,
+            channelType: item.channel.type,
+            studioName: "V",
+            studioId: "123",
+            studioType: "p",
+          },
+          progress: Math.ceil(item.progress * 100),
+          hit: item.views,
+          like: 0,
+          updatedAt: item.updated_at,
+        };
+      });
+      setTableData(newData);
+    });
+  }, [para]);
 
-	return (
-		<>
-			<ChapterInChannel data={tableData} />
-		</>
-	);
+  return (
+    <>
+      <ChapterInChannel data={tableData} book={para.book} para={para.para} />
+    </>
+  );
 };
 
 export default Widget;

+ 58 - 56
dashboard/src/components/corpus/PaliChapterHead.tsx

@@ -3,68 +3,70 @@ import { message } from "antd";
 import ChapterHead, { IChapterInfo } from "./ChapterHead";
 import { IParagraph } from "./BookViewer";
 import TocPath, { ITocPathNode } from "./TocPath";
-import { IApiResponcePaliChapter } from "../api/Corpus";
-import { ApiFetch } from "../../utils";
+import { IApiResponsePaliChapter } from "../api/Corpus";
+import { get } from "../../request";
 
 interface IWidgetPaliChapterHead {
-	para: IParagraph;
-	onChange?: Function;
+  para: IParagraph;
+  onChange?: Function;
 }
 
 const Widget = (prop: IWidgetPaliChapterHead) => {
-	const defaultPathData: ITocPathNode[] = [
-		{
-			book: 98,
-			paragraph: 55,
-			title: "string;",
-			paliTitle: "string;",
-			level: 2,
-		},
-	];
-	const [pathData, setPathData] = useState(defaultPathData);
-	const [chapterData, setChapterData] = useState({ title: "" });
-	useEffect(() => {
-		console.log("palichapterlist useEffect");
-		fetchData(prop.para);
-	}, [prop.para]);
+  const defaultPathData: ITocPathNode[] = [
+    {
+      book: 98,
+      paragraph: 55,
+      title: "string;",
+      paliTitle: "string;",
+      level: 2,
+    },
+  ];
+  const [pathData, setPathData] = useState(defaultPathData);
+  const [chapterData, setChapterData] = useState<IChapterInfo>({ title: "" });
+  useEffect(() => {
+    console.log("palichapterlist useEffect");
+    fetchData(prop.para);
+  }, [prop.para]);
 
-	function fetchData(para: IParagraph) {
-		let url = `/palitext?view=paragraph&book=${para.book}&para=${para.para}`;
-		ApiFetch(url).then(function (myJson) {
-			console.log("ajex", myJson);
-			const data = myJson as unknown as IApiResponcePaliChapter;
-			let path: ITocPathNode[] = JSON.parse(data.data.path);
-			path.push({
-				book: data.data.book,
-				paragraph: data.data.paragraph,
-				title: data.data.toc,
-				paliTitle: data.data.toc,
-				level: data.data.level,
-			});
-			setPathData(path);
-			const chapter: IChapterInfo = {
-				title: data.data.toc,
-				subTitle: data.data.toc,
-			};
-			setChapterData(chapter);
-		});
-	}
-	return (
-		<>
-			<TocPath
-				data={pathData}
-				onChange={(e: IParagraph) => {
-					message.success(e.book + ":" + e.para);
-					fetchData(e);
-					if (typeof prop.onChange !== "undefined") {
-						prop.onChange(e);
-					}
-				}}
-				link={"none"}
-			/>
-			<ChapterHead data={chapterData} />
-		</>
-	);
+  function fetchData(para: IParagraph) {
+    let url = `/v2/palitext?view=paragraph&book=${para.book}&para=${para.para}`;
+    get<IApiResponsePaliChapter>(url).then(function (myJson) {
+      console.log("ajex", myJson);
+      const data = myJson;
+      let path: ITocPathNode[] = JSON.parse(data.data.path);
+      path.push({
+        book: data.data.book,
+        paragraph: data.data.paragraph,
+        title: data.data.toc,
+        paliTitle: data.data.toc,
+        level: data.data.level,
+      });
+      setPathData(path);
+      const chapter: IChapterInfo = {
+        title: data.data.toc,
+        subTitle: data.data.toc,
+        book: data.data.book,
+        para: data.data.paragraph,
+      };
+      setChapterData(chapter);
+    });
+  }
+  return (
+    <>
+      <TocPath
+        data={pathData}
+        onChange={(e: IParagraph) => {
+          message.success(e.book + ":" + e.para);
+          fetchData(e);
+          if (typeof prop.onChange !== "undefined") {
+            prop.onChange(e);
+          }
+        }}
+        link={"none"}
+      />
+      <ChapterHead data={chapterData} />
+    </>
+  );
 };
 
 export default Widget;

+ 35 - 35
dashboard/src/components/corpus/PaliChapterListByPara.tsx

@@ -1,49 +1,49 @@
 import { useState, useEffect } from "react";
-import { ApiFetch } from "../../utils";
-import { IApiResponcePaliChapterList } from "../api/Corpus";
+import { get } from "../../request";
+import { IApiResponsePaliChapterList } from "../api/Corpus";
 import { IParagraph } from "./BookViewer";
 import { IPaliChapterData } from "./PaliChapterCard";
 import PaliChapterList, { IChapterClickEvent } from "./PaliChapterList";
 
 interface IWidgetPaliChapterListByPara {
-	para: IParagraph;
-	onChapterClick?: Function;
+  para: IParagraph;
+  onChapterClick?: Function;
 }
 const defaultData: IPaliChapterData[] = [];
 const Widget = (prop: IWidgetPaliChapterListByPara) => {
-	const [tableData, setTableData] = useState(defaultData);
+  const [tableData, setTableData] = useState(defaultData);
 
-	useEffect(() => {
-		console.log("palichapterlist useEffect");
-		let url = `/palitext?view=chapter_children&book=${prop.para.book}&para=${prop.para.para}`;
-		ApiFetch(url).then(function (myJson) {
-			console.log("ajex", myJson);
-			const data = myJson as unknown as IApiResponcePaliChapterList;
-			let newTree: IPaliChapterData[] = data.data.rows.map((item) => {
-				return {
-					Title: item.toc,
-					PaliTitle: item.toc,
-					Path: item.path,
-					Book: item.book,
-					Paragraph: item.paragraph,
-				};
-			});
-			setTableData(newTree);
-		});
-	}, [prop.para]);
+  useEffect(() => {
+    console.log("palichapterlist useEffect");
+    let url = `/v2/palitext?view=chapter_children&book=${prop.para.book}&para=${prop.para.para}`;
+    get(url).then(function (myJson) {
+      console.log("ajex", myJson);
+      const data = myJson as unknown as IApiResponsePaliChapterList;
+      let newTree: IPaliChapterData[] = data.data.rows.map((item) => {
+        return {
+          Title: item.toc,
+          PaliTitle: item.toc,
+          Path: item.path,
+          Book: item.book,
+          Paragraph: item.paragraph,
+        };
+      });
+      setTableData(newTree);
+    });
+  }, [prop.para]);
 
-	return (
-		<>
-			<PaliChapterList
-				onChapterClick={(e: IChapterClickEvent) => {
-					if (prop.onChapterClick) {
-						prop.onChapterClick(e);
-					}
-				}}
-				data={tableData}
-			/>
-		</>
-	);
+  return (
+    <>
+      <PaliChapterList
+        onChapterClick={(e: IChapterClickEvent) => {
+          if (prop.onChapterClick) {
+            prop.onChapterClick(e);
+          }
+        }}
+        data={tableData}
+      />
+    </>
+  );
 };
 
 export default Widget;

+ 36 - 36
dashboard/src/components/corpus/PaliChapterListByTag.tsx

@@ -1,49 +1,49 @@
 import { useState, useEffect } from "react";
-import { ApiFetch } from "../../utils";
-import { IApiResponcePaliChapterList } from "../api/Corpus";
+import { get } from "../../request";
+import { IApiResponsePaliChapterList } from "../api/Corpus";
 import { IPaliChapterData } from "./PaliChapterCard";
 import PaliChapterList, { IChapterClickEvent } from "./PaliChapterList";
 
 interface IWidgetPaliChapterListByTag {
-	tag: string[];
-	onChapterClick?: Function;
+  tag: string[];
+  onChapterClick?: Function;
 }
 const defaultData: IPaliChapterData[] = [];
 const Widget = (prop: IWidgetPaliChapterListByTag) => {
-	const [tableData, setTableData] = useState(defaultData);
+  const [tableData, setTableData] = useState(defaultData);
 
-	useEffect(() => {
-		console.log("palichapterlist useEffect");
-		let url = `/palitext?view=chapter&tags=${prop.tag.join()}`;
-		console.log("tag url", url);
-		ApiFetch(url).then(function (myJson) {
-			console.log("ajex", myJson);
-			const data = myJson as unknown as IApiResponcePaliChapterList;
-			let newTree: IPaliChapterData[] = data.data.rows.map((item) => {
-				return {
-					Title: item.title,
-					PaliTitle: item.title,
-					Path: item.path,
-					Book: item.book,
-					Paragraph: item.paragraph,
-				};
-			});
-			setTableData(newTree);
-		});
-	}, [prop.tag]);
+  useEffect(() => {
+    console.log("palichapterlist useEffect");
+    let url = `/v2/palitext?view=chapter&tags=${prop.tag.join()}`;
+    console.log("tag url", url);
+    get(url).then(function (myJson) {
+      console.log("ajex", myJson);
+      const data = myJson as unknown as IApiResponsePaliChapterList;
+      let newTree: IPaliChapterData[] = data.data.rows.map((item) => {
+        return {
+          Title: item.title,
+          PaliTitle: item.title,
+          Path: item.path,
+          Book: item.book,
+          Paragraph: item.paragraph,
+        };
+      });
+      setTableData(newTree);
+    });
+  }, [prop.tag]);
 
-	return (
-		<>
-			<PaliChapterList
-				data={tableData}
-				onChapterClick={(e: IChapterClickEvent) => {
-					if (typeof prop.onChapterClick !== "undefined") {
-						prop.onChapterClick(e);
-					}
-				}}
-			/>
-		</>
-	);
+  return (
+    <>
+      <PaliChapterList
+        data={tableData}
+        onChapterClick={(e: IChapterClickEvent) => {
+          if (typeof prop.onChapterClick !== "undefined") {
+            prop.onChapterClick(e);
+          }
+        }}
+      />
+    </>
+  );
 };
 
 export default Widget;

+ 56 - 43
dashboard/src/components/corpus/TocPath.tsx

@@ -2,55 +2,68 @@ import { Link } from "react-router-dom";
 import { Breadcrumb } from "antd";
 
 export interface ITocPathNode {
-	book: number;
-	paragraph: number;
-	title: string;
-	paliTitle?: string;
-	level: number;
+  book: number;
+  paragraph: number;
+  title: string;
+  paliTitle?: string;
+  level: number;
 }
 
 export declare type ELinkType = "none" | "blank" | "self";
 
 interface IWidgetTocPath {
-	data: ITocPathNode[];
-	link?: ELinkType;
-	channel?: string;
-	onChange?: Function;
+  data: ITocPathNode[];
+  link?: ELinkType;
+  channel?: string[];
+  onChange?: Function;
 }
-const Widget = (prop: IWidgetTocPath) => {
-	const link: ELinkType = prop.link ? prop.link : "blank";
-	const path = prop.data.map((item, id) => {
-		const linkChapter = `/article/index.php?view=chapter&book=${item.book}&par=${item.paragraph}`;
-		let oneItem = <></>;
-		switch (link) {
-			case "none":
-				oneItem = <>{item.title}</>;
-				break;
-			case "blank":
-				oneItem = <Link to={linkChapter}>{item.title}</Link>;
-				break;
-			case "self":
-				oneItem = <Link to={linkChapter}>{item.title}</Link>;
-				break;
-		}
-		return (
-			<Breadcrumb.Item
-				onClick={() => {
-					if (typeof prop.onChange !== "undefined") {
-						prop.onChange({ book: item.book, para: item.paragraph });
-					}
-				}}
-				key={id}
-			>
-				{oneItem}
-			</Breadcrumb.Item>
-		);
-	});
-	return (
-		<>
-			<Breadcrumb>{path}</Breadcrumb>
-		</>
-	);
+const Widget = ({
+  data,
+  link = "blank",
+  channel,
+  onChange,
+}: IWidgetTocPath) => {
+  let sChannel = "";
+  if (typeof channel !== "undefined" && channel.length > 0) {
+    sChannel = "_" + channel.join("_");
+  }
+
+  const path = data.map((item, id) => {
+    const linkChapter = `/article/chapter/${item.book}-${item.paragraph}${sChannel}`;
+    let oneItem = <></>;
+    switch (link) {
+      case "none":
+        oneItem = <>{item.title}</>;
+        break;
+      case "blank":
+        oneItem = (
+          <Link to={linkChapter} target="_blank">
+            {item.title}
+          </Link>
+        );
+        break;
+      case "self":
+        oneItem = <Link to={linkChapter}>{item.title}</Link>;
+        break;
+    }
+    return (
+      <Breadcrumb.Item
+        onClick={() => {
+          if (typeof onChange !== "undefined") {
+            onChange({ book: item.book, para: item.paragraph });
+          }
+        }}
+        key={id}
+      >
+        {oneItem}
+      </Breadcrumb.Item>
+    );
+  });
+  return (
+    <>
+      <Breadcrumb>{path}</Breadcrumb>
+    </>
+  );
 };
 
 export default Widget;

+ 64 - 0
dashboard/src/components/dict/DictComponent.tsx

@@ -0,0 +1,64 @@
+import { useState, useEffect } from "react";
+import { Affix, Col, Row } from "antd";
+import { Input } from "antd";
+
+import DictSearch from "./DictSearch";
+
+import { useAppSelector } from "../../hooks";
+import { message } from "../../reducers/command";
+
+const { Search } = Input;
+
+export interface IWidgetDict {
+  word?: string;
+}
+const Widget = ({ word }: IWidgetDict) => {
+  const [container, setContainer] = useState<HTMLDivElement | null>(null);
+  const [wordSearch, setWordSearch] = useState(word);
+
+  const onSearch = (value: string) => {
+    console.log("onSearch", value);
+    setWordSearch(value);
+  };
+
+  //接收查字典消息
+  const commandMsg = useAppSelector(message);
+  useEffect(() => {
+    console.log("get command", commandMsg);
+    if (commandMsg?.type === "dict") {
+      setWordSearch(commandMsg.prop?.word);
+    }
+  }, [commandMsg]);
+
+  return (
+    <div ref={setContainer}>
+      <Affix offsetTop={0} target={() => container}>
+        <div style={{ backgroundColor: "gray", height: "3.5em" }}>
+          <Row style={{ paddingTop: "0.5em" }}>
+            <Col xs={0} lg={8}></Col>
+            <Col xs={24} lg={8}>
+              <Search
+                placeholder="input search text"
+                onSearch={onSearch}
+                value={wordSearch}
+                style={{ width: "100%" }}
+              />
+            </Col>
+            <Col xs={0} lg={8}></Col>
+          </Row>
+        </div>
+      </Affix>
+      <div>
+        <Row>
+          <Col flex="auto"></Col>
+          <Col flex="1260px">
+            <DictSearch word={wordSearch} />
+          </Col>
+          <Col flex="auto"></Col>
+        </Row>
+      </div>
+    </div>
+  );
+};
+
+export default Widget;

+ 7 - 5
dashboard/src/components/dict/DictSearch.tsx

@@ -1,8 +1,10 @@
 import { useState, useEffect } from "react";
-
 import DictContent from "./DictContent";
-import { ApiFetch } from "../../utils";
-import type { IWidgetDictContentData, IApiDictContentData } from "./DictContent";
+import type {
+	IWidgetDictContentData,
+	IApiDictContentData,
+} from "./DictContent";
+import { get } from "../../request";
 
 interface IWidgetDictSearch {
 	word: string | undefined;
@@ -18,9 +20,9 @@ const Widget = (prop: IWidgetDictSearch) => {
 
 	useEffect(() => {
 		console.log("useEffect");
-		const url = `/dict?word=${prop.word}`;
+		const url = `/v2/dict?word=${prop.word}`;
 		console.log("url", url);
-		ApiFetch(url)
+		get(url)
 			.then((response) => {
 				const json = response as unknown as IApiDictContentData;
 				console.log("data", json);

+ 29 - 32
dashboard/src/components/general/UiLangSelect.tsx

@@ -1,39 +1,36 @@
-import { Dropdown, Menu, Button } from "antd";
+import { Dropdown, Button } from "antd";
 import { GlobalOutlined } from "@ant-design/icons";
 import type { MenuProps } from "antd";
 
-const onClick: MenuProps["onClick"] = (e) => {
-	console.log("click ", e);
-};
-
-const menu = (
-	<Menu
-		onClick={onClick}
-		items={[
-			{
-				key: "en",
-				label: "English",
-			},
-			{
-				key: "zh-Hans",
-				label: "简体中文",
-			},
-			{
-				key: "zh-Hant",
-				label: "繁体中文",
-			},
-		]}
-	/>
-);
+const items: MenuProps["items"] = [
+  {
+    key: "en",
+    label: "English",
+  },
+  {
+    key: "zh-Hans",
+    label: "简体中文",
+  },
+  {
+    key: "zh-Hant",
+    label: "繁体中文",
+  },
+];
 const Widget = () => {
-	// TODO
-	return (
-		<Dropdown overlay={menu} placement="bottomRight">
-			<Button ghost icon={<GlobalOutlined />}>
-				简体中文
-			</Button>
-		</Dropdown>
-	);
+  // TODO
+  return (
+    <Dropdown menu={{ items }} placement="bottomRight">
+      <Button
+        ghost
+        icon={<GlobalOutlined />}
+        onClick={(e) => {
+          e.preventDefault();
+        }}
+      >
+        简体中文
+      </Button>
+    </Dropdown>
+  );
 };
 
 export default Widget;

+ 80 - 19
dashboard/src/components/library/HeadBar.tsx

@@ -1,5 +1,5 @@
 import { Link } from "react-router-dom";
-import { Layout, Col, Row, Space, Button } from "antd";
+import { Layout, Col, Row, Space } from "antd";
 import { useIntl } from "react-intl";
 import type { MenuProps } from "antd";
 import { Menu } from "antd";
@@ -7,6 +7,7 @@ import { Menu } from "antd";
 import img_banner from "../../assets/library/images/wikipali_logo_library.svg";
 import UiLangSelect from "../general/UiLangSelect";
 import SignInAvatar from "../auth/SignInAvatar";
+import ToStudio from "../auth/ToStudio";
 
 const { Header } = Layout;
 
@@ -23,56 +24,112 @@ const Widget = ({ selectedKeys = "" }: IWidgetHeadBar) => {
 	// TODO
 	const items: MenuProps["items"] = [
 		{
-			label: <Link to="/community/list">{intl.formatMessage({ id: "columns.library.community.title" })}</Link>,
+			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>,
+			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>,
+			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>,
+			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>,
+			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">
+				<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>,
+			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
+							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>,
+					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>,
+					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>
+						<Link to="/statistics">
+							{intl.formatMessage({
+								id: "columns.library.statistics.title",
+							})}
+						</Link>
 					),
 					key: "statistics",
 				},
@@ -84,7 +141,11 @@ const Widget = ({ selectedKeys = "" }: IWidgetHeadBar) => {
 			<Row justify="space-between">
 				<Col flex="100px">
 					<Link to="/">
-						<img alt="code" style={{ height: "3em" }} src={img_banner} />
+						<img
+							alt="code"
+							style={{ height: "3em" }}
+							src={img_banner}
+						/>
 					</Link>
 				</Col>
 				<Col span={8}>
@@ -97,11 +158,11 @@ const Widget = ({ selectedKeys = "" }: IWidgetHeadBar) => {
 					/>
 				</Col>
 				<Col span={4}>
-					<a href="/studio/kosalla/">
-						<Button>译经楼</Button>
-					</a>
-					<SignInAvatar />
-					<UiLangSelect />
+					<Space>
+						<ToStudio />
+						<SignInAvatar />
+						<UiLangSelect />
+					</Space>
 				</Col>
 			</Row>
 		</Header>

+ 172 - 0
dashboard/src/components/library/article/ProTabs.tsx

@@ -0,0 +1,172 @@
+import { useRef, useState } from "react";
+import { Switch } from "antd";
+import { Radio, Space } from "antd";
+import { SettingOutlined, ShoppingCartOutlined } from "@ant-design/icons";
+import SettingArticle from "../../auth/setting/SettingArticle";
+import DictComponent from "../../dict/DictComponent";
+import { DictIcon, TermIcon } from "../../../assets/icon";
+import TermShell from "./TermShell";
+
+const setting = (
+  <>
+    <Space>
+      {"保存到用户设置"}
+      <Switch
+        defaultChecked
+        onChange={(checked) => {
+          console.log(checked);
+        }}
+      />
+    </Space>
+    <SettingArticle />
+  </>
+);
+
+const Widget = () => {
+  const [value2, setValue2] = useState("close");
+  const divSetting = useRef<HTMLDivElement>(null);
+  const divDict = useRef<HTMLDivElement>(null);
+  const divTerm = useRef<HTMLDivElement>(null);
+  const divCart = useRef<HTMLDivElement>(null);
+  const divPanel = useRef<HTMLDivElement>(null);
+  const rightBarWidth = "48px";
+  const closeAll = () => {
+    if (divPanel.current) {
+      divPanel.current.style.display = "none";
+    }
+  };
+  const openPanel = () => {
+    if (divPanel.current) {
+      divPanel.current.style.display = "block";
+    }
+  };
+  const headHeight = 64;
+  const stylePanel: React.CSSProperties = {
+    height: `calc(100vh - ${headHeight})`,
+    overflowY: "scroll",
+  };
+  return (
+    <div style={{ display: "flex" }}>
+      <div ref={divPanel} style={{ width: 350, display: "none" }}>
+        <div ref={divSetting} style={stylePanel}>
+          {setting}
+        </div>
+        <div ref={divDict} style={stylePanel}>
+          <DictComponent />
+        </div>
+        <div ref={divTerm} style={stylePanel}>
+          <TermShell />
+        </div>
+        <div ref={divCart} style={stylePanel}></div>
+      </div>
+      <div
+        style={{
+          width: `${rightBarWidth}`,
+          display: "flex",
+          flexDirection: "column",
+        }}
+      >
+        <Radio.Group
+          value={value2}
+          optionType="button"
+          buttonStyle="solid"
+          onChange={(e) => {
+            console.log("radio change", e.target.value);
+            if (divSetting.current) {
+              divSetting.current.style.display = "none";
+            }
+            if (divDict.current) {
+              divDict.current.style.display = "none";
+            }
+            if (divTerm.current) {
+              divTerm.current.style.display = "none";
+            }
+            if (divCart.current) {
+              divCart.current.style.display = "none";
+            }
+            switch (e.target.value) {
+              case "setting":
+                if (divSetting.current) {
+                  divSetting.current.style.display = "block";
+                }
+                openPanel();
+                break;
+              case "dict":
+                if (divDict.current) {
+                  divDict.current.style.display = "block";
+                }
+                openPanel();
+
+                break;
+              case "term":
+                if (divTerm.current) {
+                  divTerm.current.style.display = "block";
+                }
+                openPanel();
+                break;
+              case "cart":
+                if (divCart.current) {
+                  divCart.current.style.display = "block";
+                }
+                openPanel();
+                break;
+              default:
+                break;
+            }
+            setValue2(e.target.value);
+          }}
+        >
+          <Space direction="vertical">
+            <Radio
+              value="setting"
+              onClick={() => {
+                if (value2 === "setting") {
+                  setValue2("close");
+                  closeAll();
+                }
+              }}
+            >
+              <SettingOutlined />
+            </Radio>
+            <Radio
+              value="dict"
+              onClick={() => {
+                if (value2 === "dict") {
+                  setValue2("close");
+                  closeAll();
+                }
+              }}
+            >
+              <DictIcon />
+            </Radio>
+            <Radio
+              value="term"
+              onClick={() => {
+                if (value2 === "term") {
+                  setValue2("close");
+                  closeAll();
+                }
+              }}
+            >
+              <TermIcon />
+            </Radio>
+            <Radio
+              value="cart"
+              onClick={() => {
+                if (value2 === "cart") {
+                  setValue2("close");
+                  closeAll();
+                }
+              }}
+            >
+              <ShoppingCartOutlined />
+            </Radio>
+            <Radio value="close" style={{ display: "none" }}></Radio>
+          </Space>
+        </Radio.Group>
+      </div>
+    </div>
+  );
+};
+
+export default Widget;

+ 24 - 0
dashboard/src/components/library/article/TermShell.tsx

@@ -0,0 +1,24 @@
+import { useEffect, useState } from "react";
+import { useAppSelector } from "../../../hooks";
+import { message } from "../../../reducers/command";
+
+import TermCreate, { IWidgetDictCreate } from "../../studio/term/TermCreate";
+
+const Widget = () => {
+  const [termProps, setTermProps] = useState<IWidgetDictCreate>();
+  //接收术语消息
+  const commandMsg = useAppSelector(message);
+  useEffect(() => {
+    console.log("get command", commandMsg);
+    if (commandMsg?.type === "term") {
+      setTermProps(commandMsg.prop);
+    }
+  }, [commandMsg]);
+  return (
+    <div>
+      <TermCreate type="inline" {...termProps} />
+    </div>
+  );
+};
+
+export default Widget;

+ 37 - 0
dashboard/src/components/nut/Home.tsx

@@ -1,19 +1,56 @@
 import ReactMarkdown from "react-markdown";
 import code_png from "../../assets/nut/code.png";
+import ChannelPicker from "../channel/ChannelPicker";
+import MdView from "../template/MdView";
+import { IWbw } from "../template/Wbw/WbwWord";
+import WbwSent from "../template/WbwSent";
+
 import MarkdownForm from "./MarkdownForm";
 import MarkdownShow from "./MarkdownShow";
 import FontBox from "./FontBox";
 import DemoForm from "./Form";
 
 const Widget = () => {
+  let wbwData: IWbw[] = [];
+  const valueMake = (value: string) => {
+    return { value: value, status: 3 };
+  };
+  for (let index = 0; index < 20; index++) {
+    wbwData.push({
+      word: valueMake("Word" + index),
+      real: valueMake("word" + index),
+      meaning: { value: ["意思" + index], status: 3 },
+      factors: valueMake("word+word"),
+      factorMeaning: valueMake("mean+mean"),
+      type: valueMake(".n."),
+      grammar: valueMake(".m.$.sg.$.nom."),
+      confidence: 1,
+    });
+  }
   return (
     <div>
       <h1>Home</h1>
+      <h2>wbw</h2>
+      <div style={{ width: 700 }}>
+        <WbwSent
+          data={wbwData}
+          display="inline"
+          fields={{ meaning: true, factors: false, case: false }}
+        />
+      </div>
+      <h2>channel picker</h2>
+      <div style={{ width: 1000 }}>
+        <ChannelPicker type="chapter" articleId="168-915" />
+      </div>
+      <h2>MdView test</h2>
+      <MdView html="<h1 name='h1'>hello<MdTpl name='term'/></h1>" />
+
       <br />
       <DemoForm />
       <br />
       <FontBox />
       <br />
+
       <MarkdownShow body="- Hello, **《mint》**!" />
       <br />
       <h3>Form</h3>

+ 38 - 0
dashboard/src/components/nut/InnerDrawer.tsx

@@ -0,0 +1,38 @@
+import { Button, Drawer } from "antd";
+import React, { useState } from "react";
+
+const Widget = () => {
+	const [open, setOpen] = useState(false);
+
+	const showDrawer = () => {
+		setOpen(true);
+	};
+
+	const onClose = () => {
+		setOpen(false);
+	};
+
+	return (
+		<div className="site-drawer-render-in-current-wrapper">
+			Render in this
+			<div style={{ marginTop: 16 }}>
+				<Button type="primary" onClick={showDrawer}>
+					Open
+				</Button>
+			</div>
+			<Drawer
+				title="Basic Drawer"
+				placement="right"
+				closable={false}
+				onClose={onClose}
+				open={open}
+				getContainer={false}
+				style={{ position: "absolute" }}
+			>
+				<p>Some contents...</p>
+			</Drawer>
+		</div>
+	);
+};
+
+export default Widget;

+ 65 - 0
dashboard/src/components/nut/users/ForgotPassword.tsx

@@ -0,0 +1,65 @@
+import { useIntl } from "react-intl";
+import { ProForm, ProFormText } from "@ant-design/pro-components";
+import { message } from "antd";
+
+import { post } from "../../../request";
+import { useState } from "react";
+
+interface IFormData {
+	email: string;
+}
+interface IForgotPasswordResponse {
+	ok: boolean;
+	message: string;
+	data: string;
+}
+const Widget = () => {
+	const intl = useIntl();
+	const [notify, setNotify] = useState(
+		"系统将向您的注册邮箱发送包含重置密码所需信息的链接。请输入您的注册邮箱。并确保该邮箱可以接受邮件。"
+	);
+
+	return (
+		<>
+			<div>{notify}</div>
+			<ProForm<IFormData>
+				onFinish={async (values: IFormData) => {
+					// TODO
+					console.log(values);
+					const user = {
+						email: values.email,
+					};
+					const signin = await post<
+						IFormData,
+						IForgotPasswordResponse
+					>("/v2/auth/forgotpassword", user);
+					if (signin.ok) {
+						console.log("token", signin.data);
+						setNotify("重置密码的邮件已经发送到您的邮箱。");
+						message.success(
+							intl.formatMessage({ id: "flashes.success" })
+						);
+					} else {
+						message.error(signin.message);
+					}
+				}}
+			>
+				<ProForm.Group>
+					<ProFormText
+						width="md"
+						name="email"
+						required
+						label={intl.formatMessage({
+							id: "forms.fields.email.label",
+						})}
+						rules={[
+							{ required: true, type: "email", max: 255, min: 6 },
+						]}
+					/>
+				</ProForm.Group>
+			</ProForm>
+		</>
+	);
+};
+
+export default Widget;

+ 80 - 36
dashboard/src/components/nut/users/SignIn.tsx

@@ -5,47 +5,91 @@ import { useNavigate } from "react-router-dom";
 
 import { setTitle } from "../../../reducers/layout";
 import { useAppSelector, useAppDispatch } from "../../../hooks";
-import { signIn, TO_PROFILE } from "../../../reducers/current-user";
+import { IUser, signIn, TO_HOME } from "../../../reducers/current-user";
+import { get, post } from "../../../request";
+import store from "../../../store";
 
 interface IFormData {
-  email: string;
-  password: string;
+	email: string;
+	password: string;
+}
+interface ISignInResponse {
+	ok: boolean;
+	message: string;
+	data: string;
+}
+interface IUserResponse {
+	ok: boolean;
+	message: string;
+	data: IUser;
+}
+interface ISignInRequest {
+	username: string;
+	password: string;
 }
 const Widget = () => {
-  const intl = useIntl();
-  const dispatch = useAppDispatch();
-  const navigate = useNavigate();
+	const intl = useIntl();
+	const dispatch = useAppDispatch();
+	const navigate = useNavigate();
 
-  return (
-    <ProForm<IFormData>
-      onFinish={async (values: IFormData) => {
-        // TODO
-        console.log(values);
-        // dispatch(signIn([user, token]));
-        // navigate(TO_PROFILE);
-        message.success(intl.formatMessage({ id: "flashes.success" }));
-      }}
-    >
-      <ProForm.Group>
-        <ProFormText
-          width="md"
-          name="email"
-          required
-          label={intl.formatMessage({ id: "forms.fields.email.label" })}
-          rules={[{ required: true, type: "email", max: 255, min: 6 }]}
-        />
-      </ProForm.Group>
-      <ProForm.Group>
-        <ProFormText
-          width="md"
-          name="password"
-          required
-          label={intl.formatMessage({ id: "forms.fields.password.label" })}
-          rules={[{ required: true, max: 32, min: 8 }]}
-        />
-      </ProForm.Group>
-    </ProForm>
-  );
+	return (
+		<ProForm<IFormData>
+			onFinish={async (values: IFormData) => {
+				// TODO
+				console.log(values);
+				const user = {
+					username: values.email,
+					password: values.password,
+				};
+				const signin = await post<ISignInRequest, ISignInResponse>(
+					"/v2/auth/signin",
+					user
+				);
+				if (signin.ok) {
+					console.log("token", signin.data);
+					localStorage.setItem("token", signin.data);
+					get<IUserResponse>("/v2/auth/current").then((json) => {
+						if (json.ok) {
+							dispatch(signIn([json.data, signin.data]));
+							navigate(TO_HOME);
+						} else {
+							console.error(json.message);
+						}
+					});
+					message.success(
+						intl.formatMessage({ id: "flashes.success" })
+					);
+				} else {
+					message.error(signin.message);
+				}
+			}}
+		>
+			<ProForm.Group>
+				<ProFormText
+					width="md"
+					name="email"
+					required
+					label={intl.formatMessage({
+						id: "forms.fields.email.label",
+					})}
+					rules={[
+						{ required: true, type: "email", max: 255, min: 6 },
+					]}
+				/>
+			</ProForm.Group>
+			<ProForm.Group>
+				<ProFormText
+					width="md"
+					name="password"
+					required
+					label={intl.formatMessage({
+						id: "forms.fields.password.label",
+					})}
+					rules={[{ required: true, max: 32, min: 4 }]}
+				/>
+			</ProForm.Group>
+		</ProForm>
+	);
 };
 
 export default Widget;

+ 9 - 5
dashboard/src/components/studio/Confidence.tsx

@@ -1,10 +1,7 @@
-import { Slider } from "antd";
+import { ProFormSlider } from "@ant-design/pro-components";
 import type { SliderMarks } from "antd/es/slider";
 import { useIntl } from "react-intl";
 
-const onChange = (value: string) => {
-	console.log(`selected ${value}`);
-};
 type IWidgetConfidence = {
 	defaultValue?: number;
 };
@@ -17,7 +14,14 @@ const Widget = ({ defaultValue = 75 }: IWidgetConfidence) => {
 		75: intl.formatMessage({ id: "forms.fields.confidence.75.label" }),
 		100: intl.formatMessage({ id: "forms.fields.confidence.100.label" }),
 	};
-	return <Slider marks={marks} defaultValue={defaultValue} />;
+	return (
+		<ProFormSlider
+			name="confidence"
+			label={intl.formatMessage({ id: "forms.fields.confidence.label" })}
+			width="xl"
+			marks={marks}
+		/>
+	);
 };
 
 export default Widget;

+ 55 - 9
dashboard/src/components/studio/EditableTree.tsx

@@ -1,6 +1,7 @@
 import React, { useState } from "react";
 import { Tree } from "antd";
 import type { DataNode, TreeProps } from "antd/es/tree";
+import { useEffect } from "react";
 
 type TreeNodeData = {
 	key: string;
@@ -19,7 +20,12 @@ function tocGetTreeData(articles: ListNodeData[], active = "") {
 	let treeData = [];
 
 	let treeParents = [];
-	let rootNode: TreeNodeData = { key: "0", title: "root", level: 0, children: [] };
+	let rootNode: TreeNodeData = {
+		key: "0",
+		title: "root",
+		level: 0,
+		children: [],
+	};
 	treeData.push(rootNode);
 	let lastInsNode: TreeNodeData = rootNode;
 
@@ -27,7 +33,12 @@ function tocGetTreeData(articles: ListNodeData[], active = "") {
 	for (let index = 0; index < articles.length; index++) {
 		const element = articles[index];
 
-		let newNode: TreeNodeData = { key: element.key, title: element.title, children: [], level: element.level };
+		let newNode: TreeNodeData = {
+			key: element.key,
+			title: element.title,
+			children: [],
+			level: element.level,
+		};
 		/*
 		if (active == element.article) {
 			newNode["extraClasses"] = "active";
@@ -56,7 +67,6 @@ function tocGetTreeData(articles: ListNodeData[], active = "") {
 		if (active === element.key) {
 			tocActivePath = [];
 			for (let index = 1; index < treeParents.length; index++) {
-				//treeParents[index]["expanded"] = true;
 				tocActivePath.push(treeParents[index]);
 			}
 		}
@@ -64,15 +74,48 @@ function tocGetTreeData(articles: ListNodeData[], active = "") {
 	return treeData[0].children;
 }
 
-type IWidgetEditableTree = {
+function treeToList(treeNode: TreeNodeData[]): ListNodeData[] {
+	let iTocTreeCurrLevel = 1;
+
+	let arrTocTree: ListNodeData[] = [];
+
+	for (const iterator of treeNode) {
+		getTreeNodeData(iterator);
+	}
+
+	function getTreeNodeData(node: TreeNodeData) {
+		let children = 0;
+		if (typeof node.children != "undefined") {
+			children = node.children.length;
+		}
+		arrTocTree.push({
+			key: node.key,
+			title: node.title,
+			level: iTocTreeCurrLevel,
+		});
+		if (children > 0) {
+			iTocTreeCurrLevel++;
+			for (const iterator of node.children) {
+				getTreeNodeData(iterator);
+			}
+			iTocTreeCurrLevel--;
+		}
+	}
+
+	return arrTocTree;
+}
+interface IWidgetEditableTree {
 	treeData: ListNodeData[];
-};
+	onChange?: Function;
+}
 const Widget = (prop: IWidgetEditableTree) => {
 	const data = tocGetTreeData(prop.treeData);
 	console.log("treedata", data);
 	const [gData, setGData] = useState(data);
-
-	//const [expandedKeys] = useState(["0-0", "0-0-0", "0-0-0-0"]);
+	useEffect(() => {
+		const data = tocGetTreeData(prop.treeData);
+		setGData(data);
+	}, [prop]);
 
 	const onDragEnter: TreeProps["onDragEnter"] = (info) => {
 		console.log(info);
@@ -85,7 +128,8 @@ const Widget = (prop: IWidgetEditableTree) => {
 		const dropKey = info.node.key;
 		const dragKey = info.dragNode.key;
 		const dropPos = info.node.pos.split("-");
-		const dropPosition = info.dropPosition - Number(dropPos[dropPos.length - 1]);
+		const dropPosition =
+			info.dropPosition - Number(dropPos[dropPos.length - 1]);
 
 		const loop = (
 			data: DataNode[],
@@ -143,13 +187,15 @@ const Widget = (prop: IWidgetEditableTree) => {
 			}
 		}
 		setGData(data);
+		if (typeof prop.onChange !== "undefined") {
+			prop.onChange(treeToList(data));
+		}
 	};
 
 	return (
 		<>
 			<Tree
 				rootClassName="draggable-tree"
-				//defaultExpandedKeys={expandedKeys}
 				draggable
 				blockNode
 				onDragEnter={onDragEnter}

+ 20 - 0
dashboard/src/components/studio/GoBack.tsx

@@ -0,0 +1,20 @@
+import { Button, Space } from "antd";
+import { Link } from "react-router-dom";
+import { ArrowLeftOutlined } from "@ant-design/icons";
+
+interface IWidgetGoBack {
+	to: string;
+	title?: string;
+}
+const Widget = (prop: IWidgetGoBack) => {
+	return (
+		<Space>
+			<Link to={prop.to}>
+				<Button shape="circle" icon={<ArrowLeftOutlined />} />
+			</Link>
+			<span>{prop.title}</span>
+		</Space>
+	);
+};
+
+export default Widget;

+ 17 - 8
dashboard/src/components/studio/HeadBar.tsx

@@ -1,9 +1,10 @@
 import { Link } from "react-router-dom";
-import { Col, Row, Input, Layout, Button } from "antd";
+import { Col, Row, Input, Layout, Space } from "antd";
 
 import img_banner from "../../assets/studio/images/wikipali_banner.svg";
 import UiLangSelect from "../general/UiLangSelect";
 import SignInAvatar from "../auth/SignInAvatar";
+import ToLibaray from "../auth/ToLibaray";
 
 const { Search } = Input;
 const { Header } = Layout;
@@ -16,18 +17,26 @@ const Widget = () => {
 			<Row justify="space-between">
 				<Col flex="80px">
 					<Link to="/">
-						<img alt="code" style={{ height: "3em" }} src={img_banner} />
+						<img
+							alt="code"
+							style={{ height: "3em" }}
+							src={img_banner}
+						/>
 					</Link>
 				</Col>
 				<Col span={8}>
-					<Search placeholder="input search text" onSearch={onSearch} style={{ width: "100%" }} />
+					<Search
+						placeholder="input search text"
+						onSearch={onSearch}
+						style={{ width: "100%" }}
+					/>
 				</Col>
 				<Col span={4}>
-					<Link to="\">
-						<Button>藏经阁</Button>
-					</Link>
-					<SignInAvatar />
-					<UiLangSelect />
+					<Space>
+						<ToLibaray />
+						<SignInAvatar />
+						<UiLangSelect />
+					</Space>
 				</Col>
 			</Row>
 		</Header>

+ 42 - 0
dashboard/src/components/studio/LangSelect.tsx

@@ -0,0 +1,42 @@
+import { ProFormSelect } from "@ant-design/pro-components";
+import { useIntl } from "react-intl";
+
+const Widget = () => {
+  const intl = useIntl();
+
+  const langOptions = [
+    {
+      value: "English",
+      label: "en-US",
+    },
+    {
+      value: "zh-Hans",
+      label: "简体中文 zh-Hans",
+    },
+    {
+      value: "zh-Hant",
+      label: "繁体中文 zh-Hant",
+    },
+  ];
+  return (
+    <ProFormSelect
+      options={langOptions}
+      width="sm"
+      name="lang"
+      showSearch
+      debounceTime={300}
+      allowClear={false}
+      label={intl.formatMessage({ id: "forms.fields.lang.label" })}
+      rules={[
+        {
+          required: true,
+          message: intl.formatMessage({
+            id: "forms.message.lang.required",
+          }),
+        },
+      ]}
+    />
+  );
+};
+
+export default Widget;

+ 39 - 0
dashboard/src/components/studio/PublicitySelect.tsx

@@ -0,0 +1,39 @@
+import { ProFormSelect } from "@ant-design/pro-components";
+import { useIntl } from "react-intl";
+
+const Widget = () => {
+	const intl = useIntl();
+
+	const options = [
+		{
+			value: 0,
+			label: intl.formatMessage({
+				id: "forms.fields.publicity.disable.label",
+			}),
+		},
+		{
+			value: 10,
+			label: intl.formatMessage({
+				id: "forms.fields.publicity.private.label",
+			}),
+		},
+		{
+			value: 30,
+			label: intl.formatMessage({
+				id: "forms.fields.publicity.public.label",
+			}),
+		},
+	];
+	return (
+		<ProFormSelect
+			options={options}
+			initialValue={10}
+			width="sm"
+			name="status"
+			allowClear={false}
+			label={intl.formatMessage({ id: "forms.fields.publicity.label" })}
+		/>
+	);
+};
+
+export default Widget;

+ 234 - 86
dashboard/src/components/studio/SelectCase.tsx

@@ -2,95 +2,243 @@ import { Cascader } from "antd";
 import { useIntl } from "react-intl";
 
 interface CascaderOption {
-	value: string | number;
-	label: string;
-	children?: CascaderOption[];
+  value: string | number;
+  label: string;
+  children?: CascaderOption[];
 }
+interface IWidget {
+  defaultValue?: string[];
+}
+const Widget = ({ defaultValue }: IWidget) => {
+  const intl = useIntl();
 
-const Widget = () => {
-	const intl = useIntl();
-	const case8 = [
-		{
-			value: "nom",
-			label: intl.formatMessage({ id: "dict.fields.type.nom.label" }),
-		},
-		{
-			value: "acc",
-			label: intl.formatMessage({ id: "dict.fields.type.acc.label" }),
-		},
-		{
-			value: "gen",
-			label: intl.formatMessage({ id: "dict.fields.type.gen.label" }),
-		},
-		{
-			value: "dat",
-			label: intl.formatMessage({ id: "dict.fields.type.dat.label" }),
-		},
-		{
-			value: "inst",
-			label: intl.formatMessage({ id: "dict.fields.type.inst.label" }),
-		},
-		{
-			value: "abl",
-			label: intl.formatMessage({ id: "dict.fields.type.abl.label" }),
-		},
-		{
-			value: "voc",
-			label: intl.formatMessage({ id: "dict.fields.type.voc.label" }),
-		},
-	];
-	const case2 = [
-		{
-			value: "sg",
-			label: intl.formatMessage({ id: "dict.fields.type.sg.label" }),
-			children: case8,
-		},
-		{
-			value: "pl",
-			label: intl.formatMessage({ id: "dict.fields.type.pl.label" }),
-			children: case8,
-		},
-		{
-			value: "base",
-			label: intl.formatMessage({ id: "dict.fields.type.base.label" }),
-		},
-	];
-	const case3 = [
-		{
-			value: "m",
-			label: intl.formatMessage({ id: "dict.fields.type.m.label" }),
-			children: case2,
-		},
-		{
-			value: "nt",
-			label: intl.formatMessage({ id: "dict.fields.type.nt.label" }),
-			children: case2,
-		},
-		{
-			value: "f",
-			label: intl.formatMessage({ id: "dict.fields.type.f.label" }),
-			children: case2,
-		},
-	];
-	const options: CascaderOption[] = [
-		{
-			value: ".n.",
-			label: intl.formatMessage({ id: "dict.fields.type.n.label" }),
-			children: case3,
-		},
-		{
-			value: ".ti.",
-			label: intl.formatMessage({ id: "dict.fields.type.ti.label" }),
-			children: case3,
-		},
-		{
-			value: ".v.",
-			label: intl.formatMessage({ id: "dict.fields.type.v.label" }),
-			children: case3,
-		},
-	];
+  const case8 = [
+    {
+      value: "nom",
+      label: intl.formatMessage({ id: "dict.fields.type.nom.label" }),
+    },
+    {
+      value: "acc",
+      label: intl.formatMessage({ id: "dict.fields.type.acc.label" }),
+    },
+    {
+      value: "gen",
+      label: intl.formatMessage({ id: "dict.fields.type.gen.label" }),
+    },
+    {
+      value: "dat",
+      label: intl.formatMessage({ id: "dict.fields.type.dat.label" }),
+    },
+    {
+      value: "inst",
+      label: intl.formatMessage({ id: "dict.fields.type.inst.label" }),
+    },
+    {
+      value: "abl",
+      label: intl.formatMessage({ id: "dict.fields.type.abl.label" }),
+    },
+    {
+      value: "voc",
+      label: intl.formatMessage({ id: "dict.fields.type.voc.label" }),
+    },
+  ];
+  const case2 = [
+    {
+      value: "sg",
+      label: intl.formatMessage({ id: "dict.fields.type.sg.label" }),
+      children: case8,
+    },
+    {
+      value: "pl",
+      label: intl.formatMessage({ id: "dict.fields.type.pl.label" }),
+      children: case8,
+    },
+    {
+      value: "base",
+      label: intl.formatMessage({ id: "dict.fields.type.base.label" }),
+    },
+  ];
+  const case3 = [
+    {
+      value: "m",
+      label: intl.formatMessage({ id: "dict.fields.type.m.label" }),
+      children: case2,
+    },
+    {
+      value: "nt",
+      label: intl.formatMessage({ id: "dict.fields.type.nt.label" }),
+      children: case2,
+    },
+    {
+      value: "f",
+      label: intl.formatMessage({ id: "dict.fields.type.f.label" }),
+      children: case2,
+    },
+  ];
+  const caseVerb3 = [
+    {
+      value: "pres",
+      label: intl.formatMessage({ id: "dict.fields.type.pres.label" }),
+    },
+    {
+      value: "aor",
+      label: intl.formatMessage({ id: "dict.fields.type.aor.label" }),
+    },
+    {
+      value: "fut",
+      label: intl.formatMessage({ id: "dict.fields.type.fut.label" }),
+    },
+    {
+      value: "pf",
+      label: intl.formatMessage({ id: "dict.fields.type.pf.label" }),
+    },
+    {
+      value: "imp",
+      label: intl.formatMessage({ id: "dict.fields.type.imp.label" }),
+    },
+    {
+      value: "cond",
+      label: intl.formatMessage({ id: "dict.fields.type.cond.label" }),
+    },
+    {
+      value: "opt",
+      label: intl.formatMessage({ id: "dict.fields.type.opt.label" }),
+    },
+  ];
+  const caseVerb2 = [
+    {
+      value: "sg",
+      label: intl.formatMessage({ id: "dict.fields.type.sg.label" }),
+      children: caseVerb3,
+    },
+    {
+      value: "pl",
+      label: intl.formatMessage({ id: "dict.fields.type.pl.label" }),
+      children: caseVerb3,
+    },
+  ];
+  const caseVerbInd = [
+    {
+      value: "abs",
+      label: intl.formatMessage({ id: "dict.fields.type.abs.label" }),
+    },
+    {
+      value: "ger",
+      label: intl.formatMessage({ id: "dict.fields.type.ger.label" }),
+    },
+    {
+      value: "inf",
+      label: intl.formatMessage({ id: "dict.fields.type.inf.label" }),
+    },
+  ];
+  const caseInd = [
+    {
+      value: "ind",
+      label: intl.formatMessage({ id: "dict.fields.type.ind.label" }),
+    },
+    {
+      value: "adv",
+      label: intl.formatMessage({ id: "dict.fields.type.adv.label" }),
+    },
+    {
+      value: "conj",
+      label: intl.formatMessage({ id: "dict.fields.type.conj.label" }),
+    },
+    {
+      value: "prep",
+      label: intl.formatMessage({ id: "dict.fields.type.prep.label" }),
+    },
+    {
+      value: "interj",
+      label: intl.formatMessage({ id: "dict.fields.type.interj.label" }),
+    },
+    {
+      value: "pre",
+      label: intl.formatMessage({ id: "dict.fields.type.pre.label" }),
+    },
+    {
+      value: "suf",
+      label: intl.formatMessage({ id: "dict.fields.type.suf.label" }),
+    },
+    {
+      value: "end",
+      label: intl.formatMessage({ id: "dict.fields.type.end.label" }),
+    },
+    {
+      value: "part",
+      label: intl.formatMessage({ id: "dict.fields.type.part.label" }),
+    },
+  ];
+  const caseVerb1 = [
+    {
+      value: "1p",
+      label: intl.formatMessage({ id: "dict.fields.type.1p.label" }),
+      children: caseVerb2,
+    },
+    {
+      value: "2p",
+      label: intl.formatMessage({ id: "dict.fields.type.2p.label" }),
+      children: caseVerb2,
+    },
+    {
+      value: "3p",
+      label: intl.formatMessage({ id: "dict.fields.type.3p.label" }),
+      children: caseVerb2,
+    },
+    {
+      value: "ind",
+      label: intl.formatMessage({ id: "dict.fields.type.ind.label" }),
+      children: caseVerbInd,
+    },
+    {
+      value: "base",
+      label: intl.formatMessage({ id: "dict.fields.type.base.label" }),
+    },
+  ];
+  const options: CascaderOption[] = [
+    {
+      value: ".n.",
+      label: intl.formatMessage({ id: "dict.fields.type.n.label" }),
+      children: case3,
+    },
+    {
+      value: ".ti.",
+      label: intl.formatMessage({ id: "dict.fields.type.ti.label" }),
+      children: case3,
+    },
+    {
+      value: ".v.",
+      label: intl.formatMessage({ id: "dict.fields.type.v.label" }),
+      children: caseVerb1,
+    },
+    {
+      value: "ind",
+      label: intl.formatMessage({ id: "dict.fields.type.ind.label" }),
+      children: caseInd,
+    },
+    {
+      value: "un",
+      label: intl.formatMessage({ id: "dict.fields.type.un.label" }),
+    },
+    {
+      value: "adj",
+      label: intl.formatMessage({ id: "dict.fields.type.adj.label" }),
+      children: case3,
+    },
+  ];
+  type SingleValueType = (string | number)[];
+  const onChange = (value: SingleValueType) => {
+    console.log(value);
+  };
 
-	return <Cascader options={options} placeholder="Please select case" />;
+  return (
+    <Cascader
+      options={options}
+      defaultValue={defaultValue}
+      placeholder="Please select case"
+      onChange={onChange}
+    />
+  );
 };
 
 export default Widget;

+ 44 - 28
dashboard/src/components/studio/SelectLang.tsx

@@ -4,40 +4,56 @@ import { useIntl } from "react-intl";
 const { Option } = Select;
 
 const onLangChange = (value: string) => {
-	console.log(`selected ${value}`);
+  console.log(`selected ${value}`);
 };
 
 const onLangSearch = (value: string) => {
-	console.log("search:", value);
+  console.log("search:", value);
 };
 
-const Widget = () => {
-	const intl = useIntl();
+interface IWidgetSelectLang {
+  lang?: string;
+}
+const Widget = (prop: IWidgetSelectLang) => {
+  const intl = useIntl();
 
-	const data = [
-		{ value: "en", lable: intl.formatMessage({ id: "languages.en-US" }) },
-		{ value: "zh-Hans", lable: intl.formatMessage({ id: "languages.zh-Hans" }) },
-		{ value: "zh-Hant", lable: intl.formatMessage({ id: "languages.zh-Hant" }) },
-	];
-	const langOptions = data.map((d, id) => (
-		<Option key={id} value={d.value}>
-			{d.lable}
-		</Option>
-	));
-	return (
-		<Select
-			showSearch
-			placeholder="Select a language"
-			optionFilterProp="children"
-			onChange={onLangChange}
-			onSearch={onLangSearch}
-			filterOption={(input, option) =>
-				(option!.children as unknown as string).toLowerCase().includes(input.toLowerCase())
-			}
-		>
-			{langOptions}
-		</Select>
-	);
+  const data = [
+    { value: "en", label: intl.formatMessage({ id: "languages.en-US" }) },
+    {
+      value: "zh-Hans",
+      label: intl.formatMessage({ id: "languages.zh-Hans" }),
+    },
+    {
+      value: "zh-Hant",
+      label: intl.formatMessage({ id: "languages.zh-Hant" }),
+    },
+    {
+      value: "zh",
+      label: intl.formatMessage({ id: "languages.zh" }),
+    },
+  ];
+  const langOptions = data.map((d, id) => (
+    <Option key={id} value={d.value}>
+      {d.label}
+    </Option>
+  ));
+  return (
+    <Select
+      showSearch
+      placeholder="Select a language"
+      optionFilterProp="children"
+      onChange={onLangChange}
+      onSearch={onLangSearch}
+      value={prop.lang ? prop.lang : ""}
+      filterOption={(input, option) =>
+        (option!.children as unknown as string)
+          .toLowerCase()
+          .includes(input.toLowerCase())
+      }
+    >
+      {langOptions}
+    </Select>
+  );
 };
 
 export default Widget;

+ 32 - 21
dashboard/src/components/studio/anthology/AnthologyCreate.tsx

@@ -1,24 +1,44 @@
-import { ProForm, ProFormText, ProFormSelect } from "@ant-design/pro-components";
+import {
+	ProForm,
+	ProFormText,
+	ProFormSelect,
+} from "@ant-design/pro-components";
 import { useIntl } from "react-intl";
 import { message } from "antd";
+import LangSelect from "../LangSelect";
+import { IAnthologyCreateRequest, IAnthologyResponse } from "../../api/Article";
+import { post } from "../../../request";
 
 interface IFormData {
 	title: string;
 	lang: string;
+	studio: string;
 }
 
 type IWidgetAnthologyCreate = {
-	studio: string | undefined;
+	studio?: string;
 };
-const Widget = (param: IWidgetAnthologyCreate) => {
+const Widget = (prop: IWidgetAnthologyCreate) => {
 	const intl = useIntl();
 
 	return (
 		<ProForm<IFormData>
 			onFinish={async (values: IFormData) => {
 				// TODO
+				values.studio = prop.studio ? prop.studio : "";
 				console.log(values);
-				message.success(intl.formatMessage({ id: "flashes.success" }));
+				const res = await post<
+					IAnthologyCreateRequest,
+					IAnthologyResponse
+				>(`/v2/anthology`, values);
+				console.log(res);
+				if (res.ok) {
+					message.success(
+						intl.formatMessage({ id: "flashes.success" })
+					);
+				} else {
+					message.error(res.message);
+				}
 			}}
 		>
 			<ProForm.Group>
@@ -26,31 +46,22 @@ const Widget = (param: IWidgetAnthologyCreate) => {
 					width="md"
 					name="title"
 					required
-					label={intl.formatMessage({ id: "channel.name" })}
+					label={intl.formatMessage({
+						id: "forms.fields.title.label",
+					})}
 					rules={[
 						{
 							required: true,
-							message: intl.formatMessage({ id: "channel.create.message.noname" }),
+							message: intl.formatMessage({
+								id: "forms.message.title.required",
+							}),
+							max: 255,
 						},
 					]}
 				/>
 			</ProForm.Group>
 			<ProForm.Group>
-				<ProFormSelect
-					options={[
-						{
-							value: "zh-Hans",
-							label: intl.formatMessage({ id: "languages.zh-Hans" }),
-						},
-						{
-							value: "en-US",
-							label: intl.formatMessage({ id: "English" }),
-						},
-					]}
-					width="md"
-					name="lang"
-					label={intl.formatMessage({ id: "forms.fields.lang.label" })}
-				/>
+				<LangSelect />
 			</ProForm.Group>
 		</ProForm>
 	);

+ 24 - 21
dashboard/src/components/studio/article/ArticleCreate.tsx

@@ -1,24 +1,39 @@
-import { ProForm, ProFormText, ProFormSelect } from "@ant-design/pro-components";
+import { ProForm, ProFormText } from "@ant-design/pro-components";
 import { useIntl } from "react-intl";
 import { message } from "antd";
+import LangSelect from "../LangSelect";
+import { post } from "../../../request";
+import { IArticleCreateRequest, IArticleResponse } from "../../api/Article";
 
 interface IFormData {
 	title: string;
 	lang: string;
+	studio: string;
 }
 
 type IWidgetArticleCreate = {
-	studio: string | undefined;
+	studio?: string;
 };
-const Widget = (param: IWidgetArticleCreate) => {
+const Widget = (prop: IWidgetArticleCreate) => {
 	const intl = useIntl();
 
 	return (
 		<ProForm<IFormData>
 			onFinish={async (values: IFormData) => {
-				// TODO
 				console.log(values);
-				message.success(intl.formatMessage({ id: "flashes.success" }));
+				values.studio = prop.studio ? prop.studio : "";
+				const res = await post<IArticleCreateRequest, IArticleResponse>(
+					`/v2/article`,
+					values
+				);
+				console.log(res);
+				if (res.ok) {
+					message.success(
+						intl.formatMessage({ id: "flashes.success" })
+					);
+				} else {
+					message.error(res.message);
+				}
 			}}
 		>
 			<ProForm.Group>
@@ -30,27 +45,15 @@ const Widget = (param: IWidgetArticleCreate) => {
 					rules={[
 						{
 							required: true,
-							message: intl.formatMessage({ id: "channel.create.message.noname" }),
+							message: intl.formatMessage({
+								id: "channel.create.message.noname",
+							}),
 						},
 					]}
 				/>
 			</ProForm.Group>
 			<ProForm.Group>
-				<ProFormSelect
-					options={[
-						{
-							value: "zh-Hans",
-							label: intl.formatMessage({ id: "languages.zh-Hans" }),
-						},
-						{
-							value: "en-US",
-							label: intl.formatMessage({ id: "English" }),
-						},
-					]}
-					width="md"
-					name="lang"
-					label={intl.formatMessage({ id: "forms.fields.lang.label" })}
-				/>
+				<LangSelect />
 			</ProForm.Group>
 		</ProForm>
 	);

+ 26 - 19
dashboard/src/components/studio/channel/ChannelCreate.tsx

@@ -1,16 +1,22 @@
-import { ProForm, ProFormText, ProFormSelect } from "@ant-design/pro-components";
+import { ProForm, ProFormText } from "@ant-design/pro-components";
 import { useIntl } from "react-intl";
 import { message } from "antd";
+import ChannelTypeSelect from "./ChannelTypeSelect";
+import { post } from "../../../request";
+import { IApiResponseChannel } from "../../api/Channel";
+import LangSelect from "../LangSelect";
 
 interface IFormData {
 	name: string;
 	type: string;
+	lang: string;
+	studio: string;
 }
 
 type IWidgetChannelCreate = {
 	studio: string | undefined;
 };
-const Widget = (param: IWidgetChannelCreate) => {
+const Widget = (prop: IWidgetChannelCreate) => {
 	const intl = useIntl();
 
 	return (
@@ -18,7 +24,19 @@ const Widget = (param: IWidgetChannelCreate) => {
 			onFinish={async (values: IFormData) => {
 				// TODO
 				console.log(values);
-				message.success(intl.formatMessage({ id: "flashes.success" }));
+				values.studio = prop.studio ? prop.studio : "";
+				const res: IApiResponseChannel = await post(
+					`/v2/channel`,
+					values
+				);
+				console.log(res);
+				if (res.ok) {
+					message.success(
+						intl.formatMessage({ id: "flashes.success" })
+					);
+				} else {
+					message.error(res.message);
+				}
 			}}
 		>
 			<ProForm.Group>
@@ -30,27 +48,16 @@ const Widget = (param: IWidgetChannelCreate) => {
 					rules={[
 						{
 							required: true,
-							message: intl.formatMessage({ id: "channel.create.message.noname" }),
+							message: intl.formatMessage({
+								id: "channel.create.message.noname",
+							}),
 						},
 					]}
 				/>
 			</ProForm.Group>
 			<ProForm.Group>
-				<ProFormSelect
-					options={[
-						{
-							value: "translation",
-							label: intl.formatMessage({ id: "channel.type.translation.title" }),
-						},
-						{
-							value: "nissaya",
-							label: intl.formatMessage({ id: "channel.type.nissaya.title" }),
-						},
-					]}
-					width="md"
-					name="type"
-					label={intl.formatMessage({ id: "channel.type" })}
-				/>
+				<ChannelTypeSelect />
+				<LangSelect />
 			</ProForm.Group>
 		</ProForm>
 	);

+ 49 - 0
dashboard/src/components/studio/channel/ChannelTypeSelect.tsx

@@ -0,0 +1,49 @@
+import { ProFormSelect } from "@ant-design/pro-components";
+import { useIntl } from "react-intl";
+
+const Widget = () => {
+	const intl = useIntl();
+
+	const channelTypeOptions = [
+		{
+			value: "translation",
+			label: intl.formatMessage({ id: "channel.type.translation.label" }),
+		},
+		{
+			value: "nissaya",
+			label: intl.formatMessage({ id: "channel.type.nissaya.label" }),
+		},
+		{
+			value: "commentary",
+			label: intl.formatMessage({ id: "channel.type.commentary.label" }),
+		},
+		{
+			value: "original",
+			label: intl.formatMessage({ id: "channel.type.original.label" }),
+		},
+		{
+			value: "general",
+			label: intl.formatMessage({ id: "channel.type.general.label" }),
+		},
+	];
+	return (
+		<ProFormSelect
+			options={channelTypeOptions}
+			initialValue="translation"
+			width="xs"
+			name="type"
+			allowClear={false}
+			label={intl.formatMessage({ id: "channel.type" })}
+			rules={[
+				{
+					required: true,
+					message: intl.formatMessage({
+						id: "channel.type.message.required",
+					}),
+				},
+			]}
+		/>
+	);
+};
+
+export default Widget;

+ 17 - 70
dashboard/src/components/studio/dict/DictCreate.tsx

@@ -1,13 +1,12 @@
-import { ProForm, ProFormText, ProFormTextArea } from "@ant-design/pro-components";
-import { Layout } from "antd";
+import { ProForm } from "@ant-design/pro-components";
+
 import { useIntl } from "react-intl";
 import { message } from "antd";
 
-import SelectLang from "../SelectLang";
-import SelectCase from "../SelectCase";
-import Confidene from "../Confidence";
+import DictEditInner from "./DictEditInner";
 
-interface IFormData {
+export interface IDictFormData {
+	id: number;
 	word: string;
 	type: string;
 	grammar: string;
@@ -17,12 +16,14 @@ interface IFormData {
 	factors: string;
 	factormeaning: string;
 	lang: string;
+	confidence: number;
 }
 
 type IWidgetDictCreate = {
-	studio: string | undefined;
+	studio: string;
+	word?: string;
 };
-const Widget = (param: IWidgetDictCreate) => {
+const Widget = (prop: IWidgetDictCreate) => {
 	const intl = useIntl();
 	/*
 	const onLangChange = (value: string) => {
@@ -34,73 +35,19 @@ const Widget = (param: IWidgetDictCreate) => {
 	};
 	*/
 	return (
-		<Layout>
-			<ProForm<IFormData>
-				onFinish={async (values: IFormData) => {
+		<>
+			<ProForm<IDictFormData>
+				onFinish={async (values: IDictFormData) => {
 					// TODO
 					console.log(values);
-					message.success(intl.formatMessage({ id: "flashes.success" }));
+					message.success(
+						intl.formatMessage({ id: "flashes.success" })
+					);
 				}}
 			>
-				<ProForm.Group>
-					<ProFormText
-						width="md"
-						name="word"
-						required
-						label={intl.formatMessage({ id: "dict.fields.word.label" })}
-						rules={[
-							{
-								required: true,
-								message: intl.formatMessage({ id: "channel.create.message.noname" }),
-							},
-						]}
-					/>
-				</ProForm.Group>
-				<ProForm.Group>
-					<div>语法信息</div>
-					<SelectCase />
-				</ProForm.Group>
-				<ProForm.Group>
-					<ProFormText
-						width="md"
-						name="parent"
-						label={intl.formatMessage({ id: "dict.fields.parent.label" })}
-					/>
-				</ProForm.Group>
-				<ProForm.Group>
-					<div>语言</div>
-					<SelectLang />
-				</ProForm.Group>
-				<ProForm.Group>
-					<ProFormText
-						width="md"
-						name="meaning"
-						label={intl.formatMessage({ id: "dict.fields.meaning.label" })}
-					/>
-				</ProForm.Group>
-				<ProForm.Group>
-					<ProFormText
-						width="md"
-						name="factors"
-						label={intl.formatMessage({ id: "dict.fields.factors.label" })}
-					/>
-				</ProForm.Group>
-				<ProForm.Group>
-					<ProFormText
-						width="md"
-						name="factormeaning"
-						label={intl.formatMessage({ id: "dict.fields.factormeaning.label" })}
-					/>
-				</ProForm.Group>
-				<ProForm.Group>
-					<ProFormTextArea name="note" label={intl.formatMessage({ id: "forms.fields.note.label" })} />
-				</ProForm.Group>
-				<Layout>
-					<div>信心指数</div>
-					<Confidene />
-				</Layout>
+				<DictEditInner word={prop.word} />
 			</ProForm>
-		</Layout>
+		</>
 	);
 };
 

+ 78 - 0
dashboard/src/components/studio/dict/DictEdit.tsx

@@ -0,0 +1,78 @@
+import { ProForm } from "@ant-design/pro-components";
+
+import { useIntl } from "react-intl";
+import { message } from "antd";
+
+import DictEditInner from "./DictEditInner";
+import { IDictFormData } from "./DictCreate";
+import { IApiResponseDict, IDictlDataRequest } from "../../api/Dict";
+import { get, put } from "../../../request";
+import { useEffect } from "react";
+
+type IWidgetDictEdit = {
+	wordId: number;
+};
+const Widget = (prop: IWidgetDictEdit) => {
+	const intl = useIntl();
+	useEffect(() => {});
+
+	return (
+		<>
+			<ProForm<IDictFormData>
+				onFinish={async (values: IDictFormData) => {
+					// TODO
+					console.log(values);
+					const request: IDictlDataRequest = {
+						id: values.id,
+						word: values.word,
+						type: values.type,
+						grammar: values.grammar,
+						mean: values.meaning,
+						parent: values.parent,
+						note: values.note,
+						factors: values.factors,
+						factormean: values.factormeaning,
+						language: values.lang,
+						confidence: values.confidence,
+					};
+					const res = await put<IDictlDataRequest, IApiResponseDict>(
+						`/v2/userdict/${prop.wordId}`,
+						request
+					);
+					console.log(res);
+					if (res.ok) {
+						message.success(
+							intl.formatMessage({ id: "flashes.success" })
+						);
+					} else {
+						message.success(res.message);
+					}
+				}}
+				formKey="dict_edit"
+				request={async () => {
+					const res: IApiResponseDict = await get(
+						`/v2/userdict/${prop.wordId}`
+					);
+					return {
+						id: res.data.id,
+						wordId: res.data.id,
+						word: res.data.word,
+						type: res.data.type,
+						grammar: res.data.grammar,
+						parent: res.data.parent,
+						meaning: res.data.mean,
+						note: res.data.note,
+						factors: res.data.factors,
+						factormeaning: res.data.factormean,
+						lang: res.data.language,
+						confidence: res.data.confidence,
+					};
+				}}
+			>
+				<DictEditInner />
+			</ProForm>
+		</>
+	);
+};
+
+export default Widget;

+ 121 - 0
dashboard/src/components/studio/dict/DictEditInner.tsx

@@ -0,0 +1,121 @@
+import {
+	ProForm,
+	ProFormSlider,
+	ProFormText,
+	ProFormTextArea,
+} from "@ant-design/pro-components";
+
+import { useIntl } from "react-intl";
+
+import LangSelect from "../LangSelect";
+import SelectCase from "../SelectCase";
+import Confidence from "../Confidence";
+
+type IWidgetDictCreate = {
+	word?: string;
+};
+const Widget = (prop: IWidgetDictCreate) => {
+	const intl = useIntl();
+	/*
+	const onLangChange = (value: string) => {
+		console.log(`selected ${value}`);
+	};
+
+	const onLangSearch = (value: string) => {
+		console.log("search:", value);
+	};
+	*/
+	return (
+		<>
+			<ProForm.Group>
+				<ProFormText
+					width="md"
+					name="word"
+					initialValue={prop.word}
+					required
+					label={intl.formatMessage({ id: "dict.fields.word.label" })}
+					rules={[
+						{
+							required: true,
+							message: intl.formatMessage({
+								id: "channel.create.message.noname",
+							}),
+						},
+					]}
+				/>
+			</ProForm.Group>
+			<ProForm.Group>
+				<div>语法信息</div>
+				<SelectCase />
+			</ProForm.Group>
+			<ProForm.Group>
+				<ProFormText
+					width="md"
+					name="type"
+					label={intl.formatMessage({
+						id: "dict.fields.type.label",
+					})}
+				/>
+				<ProFormText
+					width="md"
+					name="grammar"
+					label={intl.formatMessage({
+						id: "dict.fields.grammar.label",
+					})}
+				/>
+			</ProForm.Group>
+			<ProForm.Group>
+				<ProFormText
+					width="md"
+					name="parent"
+					label={intl.formatMessage({
+						id: "dict.fields.parent.label",
+					})}
+				/>
+			</ProForm.Group>
+			<ProForm.Group>
+				<LangSelect />
+			</ProForm.Group>
+			<ProForm.Group>
+				<ProFormText
+					width="md"
+					name="meaning"
+					label={intl.formatMessage({
+						id: "dict.fields.meaning.label",
+					})}
+				/>
+			</ProForm.Group>
+			<ProForm.Group>
+				<ProFormText
+					width="md"
+					name="factors"
+					label={intl.formatMessage({
+						id: "dict.fields.factors.label",
+					})}
+				/>
+			</ProForm.Group>
+			<ProForm.Group>
+				<ProFormText
+					width="md"
+					name="factormeaning"
+					label={intl.formatMessage({
+						id: "dict.fields.factormeaning.label",
+					})}
+				/>
+			</ProForm.Group>
+			<ProForm.Group>
+				<ProFormTextArea
+					name="note"
+					label={intl.formatMessage({
+						id: "forms.fields.note.label",
+					})}
+				/>
+			</ProForm.Group>
+			<ProForm.Group>
+				<Confidence />
+			</ProForm.Group>
+		</>
+	);
+};
+
+export default Widget;

+ 74 - 0
dashboard/src/components/studio/table.ts

@@ -0,0 +1,74 @@
+import { useIntl } from "react-intl";
+
+export const PublicityValueEnum = () => {
+	const intl = useIntl();
+	return {
+		all: {
+			text: intl.formatMessage({
+				id: "tables.publicity.all",
+			}),
+			status: "Default",
+		},
+		0: {
+			text: intl.formatMessage({
+				id: "tables.publicity.disable",
+			}),
+			status: "Default",
+		},
+		10: {
+			text: intl.formatMessage({
+				id: "tables.publicity.private",
+			}),
+			status: "Processing",
+		},
+		20: {
+			text: intl.formatMessage({
+				id: "tables.publicity.public.bylink",
+			}),
+			status: "Processing",
+		},
+		30: {
+			text: intl.formatMessage({
+				id: "tables.publicity.public",
+			}),
+			status: "Success",
+		},
+		40: {
+			text: intl.formatMessage({
+				id: "tables.publicity.public.edit",
+			}),
+			status: "Success",
+		},
+	};
+};
+
+export const RoleValueEnum = () => {
+	const intl = useIntl();
+	return {
+		all: {
+			text: intl.formatMessage({
+				id: "tables.role.all",
+			}),
+		},
+		owner: {
+			text: intl.formatMessage({
+				id: "tables.role.owner",
+			}),
+		},
+		manager: {
+			text: intl.formatMessage({
+				id: "tables.role.manager",
+			}),
+		},
+		editor: {
+			text: intl.formatMessage({
+				id: "tables.role.editor",
+			}),
+		},
+		member: {
+			text: intl.formatMessage({
+				id: "tables.role.member",
+			}),
+		},
+	};
+};

+ 176 - 86
dashboard/src/components/studio/term/TermCreate.tsx

@@ -1,95 +1,185 @@
-import { ProForm, ProFormText, ProFormTextArea } from "@ant-design/pro-components";
-import { Layout } from "antd";
 import { useIntl } from "react-intl";
-import { message } from "antd";
+import { useRef } from "react";
+import { Button, Form, message } from "antd";
+import {
+  ModalForm,
+  ProForm,
+  ProFormInstance,
+} from "@ant-design/pro-components";
+import { PlusOutlined } from "@ant-design/icons";
 
-import SelectLang from "../SelectLang";
+import { ITermResponse, ITermCreateResponse } from "../../api/Term";
+import { get } from "../../../request";
+
+import TermEditInner from "./TermEditInner";
 
 interface IFormData {
-	word: string;
-	tag: string;
-	meaning: string;
-	meaning2: string;
-	note: string;
-	channel: string;
-	lang: string;
+  word: string;
+  tag: string;
+  meaning: string;
+  meaning2: string;
+  note: string;
+  channel: string;
+  lang: string;
 }
 
-type IWidgetDictCreate = {
-	studio: string | undefined;
-};
-const Widget = (param: IWidgetDictCreate) => {
-	const intl = useIntl();
-	/*
-	const onLangChange = (value: string) => {
-		console.log(`selected ${value}`);
-	};
-*/
-	return (
-		<Layout>
-			<ProForm<IFormData>
-				onFinish={async (values: IFormData) => {
-					// TODO
-					console.log(values);
-					message.success(intl.formatMessage({ id: "flashes.success" }));
-				}}
-			>
-				<ProForm.Group>
-					<ProFormText
-						width="md"
-						name="word"
-						required
-						label={intl.formatMessage({ id: "dict.fields.word.label" })}
-						rules={[
-							{
-								required: true,
-								message: intl.formatMessage({ id: "channel.create.message.noname" }),
-							},
-						]}
-					/>
-				</ProForm.Group>
-				<ProForm.Group>
-					<ProFormText
-						width="md"
-						name="tag"
-						tooltip={intl.formatMessage({ id: "term.fields.description.tooltip" })}
-						label={intl.formatMessage({ id: "term.fields.description.label" })}
-					/>
-				</ProForm.Group>
-				<ProForm.Group>
-					<ProFormText
-						width="md"
-						name="meaning"
-						tooltip={intl.formatMessage({ id: "term.fields.meaning.tooltip" })}
-						label={intl.formatMessage({ id: "term.fields.meaning.label" })}
-					/>
-				</ProForm.Group>
-				<ProForm.Group>
-					<ProFormText
-						width="md"
-						name="meaning2"
-						tooltip={intl.formatMessage({ id: "term.fields.meaning2.tooltip" })}
-						label={intl.formatMessage({ id: "term.fields.meaning2.label" })}
-					/>
-				</ProForm.Group>
-				<ProForm.Group>
-					<ProFormText
-						width="md"
-						name="channel"
-						tooltip={intl.formatMessage({ id: "term.fields.channel.tooltip" })}
-						label={intl.formatMessage({ id: "term.fields.channel.label" })}
-					/>
-				</ProForm.Group>
-				<ProForm.Group>
-					<div>语言</div>
-					<SelectLang />
-				</ProForm.Group>
-				<ProForm.Group>
-					<ProFormTextArea name="note" label={intl.formatMessage({ id: "forms.fields.note.label" })} />
-				</ProForm.Group>
-			</ProForm>
-		</Layout>
-	);
+export interface IWidgetDictCreate {
+  studio?: string;
+  isCreate?: boolean;
+  wordId?: string;
+  word?: string;
+  channel?: string;
+  type?: "inline" | "modal";
+}
+const Widget = ({
+  studio,
+  isCreate,
+  wordId,
+  word,
+  channel,
+  type = "modal",
+}: IWidgetDictCreate) => {
+  const intl = useIntl();
+  const [form] = Form.useForm<IFormData>();
+  const formRef = useRef<ProFormInstance>();
+  console.log("term render");
+  const waitTime = (time: number = 100) => {
+    return new Promise((resolve) => {
+      setTimeout(() => {
+        resolve(true);
+      }, time);
+    });
+  };
+  const editTrigger = (
+    <span>
+      {intl.formatMessage({
+        id: "buttons.edit",
+      })}
+    </span>
+  );
+  const createTrigger = (
+    <Button type="primary" icon={<PlusOutlined />}>
+      {intl.formatMessage({
+        id: "buttons.create",
+      })}
+    </Button>
+  );
+
+  const onFinish = async (values: IFormData) => {
+    await waitTime(2000);
+    console.log(values.word);
+    message.success("提交成功");
+    return true;
+  };
+  const request = async () => {
+    console.log("request");
+    let url: string;
+    let data: IFormData = {
+      word: "",
+      tag: "",
+      meaning: "",
+      meaning2: "",
+      note: "",
+      lang: "",
+      channel: "",
+    };
+    if (typeof isCreate !== "undefined" && isCreate === false) {
+      // 如果是编辑,就从服务器拉取数据。
+      url = "/v2/terms/" + (isCreate ? "" : wordId);
+      console.log(url);
+      const res = await get<ITermResponse>(url);
+      console.log(res);
+      return {
+        word: res.data.word,
+        tag: res.data.tag,
+        meaning: res.data.meaning,
+        meaning2: res.data.other_meaning,
+        note: res.data.note,
+        lang: res.data.language,
+        channel: res.data.channal,
+      };
+    } else if (typeof channel !== "undefined") {
+      //在channel新建
+      url = `/v2/terms?view=create-by-channel&channel=${channel}&word=${word}`;
+      const res = await get<ITermCreateResponse>(url);
+      console.log(res);
+      data = {
+        word: res.data.word,
+        tag: "",
+        meaning: "",
+        meaning2: "",
+        note: "",
+        lang: res.data.language,
+        channel: "",
+      };
+      return data;
+    } else if (typeof studio !== "undefined") {
+      //在studio新建
+      url = `/v2/terms?view=create-by-studio&studio=${studio}&word=${word}`;
+    } else {
+      return {
+        word: "",
+        tag: "",
+        meaning: "",
+        meaning2: "",
+        note: "",
+        lang: "",
+        channel: "",
+      };
+    }
+    return data;
+  };
+
+  const formProps = {
+    form: form,
+    autoFocusFirstInput: true,
+    onFinish: onFinish,
+    request: request,
+  };
+
+  let formTerm: JSX.Element;
+  switch (type) {
+    case "inline":
+      formTerm = (
+        <>
+          <Button
+            onClick={() => {
+              formRef?.current?.resetFields();
+            }}
+          >
+            {word}
+          </Button>
+          <ProForm<IFormData> {...formProps}>
+            <TermEditInner />
+          </ProForm>
+        </>
+      );
+      break;
+    case "modal":
+      formTerm = (
+        <>
+          <ModalForm<IFormData>
+            title={intl.formatMessage({
+              id: isCreate ? "buttons.create" : "buttons.edit",
+            })}
+            trigger={isCreate ? createTrigger : editTrigger}
+            modalProps={{
+              destroyOnClose: true,
+              onCancel: () => console.log("run"),
+            }}
+            submitTimeout={2000}
+            {...formProps}
+          >
+            <TermEditInner />
+          </ModalForm>
+        </>
+      );
+      break;
+    default:
+      formTerm = <></>;
+      break;
+  }
+  return formTerm;
 };
 
 export default Widget;

+ 122 - 0
dashboard/src/components/studio/term/TermEditInner.tsx

@@ -0,0 +1,122 @@
+import { useIntl } from "react-intl";
+
+import {
+  ProForm,
+  ProFormSelect,
+  ProFormText,
+  ProFormTextArea,
+} from "@ant-design/pro-components";
+
+import LangSelect from "../LangSelect";
+import { DefaultOptionType } from "antd/lib/select";
+
+interface IWidget {
+  meaningList?: string[];
+  channelList?: DefaultOptionType[];
+}
+const Widget = ({ meaningList, channelList }: IWidget) => {
+  const intl = useIntl();
+  return (
+    <>
+      <ProForm.Group>
+        <ProFormText
+          width="md"
+          name="word"
+          required
+          label={intl.formatMessage({
+            id: "term.fields.word.label",
+          })}
+          rules={[
+            {
+              required: true,
+              message: intl.formatMessage({
+                id: "term.message.meaning.required",
+              }),
+            },
+          ]}
+          fieldProps={{
+            showCount: true,
+            maxLength: 128,
+          }}
+        />
+        <ProFormText
+          width="md"
+          name="tag"
+          tooltip={intl.formatMessage({
+            id: "term.fields.description.tooltip",
+          })}
+          label={intl.formatMessage({
+            id: "term.fields.description.label",
+          })}
+        />
+      </ProForm.Group>
+      <ProForm.Group>
+        <ProFormText
+          width="md"
+          name="meaning"
+          tooltip={intl.formatMessage({
+            id: "term.fields.meaning.tooltip",
+          })}
+          label={intl.formatMessage({
+            id: "forms.fields.meaning.label",
+          })}
+          rules={[
+            {
+              required: true,
+              message: intl.formatMessage({
+                id: "forms.message.meaning.required",
+              }),
+            },
+          ]}
+          fieldProps={{
+            showCount: true,
+            maxLength: 128,
+          }}
+        />
+        <ProFormSelect
+          width="md"
+          name="meaning2"
+          label={intl.formatMessage({
+            id: "term.fields.meaning2.label",
+          })}
+          options={meaningList}
+          fieldProps={{
+            mode: "tags",
+            tokenSeparators: [",", ","],
+          }}
+          placeholder="Please select other meanings"
+          rules={[
+            {
+              type: "array",
+            },
+          ]}
+        />
+      </ProForm.Group>
+      <ProForm.Group>
+        <ProFormText
+          width="md"
+          name="channel"
+          tooltip={intl.formatMessage({
+            id: "term.fields.channel.tooltip",
+          })}
+          label={intl.formatMessage({
+            id: "term.fields.channel.label",
+          })}
+        />
+
+        <LangSelect />
+      </ProForm.Group>
+      <ProForm.Group>
+        <ProFormTextArea
+          name="note"
+          width="xl"
+          label={intl.formatMessage({
+            id: "forms.fields.note.label",
+          })}
+        />
+      </ProForm.Group>
+    </>
+  );
+};
+
+export default Widget;

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

@@ -0,0 +1,31 @@
+import Note from "./Note";
+import Quote from "./Quote";
+import SentEdit from "./SentEdit";
+import SentRead from "./SentRead";
+import Term from "./Term";
+import Wd from "./Wd";
+
+interface IWidgetMdTpl {
+  tpl?: string;
+  props?: string;
+}
+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 : ""} />;
+    case "quote":
+      return <Quote props={props ? props : ""} />;
+    default:
+      return <>未定义模版({tpl})</>;
+  }
+};
+
+export default Widget;

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

@@ -0,0 +1,14 @@
+import { TCodeConvertor, XmlToReact } from "./utilities";
+
+interface IWidget {
+  html: string;
+  wordWidget?: boolean;
+  convertor?: TCodeConvertor;
+}
+const Widget = ({ html, wordWidget = false, convertor }: IWidget) => {
+  console.log("word", wordWidget);
+  const jsx = XmlToReact(html, wordWidget, convertor);
+  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(atob(props)) as IWidgetNoteCtl;
+  return <NoteCtl {...prop} />;
+};
+
+export default Widget;

+ 75 - 0
dashboard/src/components/template/Quote.tsx

@@ -0,0 +1,75 @@
+import { ProCard } from "@ant-design/pro-components";
+import { Button, Popover } from "antd";
+import { SearchOutlined, CopyOutlined } from "@ant-design/icons";
+import { Typography } from "antd";
+
+const { Text, Link } = Typography;
+
+interface IWidgetQuoteCtl {
+  paraId: string;
+  paliPath?: string[];
+  channel?: string;
+  pali?: string;
+  error?: boolean;
+  message?: string;
+}
+const QuoteCtl = ({
+  paraId,
+  paliPath,
+  channel,
+  pali,
+  error,
+  message,
+}: IWidgetQuoteCtl) => {
+  const show = pali ? pali : paraId;
+  let textShow = <></>;
+
+  if (typeof error !== "undefined") {
+    textShow = <Text type="danger">{show}</Text>;
+  } else {
+    textShow = <Link>{show}</Link>;
+  }
+
+  const userCard = (
+    <>
+      <ProCard
+        style={{ maxWidth: 500, minWidth: 300 }}
+        actions={[
+          <Button type="link" size="small" icon={<SearchOutlined />}>
+            分栏打开
+          </Button>,
+          <Button type="link" size="small" icon={<SearchOutlined />}>
+            新窗口打开
+          </Button>,
+          <Button type="link" size="small" icon={<CopyOutlined />}>
+            复制引用
+          </Button>,
+        ]}
+      >
+        <div>{message ? message : ""}</div>
+      </ProCard>
+    </>
+  );
+  return (
+    <>
+      <Popover content={userCard} placement="bottom">
+        {textShow}
+      </Popover>
+    </>
+  );
+};
+
+interface IWidget {
+  props: string;
+}
+const Widget = ({ props }: IWidget) => {
+  const prop = JSON.parse(atob(props)) as IWidgetQuoteCtl;
+  console.log(prop);
+  return (
+    <>
+      <QuoteCtl {...prop} />
+    </>
+  );
+};
+
+export default Widget;

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

@@ -0,0 +1,80 @@
+import { Card } from "antd";
+
+import type { IUser } from "../auth/User";
+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 {
+  suggestion?: number;
+  qa?: number;
+}
+export interface ISentence {
+  content: string;
+  html: string;
+  book: number;
+  para: number;
+  wordStart: number;
+  wordEnd: number;
+  editor: IUser;
+  channel: IChannel;
+  updateAt: string;
+  suggestionCount?: ISuggestiongCount;
+}
+
+export interface IWidgetSentEditInner {
+  id: string;
+  channels?: string[];
+  origin?: ISentence[];
+  translation?: ISentence[];
+  layout?: "row" | "column";
+  tranNum?: number;
+  nissayaNum?: number;
+  commNum?: number;
+  originNum: number;
+  simNum?: number;
+}
+const SentEditInner = ({
+  id,
+  origin,
+  translation,
+  layout = "column",
+  tranNum,
+  nissayaNum,
+  commNum,
+  originNum,
+  simNum,
+}: IWidgetSentEditInner) => {
+  return (
+    <Card>
+      <SentMenu>
+        <SentContent
+          origin={origin}
+          translation={translation}
+          layout={layout}
+        />
+        <SentTab
+          id={id}
+          tranNum={tranNum}
+          nissayaNum={nissayaNum}
+          commNum={commNum}
+          originNum={originNum}
+          simNum={simNum}
+        />
+      </SentMenu>
+    </Card>
+  );
+};
+
+interface IWidgetSentEdit {
+  props: string;
+}
+const Widget = ({ props }: IWidgetSentEdit) => {
+  const prop = JSON.parse(atob(props)) as IWidgetSentEditInner;
+  console.log("sent data", prop);
+  return <SentEditInner {...prop} />;
+};
+
+export default Widget;

+ 26 - 0
dashboard/src/components/template/SentEdit/EditInfo.tsx

@@ -0,0 +1,26 @@
+import { Typography } from "antd";
+import { Space } from "antd";
+import User from "../../auth/User";
+import TimeShow from "../../utilities/TimeShow";
+import { ISentence } from "../SentEdit";
+
+const { Text } = Typography;
+
+interface IWidget {
+  data: ISentence;
+}
+const Widget = ({ data }: IWidget) => {
+  return (
+    <div style={{ fontSize: "80%" }}>
+      <Text type="secondary">
+        <Space>
+          <User {...data.editor} />
+          <span>updated</span>
+          <TimeShow time={data.updateAt} title="UpdatedAt" />
+        </Space>
+      </Text>
+    </div>
+  );
+};
+
+export default Widget;

+ 53 - 0
dashboard/src/components/template/SentEdit/SentCell.tsx

@@ -0,0 +1,53 @@
+import { useState } from "react";
+import { ISentence } from "../SentEdit";
+import SentEditMenu from "./SentEditMenu";
+import SentCellEditable from "./SentCellEditable";
+import MdView from "../MdView";
+import SuggestionTabs from "./SuggestionTabs";
+import EditInfo from "./EditInfo";
+
+interface ISentCell {
+  data: ISentence;
+  wordWidget?: boolean;
+}
+const Widget = ({ data, wordWidget = false }: ISentCell) => {
+  const [isEditMode, setIsEditMode] = useState(false);
+  const [sentData, setSentData] = useState<ISentence>(data);
+
+  return (
+    <div style={{ marginBottom: "8px" }}>
+      <SentEditMenu
+        onModeChange={(mode: string) => {
+          if (mode === "edit") {
+            setIsEditMode(true);
+          }
+        }}
+      >
+        <EditInfo data={data} />
+        <div style={{ display: isEditMode ? "none" : "block" }}>
+          <MdView
+            html={sentData.html !== "" ? sentData.html : "请输入"}
+            wordWidget={wordWidget}
+          />
+        </div>
+        <div style={{ display: isEditMode ? "block" : "none" }}>
+          <SentCellEditable
+            data={sentData}
+            onClose={() => {
+              setIsEditMode(false);
+            }}
+            onDataChange={(data: ISentence) => {
+              setSentData(data);
+            }}
+          />
+        </div>
+
+        <div>
+          <SuggestionTabs data={data} />
+        </div>
+      </SentEditMenu>
+    </div>
+  );
+};
+
+export default Widget;

+ 111 - 0
dashboard/src/components/template/SentEdit/SentCellEditable.tsx

@@ -0,0 +1,111 @@
+import { Button, message, Typography } from "antd";
+import { SaveOutlined } from "@ant-design/icons";
+import TextArea from "antd/lib/input/TextArea";
+import { useState } from "react";
+import { useIntl } from "react-intl";
+import { put } from "../../../request";
+import { ISentenceRequest, ISentenceResponse } from "../../api/Corpus";
+import { ISentence } from "../SentEdit";
+const { Text } = Typography;
+
+interface ISentCellEditable {
+  data: ISentence;
+  onDataChange?: Function;
+  onClose?: Function;
+}
+const Widget = ({ data, onDataChange, onClose }: ISentCellEditable) => {
+  const intl = useIntl();
+  const [value, setValue] = useState(data.content);
+  const [saving, setSaving] = useState<boolean>(false);
+  const save = () => {
+    setSaving(true);
+    put<ISentenceRequest, 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) => {
+        console.log(json);
+        setSaving(false);
+
+        if (json.ok) {
+          message.success(intl.formatMessage({ id: "flashes.success" }));
+          if (typeof onDataChange !== "undefined") {
+            const newData: ISentence = {
+              content: json.data.content,
+              html: json.data.html,
+              book: json.data.book,
+              para: json.data.paragraph,
+              wordStart: json.data.word_start,
+              wordEnd: json.data.word_end,
+              editor: json.data.editor,
+              channel: json.data.channel,
+              updateAt: json.data.updated_at,
+            };
+            onDataChange(newData);
+          }
+        } else {
+          message.error(json.message);
+        }
+      })
+      .catch((e) => {
+        setSaving(false);
+        console.error("catch", e);
+        message.error(e.message);
+      });
+  };
+  return (
+    <div>
+      <TextArea
+        value={value}
+        onChange={(e) => setValue(e.target.value)}
+        placeholder="请输入"
+        autoSize={{ minRows: 3, maxRows: 5 }}
+      />
+      <div style={{ display: "flex", justifyContent: "space-between" }}>
+        <div>
+          <span>
+            <Text keyboard>esc</Text>=
+            <Button
+              size="small"
+              type="link"
+              onClick={(e) => {
+                if (typeof onClose !== "undefined") {
+                  onClose(e);
+                }
+              }}
+            >
+              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>
+  );
+};
+
+export default Widget;

+ 29 - 0
dashboard/src/components/template/SentEdit/SentContent.tsx

@@ -0,0 +1,29 @@
+import { ISentence } from "../SentEdit";
+import SentCell from "./SentCell";
+interface IWidgetSentContent {
+  origin?: ISentence[];
+  translation?: ISentence[];
+  layout?: "row" | "column";
+}
+const Widget = ({
+  origin,
+  translation,
+  layout = "column",
+}: IWidgetSentContent) => {
+  return (
+    <div style={{ display: "flex", flexDirection: layout }}>
+      <div style={{ flex: "5", color: "#9f3a01" }}>
+        {origin?.map((item, id) => {
+          return <SentCell key={id} data={item} wordWidget={true} />;
+        })}
+      </div>
+      <div style={{ flex: "5" }}>
+        {translation?.map((item, id) => {
+          return <SentCell key={id} data={item} />;
+        })}
+      </div>
+    </div>
+  );
+};
+
+export default Widget;

+ 66 - 0
dashboard/src/components/template/SentEdit/SentEditMenu.tsx

@@ -0,0 +1,66 @@
+import { Button, Dropdown, Menu } from "antd";
+import { useState } from "react";
+import { EditOutlined, CopyOutlined, MoreOutlined } from "@ant-design/icons";
+
+interface ISentEditMenu {
+  children?: React.ReactNode;
+  onModeChange?: Function;
+}
+const Widget = ({ children, onModeChange }: ISentEditMenu) => {
+  const [isHover, setIsHover] = useState(false);
+
+  const menu = (
+    <Menu
+      onClick={(e) => {
+        console.log(e);
+      }}
+      items={[
+        {
+          key: "en",
+          label: "时间线",
+        },
+        {
+          key: "zh-Hans",
+          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={<CopyOutlined />} size="small" />
+        <Dropdown overlay={menu} placement="bottomRight">
+          <Button icon={<MoreOutlined />} size="small" />
+        </Dropdown>
+      </div>
+      {children}
+    </div>
+  );
+};
+
+export default Widget;

+ 65 - 0
dashboard/src/components/template/SentEdit/SentMenu.tsx

@@ -0,0 +1,65 @@
+import { useState } from "react";
+import { Button, Dropdown } from "antd";
+import { MoreOutlined } from "@ant-design/icons";
+
+import type { MenuProps } from "antd";
+
+const onClick: MenuProps["onClick"] = ({ key }) => {
+  console.log(`Click on item ${key}`);
+};
+
+interface ISentMenu {
+  children?: React.ReactNode;
+}
+const Widget = ({ children }: ISentMenu) => {
+  const [isHover, setIsHover] = useState(false);
+  const items: MenuProps["items"] = [
+    {
+      key: "show-commentary",
+      label: "相关段落",
+    },
+    {
+      key: "show-nissaya",
+      label: "Nissaya",
+    },
+    {
+      key: "copy-id",
+      label: "复制句子编号",
+    },
+    {
+      key: "copy-link",
+      label: "复制句子链接",
+    },
+  ];
+
+  return (
+    <div
+      onMouseEnter={() => {
+        setIsHover(true);
+      }}
+      onMouseLeave={() => {
+        setIsHover(false);
+      }}
+    >
+      <div
+        style={{
+          marginTop: "-1.5em",
+          position: "absolute",
+          display: isHover ? "block" : "none",
+        }}
+      >
+        <Dropdown menu={{ items, onClick }} placement="bottomLeft">
+          <Button
+            onClick={(e) => e.preventDefault()}
+            type="primary"
+            icon={<MoreOutlined />}
+            size="small"
+          />
+        </Dropdown>
+      </div>
+      {children}
+    </div>
+  );
+};
+
+export default Widget;

+ 147 - 0
dashboard/src/components/template/SentEdit/SentTab.tsx

@@ -0,0 +1,147 @@
+import { useState } from "react";
+import { Badge, Tabs, Typography } from "antd";
+import {
+  TranslationOutlined,
+  CloseOutlined,
+  BlockOutlined,
+} from "@ant-design/icons";
+
+import { IWidgetSentEditInner } from "../SentEdit";
+import Article from "../../article/Article";
+import SentTabButton from "./SentTabButton";
+
+const { Text } = Typography;
+
+const Widget = ({
+  id,
+  channels,
+  tranNum,
+  nissayaNum,
+  commNum,
+  originNum,
+  simNum,
+}: IWidgetSentEditInner) => {
+  const [translationActive, setTranslationActive] = useState<boolean>(false);
+  const [nissayaActive, setNissayaActive] = useState<boolean>(false);
+  const [commentaryActive, setCommentaryActive] = useState<boolean>(false);
+  const [originalActive, setOriginalActive] = useState<boolean>(false);
+  const sentId = id.split("_");
+
+  const onChange = (key: string) => {
+    switch (key) {
+      case "translation":
+        setTranslationActive(true);
+        break;
+      case "nissaya":
+        setNissayaActive(true);
+        break;
+      case "commentary":
+        setCommentaryActive(true);
+        break;
+      case "original":
+        setOriginalActive(true);
+        break;
+    }
+  };
+  return (
+    <>
+      <Tabs
+        size="small"
+        tabBarGutter={0}
+        onChange={onChange}
+        tabBarExtraContent={
+          <Text copyable={{ text: sentId[0] }}>{sentId[0]}</Text>
+        }
+        items={[
+          {
+            label: (
+              <Badge size="small" count={0}>
+                <CloseOutlined />
+              </Badge>
+            ),
+            key: "close",
+            children: <></>,
+          },
+          {
+            label: (
+              <SentTabButton
+                icon={<TranslationOutlined />}
+                type="translation"
+                sentId={id}
+                count={tranNum}
+              />
+            ),
+            key: "translation",
+            children: (
+              <Article
+                active={translationActive}
+                type="corpus_sent/translation"
+                articleId={id}
+                mode="edit"
+              />
+            ),
+          },
+          {
+            label: (
+              <SentTabButton
+                icon={<CloseOutlined />}
+                type="nissaya"
+                sentId={id}
+                count={nissayaNum}
+              />
+            ),
+            key: "nissaya",
+            children: (
+              <Article
+                active={nissayaActive}
+                type="corpus_sent/nissaya"
+                articleId={id}
+                mode="edit"
+              />
+            ),
+          },
+          {
+            label: (
+              <SentTabButton
+                icon={<TranslationOutlined />}
+                type="commentary"
+                sentId={id}
+                count={commNum}
+              />
+            ),
+            key: "commentary",
+            children: (
+              <Article
+                active={commentaryActive}
+                type="corpus_sent/commentary"
+                articleId={id}
+                mode="edit"
+              />
+            ),
+          },
+          {
+            label: (
+              <SentTabButton
+                icon={<BlockOutlined />}
+                type="original"
+                sentId={id}
+                count={originNum}
+              />
+            ),
+            key: "original",
+            children: (
+              <Article
+                active={originalActive}
+                type="corpus_sent/original"
+                articleId={id}
+                mode="edit"
+              />
+            ),
+          },
+        ]}
+      />
+    </>
+  );
+};
+
+export default Widget;

+ 80 - 0
dashboard/src/components/template/SentEdit/SentTabButton.tsx

@@ -0,0 +1,80 @@
+import { useIntl } from "react-intl";
+import { Badge, Dropdown } from "antd";
+import {
+  OneToOneOutlined,
+  LinkOutlined,
+  CalendarOutlined,
+} from "@ant-design/icons";
+
+import store from "../../../store";
+import {
+  ISite,
+  refresh as refreshLayout,
+} from "../../../reducers/open-article";
+import type { MenuProps } from "antd";
+
+const handleButtonClick = (e: React.MouseEvent<HTMLButtonElement>) => {
+  console.log("click left button", e);
+};
+
+interface IWidgetSentTabButton {
+  icon?: JSX.Element;
+  type: string;
+  sentId: string;
+  count?: number;
+}
+const Widget = ({ icon, type, sentId, count = 0 }: IWidgetSentTabButton) => {
+  const intl = useIntl();
+  const items: MenuProps["items"] = [
+    {
+      label: "在分栏中打开",
+      key: "openInCol",
+      icon: <OneToOneOutlined />,
+    },
+    {
+      label: "在新标签页中打开",
+      key: "openInWin",
+      icon: <CalendarOutlined />,
+    },
+    {
+      label: "复制链接",
+      key: "copyLink",
+      icon: <LinkOutlined />,
+    },
+  ];
+  const handleMenuClick: MenuProps["onClick"] = (e) => {
+    e.domEvent.stopPropagation();
+    switch (e.key) {
+      case "openInCol":
+        const it: ISite = {
+          title: intl.formatMessage({
+            id: `channel.type.${type}.label`,
+          }),
+          url: "corpus_sent/" + type,
+          id: sentId,
+        };
+        store.dispatch(refreshLayout(it));
+        break;
+    }
+  };
+  const menuProps = {
+    items,
+    onClick: handleMenuClick,
+  };
+
+  return (
+    <Dropdown.Button
+      size="small"
+      type="text"
+      menu={menuProps}
+      onClick={handleButtonClick}
+    >
+      {intl.formatMessage({
+        id: `channel.type.${type}.label`,
+      })}
+      <Badge size="small" color="geekblue" count={count}></Badge>
+    </Dropdown.Button>
+  );
+};
+
+export default Widget;

+ 46 - 0
dashboard/src/components/template/SentEdit/SuggestionAdd.tsx

@@ -0,0 +1,46 @@
+import { Button } from "antd";
+import { useState } from "react";
+import { Typography } from "antd";
+import { PlusOutlined } from "@ant-design/icons";
+
+import { ISentence } from "../SentEdit";
+import SentEditMenu from "./SentEditMenu";
+import SentCellEditable from "./SentCellEditable";
+
+interface ISentCell {
+  data: ISentence;
+}
+const Widget = ({ data }: ISentCell) => {
+  const [isEditMode, setIsEditMode] = useState(false);
+  const [sentData, setSentData] = useState<ISentence>(data);
+
+  return (
+    <>
+      <div style={{ display: isEditMode ? "none" : "block" }}>
+        <Button
+          type="dashed"
+          style={{ width: 300 }}
+          icon={<PlusOutlined />}
+          onClick={() => {
+            setIsEditMode(true);
+          }}
+        >
+          添加修改建议
+        </Button>
+      </div>
+      <div style={{ display: isEditMode ? "block" : "none" }}>
+        <SentCellEditable
+          data={sentData}
+          onClose={() => {
+            setIsEditMode(false);
+          }}
+          onDataChange={(data: ISentence) => {
+            setSentData(data);
+          }}
+        />
+      </div>
+    </>
+  );
+};
+
+export default Widget;

+ 51 - 0
dashboard/src/components/template/SentEdit/SuggestionList.tsx

@@ -0,0 +1,51 @@
+import { useEffect, useState } from "react";
+import { get } from "../../../request";
+import { ISuggestionListResponse } from "../../api/Suggestion";
+import { IChannel } from "../../channel/Channel";
+import { ISentence } from "../SentEdit";
+import SentCell from "./SentCell";
+interface IWidget {
+  book: number;
+  para: number;
+  wordStart: number;
+  wordEnd: number;
+  channel: IChannel;
+}
+const Widget = ({ book, para, wordStart, wordEnd, channel }: IWidget) => {
+  const [sentData, setSentData] = useState<ISentence[]>([]);
+
+  useEffect(() => {
+    get<ISuggestionListResponse>(
+      `/v2/sentpr?view=sent-info&book=${book}&para=${para}&start=${wordStart}&end=${wordEnd}&channel=${channel.id}`
+    ).then((json) => {
+      const newData: ISentence[] = json.data.rows.map((item) => {
+        return {
+          content: item.content,
+          html: item.html,
+          book: item.book,
+          para: item.paragraph,
+          wordStart: item.word_start,
+          wordEnd: item.word_end,
+          editor: {
+            id: item.editor.id,
+            nickName: item.editor.nickName,
+            realName: item.editor.userName,
+            avatar: item.editor.avatar,
+          },
+          channel: { name: item.channel.name, id: item.channel.id },
+          updateAt: item.updated_at,
+        };
+      });
+      setSentData(newData);
+    });
+  }, [book, para, wordStart, wordEnd, channel]);
+  return (
+    <div>
+      {sentData.map((item, id) => {
+        return <SentCell data={item} key={id} />;
+      })}
+    </div>
+  );
+};
+
+export default Widget;

+ 77 - 0
dashboard/src/components/template/SentEdit/SuggestionTabs.tsx

@@ -0,0 +1,77 @@
+import { useState } from "react";
+import { RadioChangeEvent, Space } from "antd";
+import { Radio } from "antd";
+import { ISentence } from "../SentEdit";
+import { SuggestionIcon } from "../../../assets/icon";
+import SuggestionAdd from "./SuggestionAdd";
+import SuggestionList from "./SuggestionList";
+
+interface IWidget {
+  data: ISentence;
+}
+const Widget = ({ data }: IWidget) => {
+  const [value, setValue] = useState("close");
+  const [showPanel, setShowPanel] = useState("none");
+  const [showSuggestion, setShowSuggestion] = useState("none");
+
+  const onChange = ({ target: { value } }: RadioChangeEvent) => {
+    console.log("radio1 checked", value);
+    switch (value) {
+      case "suggestion":
+        setShowSuggestion("block");
+        setShowPanel("block");
+        break;
+    }
+    setValue(value);
+  };
+  const closeAll = () => {
+    setShowPanel("none");
+  };
+
+  return (
+    <div>
+      <div>
+        <Radio.Group
+          size="small"
+          optionType="button"
+          buttonStyle="solid"
+          onChange={onChange}
+          value={value}
+        >
+          <Radio
+            value="suggestion"
+            onClick={() => {
+              if (value === "suggestion") {
+                setValue("close");
+                closeAll();
+              }
+            }}
+            style={{
+              border: "none",
+              backgroundColor: "wheat",
+              borderRadius: 5,
+            }}
+          >
+            <Space>
+              <SuggestionIcon />
+              {data.suggestionCount?.suggestion}
+            </Space>
+          </Radio>
+          <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>
+        </div>
+      </div>
+    </div>
+  );
+};
+
+export default Widget;

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

@@ -0,0 +1,105 @@
+import { useEffect, useRef, useState } from "react";
+import { Tooltip, Button } from "antd";
+import MdView from "./MdView";
+import { ISentence } from "./SentEdit";
+import { useAppSelector } from "../../hooks";
+import {
+  onChangeKey,
+  onChangeValue,
+  settingInfo,
+} from "../../reducers/setting";
+import { GetUserSetting } from "../auth/setting/default";
+import { TCodeConvertor } from "./utilities";
+
+interface IWidgetSentReadFrame {
+  origin?: ISentence[];
+  translation?: ISentence[];
+  layout?: "row" | "column";
+  sentId?: string;
+}
+const SentReadFrame = ({
+  origin,
+  translation,
+  layout = "column",
+  sentId,
+}: IWidgetSentReadFrame) => {
+  const [paliCode1, setPaliCode1] = useState<TCodeConvertor>("roman");
+  const key = useAppSelector(onChangeKey);
+  const value = useAppSelector(onChangeValue);
+  const settings = useAppSelector(settingInfo);
+  const boxOrg = useRef<HTMLDivElement>(null);
+  const boxSent = useRef<HTMLDivElement>(null);
+
+  useEffect(() => {
+    switch (key) {
+      case "setting.display.original":
+        if (boxOrg.current) {
+          if (value === true) {
+            boxOrg.current.style.display = "block";
+          } else {
+            boxOrg.current.style.display = "none";
+          }
+        }
+        break;
+      case "setting.layout.direction":
+        if (boxSent.current) {
+          if (typeof value === "string") {
+            boxSent.current.style.flexDirection = value;
+          }
+        }
+        break;
+      default:
+        break;
+    }
+    const _paliCode1 = GetUserSetting("setting.pali.script1", settings);
+    if (typeof _paliCode1 !== "undefined") {
+      setPaliCode1(_paliCode1.toString() as TCodeConvertor);
+    }
+  }, [key, value, settings]);
+  return (
+    <Tooltip
+      placement="topLeft"
+      color="white"
+      title={
+        <Button type="link" size="small">
+          aa
+        </Button>
+      }
+    >
+      <div style={{ display: "flex", flexDirection: layout }} ref={boxSent}>
+        <div style={{ flex: "5", color: "#9f3a01" }} ref={boxOrg}>
+          {origin?.map((item, id) => {
+            return (
+              <MdView
+                key={id}
+                html={item.html}
+                wordWidget={true}
+                convertor={paliCode1}
+              />
+            );
+          })}
+        </div>
+        <div style={{ flex: "5" }}>
+          {translation?.map((item, id) => {
+            if (item.html.indexOf("<hr>") >= 0) console.log(item.html);
+            return <MdView key={id} html={item.html} />;
+          })}
+        </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;

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

@@ -0,0 +1,92 @@
+import { ProCard } from "@ant-design/pro-components";
+import { Button, Popover } from "antd";
+import { SearchOutlined } from "@ant-design/icons";
+import { Typography } from "antd";
+import TermCreate, { IWidgetDictCreate } from "../studio/term/TermCreate";
+import { command } from "../../reducers/command";
+import store from "../../store";
+
+const { Text, Link } = Typography;
+
+interface IWidgetTermCtl {
+  id?: string;
+  word?: string;
+  meaning?: string;
+  meaning2?: string;
+  channel?: string;
+}
+const TermCtl = ({ id, word, meaning, meaning2, channel }: IWidgetTermCtl) => {
+  const show = meaning ? meaning : word ? word : "unknown";
+  let textShow = <></>;
+
+  if (typeof id === "undefined") {
+    console.log("danger");
+    textShow = <Text type="danger">{show}</Text>;
+  } else {
+    textShow = <Link>{show}</Link>;
+  }
+  const editButton = (
+    <Button
+      onClick={() => {
+        const it: IWidgetDictCreate = {
+          studio: "string",
+          isCreate: true,
+          word: word,
+          channel: channel,
+          type: "inline",
+        };
+        store.dispatch(command({ prop: it, type: "term" }));
+      }}
+    >
+      新建
+    </Button>
+  );
+  const userCard = (
+    <>
+      <ProCard
+        title={word}
+        style={{ maxWidth: 500, minWidth: 300 }}
+        actions={[
+          <Button type="link" size="small" icon={<SearchOutlined />}>
+            更多
+          </Button>,
+          <Button type="link" size="small" icon={<SearchOutlined />}>
+            详情
+          </Button>,
+          editButton,
+        ]}
+      >
+        <div>
+          {id ? "" : <TermCreate isCreate={true} word={word} studio="" />}
+        </div>
+      </ProCard>
+    </>
+  );
+  return (
+    <>
+      <Popover content={userCard} placement="bottom">
+        {textShow}
+      </Popover>
+      {"("}
+      <Text italic>{word}</Text>
+      {","}
+      <Text>{meaning2}</Text>
+      {")"}
+    </>
+  );
+};
+
+interface IWidgetTerm {
+  props: string;
+}
+const Widget = ({ props }: IWidgetTerm) => {
+  const prop = JSON.parse(atob(props)) as IWidgetTermCtl;
+  console.log(prop);
+  return (
+    <>
+      <TermCtl {...prop} />
+    </>
+  );
+};
+
+export default Widget;

+ 14 - 0
dashboard/src/components/template/Wbw/WbwCase.tsx

@@ -0,0 +1,14 @@
+import { IWbw } from "./WbwWord";
+
+interface IWidget {
+  data: IWbw;
+}
+const Widget = ({ data }: IWidget) => {
+  return (
+    <div>
+      {data.type?.value}-{data.grammar?.value}
+    </div>
+  );
+};
+
+export default Widget;

Some files were not shown because too many files changed in this diff