Pārlūkot izejas kodu

:construction: libaray(门户) 初步完成

visuddhinanda 3 gadi atpakaļ
vecāks
revīzija
09a9fbe833
85 mainītis faili ar 3275 papildinājumiem un 505 dzēšanām
  1. 22 6
      dashboard/src/Router.tsx
  2. 89 0
      dashboard/src/assets/general/images/wikipali_logo.svg
  3. 22 0
      dashboard/src/assets/library/images/wikipali_logo_library.svg
  4. 29 0
      dashboard/src/components/api/Article.ts
  5. 12 0
      dashboard/src/components/api/Auth.ts
  6. 8 0
      dashboard/src/components/api/Channel.ts
  7. 75 0
      dashboard/src/components/api/Corpus.ts
  8. 65 0
      dashboard/src/components/article/AnthologStudioList.tsx
  9. 61 0
      dashboard/src/components/article/AnthologyCard.tsx
  10. 87 0
      dashboard/src/components/article/AnthologyDetail.tsx
  11. 66 0
      dashboard/src/components/article/AnthologyList.tsx
  12. 80 0
      dashboard/src/components/article/TocTree.tsx
  13. 34 0
      dashboard/src/components/auth/SignInAvatar.tsx
  14. 22 0
      dashboard/src/components/auth/StudioName.tsx
  15. 62 0
      dashboard/src/components/channel/ChannelList.tsx
  16. 25 0
      dashboard/src/components/channel/ChannelListItem.tsx
  17. 91 0
      dashboard/src/components/corpus/BookTree.tsx
  18. 129 0
      dashboard/src/components/corpus/BookTreeList.tsx
  19. 48 0
      dashboard/src/components/corpus/BookViewer.tsx
  20. 81 0
      dashboard/src/components/corpus/ChapterCard.tsx
  21. 29 0
      dashboard/src/components/corpus/ChapterFilter.tsx
  22. 29 0
      dashboard/src/components/corpus/ChapterFilterLang.tsx
  23. 27 0
      dashboard/src/components/corpus/ChapterFilterProgress.tsx
  24. 22 0
      dashboard/src/components/corpus/ChapterFilterType.tsx
  25. 25 0
      dashboard/src/components/corpus/ChapterHead.tsx
  26. 48 0
      dashboard/src/components/corpus/ChapterInChannel.tsx
  27. 92 0
      dashboard/src/components/corpus/ChapterList.tsx
  28. 67 0
      dashboard/src/components/corpus/PaliChapterCard.tsx
  29. 47 0
      dashboard/src/components/corpus/PaliChapterChannelList.tsx
  30. 70 0
      dashboard/src/components/corpus/PaliChapterHead.tsx
  31. 38 0
      dashboard/src/components/corpus/PaliChapterList.tsx
  32. 49 0
      dashboard/src/components/corpus/PaliChapterListByPara.tsx
  33. 49 0
      dashboard/src/components/corpus/PaliChapterListByTag.tsx
  34. 56 0
      dashboard/src/components/corpus/TocPath.tsx
  35. 31 0
      dashboard/src/components/dict/CaseList.tsx
  36. 46 0
      dashboard/src/components/dict/DictContent.tsx
  37. 29 0
      dashboard/src/components/dict/DictList.tsx
  38. 41 0
      dashboard/src/components/dict/DictSearch.tsx
  39. 51 0
      dashboard/src/components/dict/GrammarPop.tsx
  40. 56 0
      dashboard/src/components/dict/WordCard.tsx
  41. 39 0
      dashboard/src/components/dict/WordCardByDict.tsx
  42. 39 0
      dashboard/src/components/general/UiLangSelect.tsx
  43. 0 26
      dashboard/src/components/library/Footer.tsx
  44. 26 0
      dashboard/src/components/library/FooterBar.tsx
  45. 82 44
      dashboard/src/components/library/HeadBar.tsx
  46. 0 0
      dashboard/src/components/library/palicanon/.txt
  47. 21 18
      dashboard/src/components/studio/EditableTree.tsx
  48. 15 7
      dashboard/src/components/studio/HeadBar.tsx
  49. 4 0
      dashboard/src/components/studio/SelectCase.tsx
  50. 5 1
      dashboard/src/components/studio/SelectLang.tsx
  51. 2 1
      dashboard/src/components/studio/dict/DictCreate.tsx
  52. 41 0
      dashboard/src/components/studio/group/GroupCreate.tsx
  53. 78 84
      dashboard/src/components/studio/group/GroupFile.tsx
  54. 73 74
      dashboard/src/components/studio/group/GroupMember.tsx
  55. 95 0
      dashboard/src/components/studio/term/TermCreate.tsx
  56. 23 0
      dashboard/src/components/tag/TagArea.tsx
  57. 73 0
      dashboard/src/components/utilities/TimeShow.tsx
  58. 1 0
      dashboard/src/locales/zh-Hans/dict/index.ts
  59. 12 9
      dashboard/src/locales/zh-Hans/term/index.ts
  60. 12 12
      dashboard/src/pages/library/anthology/article.tsx
  61. 54 16
      dashboard/src/pages/library/anthology/index.tsx
  62. 45 14
      dashboard/src/pages/library/anthology/show.tsx
  63. 15 15
      dashboard/src/pages/library/blog/anthology.tsx
  64. 12 12
      dashboard/src/pages/library/blog/course.tsx
  65. 14 14
      dashboard/src/pages/library/blog/index.tsx
  66. 12 12
      dashboard/src/pages/library/blog/term.tsx
  67. 15 15
      dashboard/src/pages/library/blog/translation.tsx
  68. 18 12
      dashboard/src/pages/library/community/index.tsx
  69. 34 0
      dashboard/src/pages/library/community/list.tsx
  70. 15 15
      dashboard/src/pages/library/course/course.tsx
  71. 14 14
      dashboard/src/pages/library/course/index.tsx
  72. 12 12
      dashboard/src/pages/library/course/lesson.tsx
  73. 11 12
      dashboard/src/pages/library/dict/index.tsx
  74. 36 0
      dashboard/src/pages/library/dict/recent.tsx
  75. 40 15
      dashboard/src/pages/library/dict/show.tsx
  76. 110 0
      dashboard/src/pages/library/palicanon/bypath.tsx
  77. 33 0
      dashboard/src/pages/library/palicanon/chapter.tsx
  78. 13 12
      dashboard/src/pages/library/palicanon/index.tsx
  79. 12 12
      dashboard/src/pages/library/term/show.tsx
  80. 8 9
      dashboard/src/pages/nut/index.tsx
  81. 1 1
      dashboard/src/pages/studio/analysis/index.tsx
  82. 23 4
      dashboard/src/pages/studio/anthology/edit.tsx
  83. 17 3
      dashboard/src/pages/studio/group/index.tsx
  84. 8 4
      dashboard/src/pages/studio/term/index.tsx
  85. 52 0
      dashboard/src/utils.ts

+ 22 - 6
dashboard/src/Router.tsx

@@ -18,14 +18,18 @@ import NutSwitchLanguage from "./pages/nut/switch-languages";
 import NutHome from "./pages/nut";
 
 import LibraryCommunity from "./pages/library/community";
+import LibraryCommunityList from "./pages/library/community/list";
 import LibraryCommunityRecent from "./pages/library/community/recent";
 import LibraryPalicanon from "./pages/library/palicanon";
+import LibraryPalicanonByPath from "./pages/library/palicanon/bypath";
+import LibraryPalicanonChapter from "./pages/library/palicanon/chapter";
 import LibraryCourse from "./pages/library/course";
 import LibraryCourseShow from "./pages/library/course/course";
 import LibraryLessonShow from "./pages/library/course/lesson";
 import LibraryTerm from "./pages/library/term/show";
 import LibraryDict from "./pages/library/dict";
 import LibraryDictShow from "./pages/library/dict/show";
+import LibraryDictRecent from "./pages/library/dict/recent";
 import LibraryAnthology from "./pages/library/anthology";
 import LibraryAnthologyShow from "./pages/library/anthology/show";
 import LibraryArticle from "./pages/library/anthology/article";
@@ -88,9 +92,18 @@ const Widget = () => {
 			<Route path="" element={<NutHome />} />
 			<Route path="*" element={<NutNotFound />} />
 
-			<Route path="community" element={<LibraryCommunity />}></Route>
-			<Route path="recent" element={<LibraryCommunityRecent />} />
-			<Route path="palicanon" element={<LibraryPalicanon />} />
+			<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>
 			<Route path="course/show/:id" element={<LibraryCourseShow />}></Route>
@@ -98,11 +111,14 @@ const Widget = () => {
 
 			<Route path="term/:word" element={<LibraryTerm />} />
 
-			<Route path="dict" element={<LibraryDict />} />
-			<Route path="dict/:word" element={<LibraryDictShow />} />
+			<Route path="dict" element={<LibraryDict />}>
+				<Route path=":word" element={<LibraryDictShow />} />
+				<Route path="recent" element={<LibraryDictRecent />} />
+			</Route>
 
 			<Route path="anthology" element={<LibraryAnthology />} />
-			<Route path="anthology/show/:id" element={<LibraryAnthologyShow />} />
+			<Route path="anthology/:id" element={<LibraryAnthologyShow />} />
+			<Route path="anthology/:id/by_channel/:tags" element={<LibraryAnthologyShow />} />
 			<Route path="article/show/:id" element={<LibraryArticle />} />
 
 			<Route path="blog/:studioname" element={<LibraryBlog />} />

+ 89 - 0
dashboard/src/assets/general/images/wikipali_logo.svg

@@ -0,0 +1,89 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 24.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<svg version="1.0" id="wikipali_logo" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px"
+	 y="0px" viewBox="0 0 100 50" style="enable-background:new 0 0 100 50;" xml:space="preserve">
+<g id="圖層_1">
+	<g id="Group_12" transform="translate(-396 -320)">
+		<g id="Group_2" transform="translate(396 320)">
+			<g id="Group_1" transform="translate(39.472 12.369)">
+				<g id="Path_1">
+					<path style="fill:#FFFFFF;" d="M-5.9,19.5c-0.2,0-0.3-0.1-0.5-0.2c-0.1-0.1-0.2-0.2-0.3-0.4l-2.2-8.2v-0.1c0-0.1,0-0.2,0.1-0.3
+						c0.1-0.1,0.2-0.2,0.3-0.2h0.8c0.2,0,0.3,0.1,0.5,0.2c0.1,0.1,0.2,0.2,0.3,0.4l1.1,4.5c0,0,0.2,0.9,0.5,2.6l0,0l0,0l0,0l0,0
+						c0.4-1.7,0.6-2.6,0.6-2.6l1.2-4.5c0.1-0.3,0.4-0.6,0.7-0.6h0.6c0.3,0,0.6,0.2,0.7,0.6l1.2,4.5c0.1,0.3,0.2,0.7,0.3,1.3
+						c0.1,0.6,0.2,1,0.3,1.3l0,0l0,0c0,0,0,0,0.1,0c0-0.2,0.1-0.6,0.2-1.2c0.3-0.6,0.4-1,0.5-1.4l1.1-4.5c0.1-0.3,0.4-0.6,0.7-0.6
+						h0.7c0.1,0,0.3,0.1,0.3,0.2c0,0.1,0.1,0.2,0.1,0.3v0.1l-2.1,8.2c0,0.2-0.1,0.3-0.3,0.4c-0.1,0.1-0.3,0.2-0.5,0.2H0
+						c-0.2,0-0.3-0.1-0.5-0.2c-0.1-0.1-0.2-0.2-0.3-0.4l-1-4.1c-0.1-0.4-0.3-1.3-0.6-2.7c0,0,0,0-0.1,0c0,0,0,0-0.1,0
+						c-0.2,1.2-0.4,2.1-0.6,2.7l-1,4.1c0,0.2-0.1,0.3-0.3,0.4c-0.1,0.1-0.3,0.2-0.5,0.2H-5.9L-5.9,19.5z"/>
+				</g>
+				<g id="Path_2">
+					<path style="fill:#FFFFFF;" d="M8.1,8.1C7.8,8.3,7.5,8.4,7.2,8.4S6.6,8.3,6.3,8.1C6.1,7.9,5.9,7.6,5.9,7.3S6,6.7,6.2,6.5
+						c0.2-0.2,0.6-0.3,0.9-0.3c0.3,0,0.7,0.1,0.9,0.3C8.2,6.7,8.4,7,8.4,7.3C8.4,7.6,8.3,7.9,8.1,8.1z M6.8,19.5
+						c-0.3,0-0.6-0.3-0.6-0.6l0,0v-8.2c0-0.2,0.1-0.3,0.2-0.4c0.1-0.1,0.3-0.2,0.4-0.2h0.7c0.2,0,0.3,0.1,0.4,0.2
+						c0.1,0.1,0.2,0.3,0.2,0.4v8.2c0,0.3-0.3,0.6-0.6,0.6l0,0H6.8z"/>
+				</g>
+				<g id="Path_3">
+					<path style="fill:#FFFFFF;" d="M11.8,19.5c-0.3,0-0.6-0.3-0.6-0.6l0,0V6.5c0-0.2,0.1-0.3,0.2-0.4c0.1-0.1,0.3-0.2,0.4-0.2h0.7
+						c0.2,0,0.3,0.1,0.4,0.2c0.1,0.1,0.2,0.3,0.2,0.4v8.1l0,0l0,0l3.3-4.1c0.2-0.3,0.6-0.5,1-0.4h1c0.1,0,0.2,0.1,0.2,0.2
+						c0.1,0.1,0,0.2,0,0.3l-2.8,3.3V14l3.2,5c0,0,0,0.1,0.1,0.2c0,0.1,0,0.1-0.1,0.2s-0.2,0.2-0.3,0.2h-0.9c-0.4,0-0.7-0.2-0.9-0.5
+						l-2.2-3.7c0,0,0,0-0.1,0l-1.5,1.7c0,0,0,0.1-0.1,0.1v1.7c0,0.3-0.3,0.6-0.6,0.6l0,0L11.8,19.5z"/>
+				</g>
+				<g id="Path_4">
+					<path style="fill:#FFFFFF;" d="M23,8.1c-0.2,0.2-0.6,0.3-0.9,0.3s-0.6-0.1-0.9-0.3c-0.2-0.2-0.3-0.5-0.3-0.8s0.1-0.6,0.3-0.8
+						c0.2-0.2,0.6-0.3,0.9-0.3s0.7,0.1,0.9,0.3c0.2,0.2,0.4,0.5,0.4,0.8C23.4,7.6,23.3,7.9,23,8.1z M21.8,19.5
+						c-0.3,0-0.6-0.3-0.6-0.6l0,0v-8.2c0-0.2,0.1-0.3,0.2-0.4c0.1-0.1,0.3-0.2,0.4-0.2h0.7c0.2,0,0.3,0.1,0.4,0.2
+						c0.1,0.1,0.2,0.3,0.2,0.4v8.2c0,0.3-0.3,0.6-0.6,0.6l0,0H21.8z"/>
+				</g>
+				<g id="Path_5">
+					<path style="fill:#FFFFFF;" d="M26.8,23.3c-0.3,0-0.6-0.3-0.6-0.6l0,0v-12c0-0.3,0.3-0.6,0.6-0.6l0,0h0.4
+						c0.2,0,0.3,0.1,0.4,0.2c0.1,0.1,0.2,0.3,0.2,0.4v0.4l0,0l0,0c0.8-0.7,1.8-1.2,2.9-1.2c1.1,0,2.1,0.4,2.7,1.3c0.7,1,1,2.2,1,3.5
+						c0,0.7-0.1,1.4-0.3,2.1c-0.2,0.6-0.5,1.1-0.9,1.6c-0.4,0.4-0.8,0.7-1.3,1c-0.5,0.2-1,0.3-1.5,0.3c-0.9,0-1.8-0.4-2.4-1l0,0l0,0
+						l0,0v1.5v2.4c0,0.3-0.3,0.6-0.6,0.6l0,0L26.8,23.3L26.8,23.3z M30.2,18.2c0.7,0,1.3-0.3,1.7-0.9c0.5-0.7,0.7-1.6,0.7-2.5
+						c0-2.1-0.7-3.2-2.2-3.2c-0.8,0.1-1.6,0.5-2.2,1.1l-0.1,0.1v4.4c0,0,0,0.1,0.1,0.1C28.8,17.8,29.5,18.1,30.2,18.2L30.2,18.2z"/>
+				</g>
+				<g id="Path_6">
+					<path style="fill:#FFFFFF;" d="M39.1,19.7c-0.7,0-1.4-0.2-2-0.7c-1.1-1.1-1-2.8,0.1-3.9c0.1-0.1,0.3-0.3,0.5-0.4
+						c1.3-0.7,2.7-1.1,4.2-1.2c0,0,0.1,0,0.1-0.1c0-1.3-0.6-2-1.8-2c-0.8,0-1.5,0.2-2.2,0.6c-0.1,0.1-0.3,0.1-0.4,0.1
+						c-0.2,0-0.3-0.1-0.3-0.3L37,11.6c-0.1-0.1-0.1-0.3-0.1-0.5s0.1-0.3,0.3-0.4c1-0.6,2.1-0.9,3.3-0.9c1-0.1,1.9,0.3,2.6,1
+						c0.6,0.9,0.9,1.9,0.8,2.9v5c0,0.3-0.3,0.6-0.6,0.6l0,0h-0.4c-0.2,0-0.3-0.1-0.4-0.2c-0.1-0.1-0.2-0.3-0.2-0.4l-0.1-0.5l0,0l0,0
+						C41.3,19.2,40.2,19.7,39.1,19.7z M38.5,8.1c-0.3,0-0.6-0.3-0.6-0.6l0,0V7.3c0-0.3,0.3-0.6,0.6-0.6l0,0h4.1
+						c0.3,0,0.6,0.3,0.6,0.6l0,0v0.1c0,0.3-0.3,0.6-0.6,0.6l0,0h-4.1V8.1z M39.7,18.2c0.8-0.1,1.6-0.4,2.2-1c0,0,0.1-0.1,0-0.1V15
+						c0-0.1,0-0.1-0.1-0.1c-1,0.1-1.9,0.3-2.8,0.8c-0.5,0.3-0.8,0.8-0.8,1.3c0,0.4,0.1,0.7,0.4,1C38.9,18.1,39.3,18.2,39.7,18.2
+						L39.7,18.2z"/>
+				</g>
+				<g id="Path_7">
+					<path style="fill:#FFFFFF;" d="M48.8,19.7c-0.6,0.1-1.1-0.2-1.5-0.6c-0.3-0.5-0.5-1.2-0.5-1.8V6.5c0-0.2,0.1-0.3,0.2-0.4
+						c0.1-0.1,0.3-0.2,0.4-0.2h0.7c0.2,0,0.3,0.1,0.4,0.2c0.1,0.1,0.2,0.3,0.2,0.4v10.9c0,0.3,0.1,0.5,0.3,0.7c0,0,0.1,0,0.1,0.1
+						c0.1,0,0.1,0.1,0.2,0.1l0.1,0.1l0.1,0.1v0.1l0.1,0.4v0.1c0,0.1,0,0.2-0.1,0.3c-0.1,0.1-0.2,0.2-0.4,0.2
+						C49.1,19.7,48.9,19.7,48.8,19.7z"/>
+				</g>
+				<g id="Path_8">
+					<path style="fill:#FFFFFF;" d="M53.9,8.1c-0.2,0.2-0.6,0.3-0.9,0.3c-0.3,0-0.6-0.1-0.9-0.3c-0.2-0.2-0.4-0.5-0.3-0.8
+						c0-0.3,0.1-0.6,0.3-0.8c0.2-0.2,0.6-0.3,0.9-0.3c0.3,0,0.7,0.1,0.9,0.3c0.2,0.2,0.4,0.5,0.4,0.8C54.3,7.6,54.2,7.9,53.9,8.1z
+						 M52.7,19.5c-0.3,0-0.6-0.3-0.6-0.6l0,0v-8.2c0-0.2,0.1-0.3,0.2-0.4c0.1-0.1,0.3-0.2,0.4-0.2h0.7c0.2,0,0.3,0.1,0.4,0.2
+						c0.1,0.1,0.2,0.3,0.2,0.4v8.2c0,0.3-0.3,0.6-0.6,0.6l0,0H52.7z"/>
+				</g>
+			</g>
+			<g id="Path_9">
+				<path style="fill:#F1CA23;" d="M18.9,34.4c-0.5,0-1-0.4-1-1l0,0v-3.6c0-5.6,2.7-8.8,7.3-8.8c0.5,0,1,0.4,1,1c0,0.5-0.4,1-1,1
+					l0,0c-3.5,0-5.3,2.3-5.3,6.8v3.6C19.8,34,19.4,34.4,18.9,34.4L18.9,34.4z"/>
+			</g>
+			<g id="Path_10">
+				<path style="fill:#F1CA23;" d="M20.8,45.5c-0.5,0-1-0.4-1-1c0-0.5,0.4-1,1-1l0,0c1.9,0,3.3-2.6,3.3-6.3V26.6c0-0.5,0.4-1,1-1
+					c0.5,0,1,0.4,1,1v10.6C26.1,42.9,23.4,45.5,20.8,45.5z"/>
+			</g>
+			<g id="Path_11">
+				<path style="fill:#F1CA23;" d="M14.6,34.4c-0.5,0-1-0.4-1-1l0,0V11.3c0-0.5,0.4-1,1-1c0.5,0,1,0.4,1,1v22.1
+					C15.6,34,15.1,34.4,14.6,34.4L14.6,34.4z"/>
+			</g>
+			<g id="Path_12">
+				<path style="fill:#F1CA23;" d="M10.3,34.4c-0.5,0-1-0.4-1-1l0,0V11.3c0-0.5,0.4-1,1-1c0.5,0,1,0.4,1,1v22.1
+					C11.3,34,10.9,34.4,10.3,34.4L10.3,34.4z"/>
+			</g>
+			<g id="Path_13">
+				<path style="fill:#F1CA23;" d="M6.1,34.4c-0.5,0-1-0.4-1-1l0,0V11.3c0-0.5,0.4-1,1-1s1,0.4,1,1v22.1C7.1,34,6.6,34.4,6.1,34.4z"
+					/>
+			</g>
+		</g>
+	</g>
+</g>
+</svg>

+ 22 - 0
dashboard/src/assets/library/images/wikipali_logo_library.svg

@@ -0,0 +1,22 @@
+<svg id="wikipali_banner" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 221 54.417">
+  <g id="Group_12" data-name="Group 12" transform="translate(-396 -320)">
+    <g id="Group_2" data-name="Group 2" transform="translate(396 320)">
+      <g id="Group_1" data-name="Group 1" transform="translate(39.472 12.369)">
+        <path id="Path_1" data-name="Path 1" d="M252.239,132.886a1.184,1.184,0,0,1-.733-.244,1.144,1.144,0,0,1-.424-.63l-3.447-12.729a.825.825,0,0,1-.026-.206.683.683,0,0,1,.155-.411.655.655,0,0,1,.539-.257h1.234a1.129,1.129,0,0,1,.721.244,1.171,1.171,0,0,1,.411.63l1.7,6.97q.026.1.8,4.063a.046.046,0,0,0,.053.051.046.046,0,0,0,.051-.051q.925-3.96.952-4.063l1.8-6.97a1.187,1.187,0,0,1,1.132-.874h1a1.187,1.187,0,0,1,1.13.874l1.851,6.97q.153.643.475,1.993t.5,2.071a.046.046,0,0,0,.051.051.084.084,0,0,0,.078-.051q.076-.464.36-1.865t.462-2.2l1.647-6.97a1.187,1.187,0,0,1,1.132-.874h1.028a.66.66,0,0,1,.54.257.723.723,0,0,1,.155.437.813.813,0,0,1-.026.18l-3.266,12.729a1.143,1.143,0,0,1-.424.63,1.174,1.174,0,0,1-.733.244h-1.8a1.129,1.129,0,0,1-.721-.244,1.165,1.165,0,0,1-.411-.63l-1.62-6.3q-.258-1.028-.9-4.114a.1.1,0,0,0-.091-.051.091.091,0,0,0-.089.051q-.514,2.726-.9,4.142l-1.543,6.275a1.166,1.166,0,0,1-.411.63,1.12,1.12,0,0,1-.721.244h-1.671Z" transform="translate(-247.61 -111.903)" fill="#fff"/>
+        <path id="Path_2" data-name="Path 2" d="M395.183,81.944a1.988,1.988,0,0,1-1.389.5,1.942,1.942,0,0,1-1.376-.5,1.661,1.661,0,0,1-.539-1.274,1.692,1.692,0,0,1,.539-1.3,1.94,1.94,0,0,1,1.376-.5,1.978,1.978,0,0,1,1.389.5,1.673,1.673,0,0,1,.553,1.3A1.644,1.644,0,0,1,395.183,81.944ZM393.216,99.65a.92.92,0,0,1-.926-.926V86.072a.864.864,0,0,1,.269-.63.892.892,0,0,1,.657-.269h1.157a.9.9,0,0,1,.657.269.865.865,0,0,1,.271.63V98.723a.923.923,0,0,1-.928.926Z" transform="translate(-368.881 -78.666)" fill="#fff"/>
+        <path id="Path_3" data-name="Path 3" d="M444.3,98.574a.92.92,0,0,1-.926-.926V78.489a.869.869,0,0,1,.269-.63.892.892,0,0,1,.657-.269H445.4a.9.9,0,0,1,.657.269.863.863,0,0,1,.269.63v12.6c0,.018.013.026.038.026a.091.091,0,0,0,.065-.026l5.092-6.3a1.831,1.831,0,0,1,1.492-.693h1.518a.387.387,0,0,1,.373.244.382.382,0,0,1-.064.45l-4.269,5.092a.167.167,0,0,0,0,.18l4.937,7.74a.49.49,0,0,1,.078.257.481.481,0,0,1-.078.257.448.448,0,0,1-.437.257h-1.465a1.55,1.55,0,0,1-1.389-.772l-3.4-5.683c-.033-.069-.077-.077-.129-.026l-2.288,2.649a.336.336,0,0,0-.078.206v2.7a.92.92,0,0,1-.926.926Z" transform="translate(-412.163 -77.59)" fill="#fff"/>
+        <path id="Path_4" data-name="Path 4" d="M540.613,81.944a1.987,1.987,0,0,1-1.388.5,1.94,1.94,0,0,1-1.376-.5,1.661,1.661,0,0,1-.539-1.274,1.692,1.692,0,0,1,.539-1.3,1.942,1.942,0,0,1,1.376-.5,1.977,1.977,0,0,1,1.388.5,1.673,1.673,0,0,1,.553,1.3A1.649,1.649,0,0,1,540.613,81.944ZM538.646,99.65a.923.923,0,0,1-.928-.926V86.072a.86.86,0,0,1,.271-.63.892.892,0,0,1,.657-.269H539.8a.9.9,0,0,1,.657.269.863.863,0,0,1,.269.63V98.723a.92.92,0,0,1-.926.926Z" transform="translate(-491.128 -78.666)" fill="#fff"/>
+        <path id="Path_5" data-name="Path 5" d="M589.735,137.187a.92.92,0,0,1-.925-.925V117.746a.92.92,0,0,1,.925-.926h.642a1.016,1.016,0,0,1,.682.257,1.136,1.136,0,0,1,.373.642l.077.642c.018.035.038.053.064.053a.1.1,0,0,0,.065-.026,7.051,7.051,0,0,1,4.424-1.929,5,5,0,0,1,4.231,1.993,8.742,8.742,0,0,1,1.5,5.388,10.164,10.164,0,0,1-.515,3.3,6.991,6.991,0,0,1-1.389,2.469,6.473,6.473,0,0,1-1.993,1.5,5.339,5.339,0,0,1-2.353.54,5.846,5.846,0,0,1-3.729-1.569.031.031,0,0,0-.051,0,.075.075,0,0,0-.025.053l.076,2.366v3.754a.92.92,0,0,1-.926.925h-1.159Zm5.246-8.023a3.153,3.153,0,0,0,2.65-1.4,6.426,6.426,0,0,0,1.028-3.871q0-4.912-3.394-4.912a5.135,5.135,0,0,0-3.368,1.7.245.245,0,0,0-.078.18v6.866a.243.243,0,0,0,.078.18A4.734,4.734,0,0,0,594.981,129.164Z" transform="translate(-534.418 -110.264)" fill="#fff"/>
+        <path id="Path_6" data-name="Path 6" d="M691.509,105.4a4.264,4.264,0,0,1-3.073-1.143,4.27,4.27,0,0,1,.85-6.609,16.4,16.4,0,0,1,6.518-1.787c.069,0,.1-.041.1-.129q-.1-3.032-2.751-3.034a7.157,7.157,0,0,0-3.419,1,.864.864,0,0,1-.669.089.811.811,0,0,1-.539-.424l-.257-.462a.955.955,0,0,1-.089-.706.822.822,0,0,1,.424-.553,10.423,10.423,0,0,1,5.066-1.44,4.826,4.826,0,0,1,3.96,1.594,6.988,6.988,0,0,1,1.312,4.551v7.792a.923.923,0,0,1-.928.926h-.642a1.008,1.008,0,0,1-.681-.257,1.118,1.118,0,0,1-.373-.642l-.1-.721c-.018-.033-.038-.053-.065-.053s-.046.018-.064.053A7.155,7.155,0,0,1,691.509,105.4Zm-.977-18.027a.92.92,0,0,1-.926-.926v-.206a.92.92,0,0,1,.926-.926h6.3a.92.92,0,0,1,.925.926v.206a.92.92,0,0,1-.925.926Zm1.9,15.637a5.287,5.287,0,0,0,3.4-1.6.278.278,0,0,0,.077-.206V97.9c0-.086-.033-.12-.1-.1a11.688,11.688,0,0,0-4.346,1.17,2.336,2.336,0,0,0-1.286,2.045,1.82,1.82,0,0,0,.617,1.518A2.582,2.582,0,0,0,692.435,103.015Z" transform="translate(-617.157 -84.088)" fill="#fff"/>
+        <path id="Path_7" data-name="Path 7" d="M792.08,98.907a2.68,2.68,0,0,1-2.3-.952,4.607,4.607,0,0,1-.708-2.779V78.489a.865.865,0,0,1,.271-.63A.893.893,0,0,1,790,77.59h1.157a.9.9,0,0,1,.657.269.865.865,0,0,1,.271.63V95.333a1.12,1.12,0,0,0,.411,1.028c.034.018.11.061.231.129s.206.12.257.155.12.081.206.14a.691.691,0,0,1,.193.193.438.438,0,0,1,.064.231l.1.566a.736.736,0,0,1,.026.18.96.96,0,0,1-.155.54.792.792,0,0,1-.591.386C792.585,98.9,792.336,98.907,792.08,98.907Z" transform="translate(-702.754 -77.59)" fill="#fff"/>
+        <path id="Path_8" data-name="Path 8" d="M840.663,81.944a1.988,1.988,0,0,1-1.389.5,1.94,1.94,0,0,1-1.376-.5,1.661,1.661,0,0,1-.539-1.274,1.692,1.692,0,0,1,.539-1.3,1.939,1.939,0,0,1,1.376-.5,1.978,1.978,0,0,1,1.389.5,1.673,1.673,0,0,1,.553,1.3A1.649,1.649,0,0,1,840.663,81.944ZM838.7,99.65a.92.92,0,0,1-.926-.926V86.072a.864.864,0,0,1,.269-.63.892.892,0,0,1,.657-.269h1.157a.9.9,0,0,1,.657.269.865.865,0,0,1,.271.63V98.723a.923.923,0,0,1-.928.926Z" transform="translate(-743.346 -78.666)" fill="#fff"/>
+      </g>
+      <path id="Path_9" data-name="Path 9" d="M125.632,125.382a1.531,1.531,0,0,1-1.532-1.532v-5.532c0-8.629,4.134-13.579,11.342-13.579a1.532,1.532,0,0,1,0,3.064c-5.493,0-8.28,3.537-8.28,10.517v5.532A1.529,1.529,0,0,1,125.632,125.382Z" transform="translate(-104.317 -88.043)" fill="#f1ca23"/>
+      <path id="Path_10" data-name="Path 10" d="M144.722,179.085a1.532,1.532,0,1,1,0-3.064c2.987,0,5.076-4,5.076-9.727V149.842a1.532,1.532,0,0,1,3.064,0v16.452C152.86,175.13,148.773,179.085,144.722,179.085Z" transform="translate(-120.364 -124.667)" fill="#f1ca23"/>
+      <path id="Path_11" data-name="Path 11" d="M84.262,37.339a1.531,1.531,0,0,1-1.532-1.532V1.532a1.532,1.532,0,0,1,3.064,0V35.809A1.531,1.531,0,0,1,84.262,37.339Z" transform="translate(-69.542)" fill="#f1ca23"/>
+      <path id="Path_12" data-name="Path 12" d="M42.892,37.339a1.531,1.531,0,0,1-1.532-1.532V1.532a1.532,1.532,0,0,1,3.064,0V35.809A1.531,1.531,0,0,1,42.892,37.339Z" transform="translate(-34.767)" fill="#f1ca23"/>
+      <path id="Path_13" data-name="Path 13" d="M1.532,37.339A1.531,1.531,0,0,1,0,35.808V1.532a1.532,1.532,0,0,1,3.064,0V35.809A1.533,1.533,0,0,1,1.532,37.339Z" fill="#f1ca23"/>
+    </g>
+    <text id="studio" transform="translate(542 353.2)" fill="#fff" font-size="27" font-family="NotoSans-ExtraLight, Noto Sans" font-weight="200"><tspan x="0" y="0">Library</tspan></text>
+  </g>
+</svg>

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

@@ -0,0 +1,29 @@
+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;
+}
+export interface IAnthologyStudioListApiResponse {
+	count: number;
+	studio: IStudioApiResponse;
+}

+ 12 - 0
dashboard/src/components/api/Auth.ts

@@ -0,0 +1,12 @@
+export interface IUserApiResponse {
+	id: string;
+	name: string;
+	avatar: string;
+}
+
+export interface IStudioApiResponse {
+	id: string;
+	name: string;
+	avatar: string;
+	owner: IUserApiResponse;
+}

+ 8 - 0
dashboard/src/components/api/Channel.ts

@@ -0,0 +1,8 @@
+export type ChannelInfoProps = {
+	ChannelName: string;
+	ChannelId: string;
+	ChannelType: string;
+	StudioName: string;
+	StudioId: string;
+	StudioType: string;
+};

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

@@ -0,0 +1,75 @@
+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;
+}
+
+export interface IApiResponcePaliChapterList {
+	ok: boolean;
+	message: string;
+	data: { rows: IApiPaliChapterList[]; count: number };
+}
+export interface IApiResponcePaliChapter {
+	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;
+}
+
+/**
+ * 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;
+	};
+}
+
+export interface IApiResponseChapterChannelList {
+	ok: boolean;
+	message: string;
+	data: { rows: IApiChapterChannels[]; count: number };
+}

+ 65 - 0
dashboard/src/components/article/AnthologStudioList.tsx

@@ -0,0 +1,65 @@
+import { useState, useEffect } from "react";
+import { List, Space, Card } from "antd";
+import StudioNmae from "../auth/StudioName";
+import type { IAnthologyStudioListApiResponse } from "../api/Article";
+import type { IStudioApiResponse } from "../api/Auth";
+
+const defaultData: IAnthologyStudioData[] = [];
+
+interface IAnthologyStudioData {
+	count: number;
+	studio: IStudioApiResponse;
+}
+/*
+interface IWidgetAnthologyList {
+	data: IAnthologyData[];
+}
+*/
+const Widget = () => {
+	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=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);
+			});
+	}
+
+	return (
+		<Card title="作者">
+			<List
+				itemLayout="vertical"
+				size="large"
+				dataSource={tableData}
+				renderItem={(item) => (
+					<List.Item>
+						<Space>
+							<StudioNmae data={item.studio} />
+							<span>({item.count})</span>
+						</Space>
+					</List.Item>
+				)}
+			/>
+		</Card>
+	);
+};
+
+export default Widget;

+ 61 - 0
dashboard/src/components/article/AnthologyCard.tsx

@@ -0,0 +1,61 @@
+import { Link } from "react-router-dom";
+import { Row, Col } from "antd";
+import { Card } from "antd";
+import { Typography } from "antd";
+import StudioName from "../auth/StudioName";
+import type { IStudio } from "../auth/StudioName";
+import type { ListNodeData } from "../studio/EditableTree";
+
+const { Title, Text } = Typography;
+
+export interface IArticleData {
+	id: string;
+	title: string;
+	subTitle: string;
+	summary: string;
+	created_at: string;
+	updated_at: string;
+}
+
+export interface IAnthologyData {
+	id: string;
+	title: string;
+	subTitle: string;
+	summary: string;
+	articles: ListNodeData[];
+	studio: IStudio;
+	created_at: string;
+	updated_at: string;
+}
+
+interface IWidgetAnthologyCard {
+	data: IAnthologyData;
+}
+
+const Widget = (prop: IWidgetAnthologyCard) => {
+	const articleList = prop.data.articles.map((item, id) => {
+		return <div key={id}>{item.title}</div>;
+	});
+	return (
+		<>
+			<Card hoverable bordered={false} style={{ width: "100%" }}>
+				<Title level={4}>
+					<Link to={prop.data.id}>{prop.data.title}</Link>
+				</Title>
+				<div>
+					<Text type="secondary">{prop.data.subTitle}</Text>
+				</div>
+				<div>
+					<Text>{prop.data.summary}</Text>
+				</div>
+				<StudioName data={prop.data.studio} />
+				<Row>
+					<Col flex={"100px"}>Content</Col>
+					<Col flex={"auto"}>{articleList}</Col>
+				</Row>
+			</Card>
+		</>
+	);
+};
+
+export default Widget;

+ 87 - 0
dashboard/src/components/article/AnthologyDetail.tsx

@@ -0,0 +1,87 @@
+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 TocTree from "./TocTree";
+import { ApiFetch } from "../../utils";
+
+const { Title, Text } = Typography;
+
+interface IWidgetAnthologyDetail {
+	aid: string;
+	channels?: string[];
+}
+
+const defaultData: IAnthologyData = {
+	id: "",
+	title: "",
+	subTitle: "",
+	summary: "",
+	articles: [],
+	studio: {
+		id: "",
+		name: "",
+		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");
+		fetchData();
+	}, [setTableData]);
+
+	function fetchData() {
+		ApiFetch(`/anthology/${prop.aid}`)
+			.then((response) => {
+				const json = response as unknown as IAnthologyListApiResponse2;
+
+				const item: IAnthologyListApiResponse = json.data;
+				let newTree: IAnthologyData = {
+					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);
+				//setTreeData(newTree.articles);
+				console.log("toc", newTree.articles);
+			})
+			.catch((error) => {
+				console.error(error);
+			});
+	}
+	return (
+		<>
+			<Title level={4}>{tableData.title}</Title>
+			<div>
+				<Text type="secondary">{tableData.subTitle}</Text>
+			</div>
+			<div>
+				<ReactMarkdown>{tableData.summary}</ReactMarkdown>
+			</div>
+			<Title level={5}>目录</Title>
+
+			<TocTree treeData={tableData.articles} />
+		</>
+	);
+};
+
+export default Widget;

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

@@ -0,0 +1,66 @@
+import { useState, useEffect } from "react";
+import { List } from "antd";
+import AnthologyCard from "./AnthologyCard";
+import type { IAnthologyData } from "./AnthologyCard";
+import type { IAnthologyListApiResponse } from "../api/Article";
+
+const defaultData: IAnthologyData[] = [];
+
+const Widget = () => {
+	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);
+
+				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);
+			});
+	}
+
+	return (
+		<List
+			itemLayout="vertical"
+			size="large"
+			dataSource={tableData}
+			renderItem={(item) => (
+				<List.Item>
+					<AnthologyCard data={item} />
+				</List.Item>
+			)}
+		/>
+	);
+};
+
+export default Widget;

+ 80 - 0
dashboard/src/components/article/TocTree.tsx

@@ -0,0 +1,80 @@
+import { Tree } from "antd";
+import type { TreeProps } from "antd/es/tree";
+import type { ListNodeData } from "../studio/EditableTree";
+
+type TreeNodeData = {
+	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 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 };
+		/*
+		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 (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[];
+};
+const onSelect: TreeProps["onSelect"] = (selectedKeys, info) => {
+	//let aaa: NewTree = info.node;
+	console.log("selected", selectedKeys);
+};
+const Widget = (prop: IWidgetTocTree) => {
+	const data = tocGetTreeData(prop.treeData);
+
+	//const [expandedKeys] = useState(["0-0", "0-0-0", "0-0-0-0"]);
+
+	return (
+		<>
+			<Tree onSelect={onSelect} treeData={data} />
+		</>
+	);
+};
+
+export default Widget;

+ 34 - 0
dashboard/src/components/auth/SignInAvatar.tsx

@@ -0,0 +1,34 @@
+import { Dropdown, Tooltip } from "antd";
+import { Avatar } from "antd";
+import { ProCard } from "@ant-design/pro-components";
+import { UserOutlined, HomeOutlined, LogoutOutlined, SettingOutlined } from "@ant-design/icons";
+
+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>
+	);
+};
+
+export default Widget;

+ 22 - 0
dashboard/src/components/auth/StudioName.tsx

@@ -0,0 +1,22 @@
+import { Avatar, Space } from "antd";
+
+export interface IStudio {
+	id: string;
+	name: string;
+	avatar: string;
+}
+interface IWidghtStudio {
+	data: IStudio;
+}
+const Widget = (prop: IWidghtStudio) => {
+	// TODO
+	const name = prop.data.name.slice(0, 1);
+	return (
+		<Space>
+			<Avatar size="small">{name}</Avatar>
+			{prop.data.name}
+		</Space>
+	);
+};
+
+export default Widget;

+ 62 - 0
dashboard/src/components/channel/ChannelList.tsx

@@ -0,0 +1,62 @@
+import { useState } from "react";
+import { List } from "antd";
+import ChannelListItem from "./ChannelListItem";
+import type { ChannelInfoProps } from "../api/Channel";
+import { Collapse } from "antd";
+
+const { Panel } = Collapse;
+
+export type ChannelFilterProps = {
+	ChapterProgress: number;
+	lang: string;
+	ChannelType: string;
+};
+type IWidgetChannelList = {
+	props?: ChannelFilterProps;
+};
+const defaultChannelFilterProps: ChannelFilterProps = {
+	ChapterProgress: 0.9,
+	lang: "en",
+	ChannelType: "translation",
+};
+
+const Widget = ({ props = defaultChannelFilterProps }: IWidgetChannelList) => {
+	//const [tableData, setTableData] = useState();
+	//: ChannelInfoProps[]
+	const tableData: ChannelInfoProps[] = [
+		{
+			ChannelName: "正式版",
+			ChannelId: "344",
+			ChannelType: "translation",
+			StudioName: "visuddhinadna",
+			StudioId: "2333",
+			StudioType: "org",
+		},
+		{
+			ChannelName: "中文译文",
+			ChannelId: "2345",
+			ChannelType: "translation",
+			StudioName: "Kosalla",
+			StudioId: "1234",
+			StudioType: "people",
+		},
+	];
+	return (
+		<Collapse defaultActiveKey={["1"]} expandIconPosition="start">
+			<Panel header="版本" key="1">
+				<List
+					itemLayout="vertical"
+					size="large"
+					dataSource={tableData}
+					renderItem={(item) => (
+						<List.Item>
+							<ChannelListItem data={item} />
+						</List.Item>
+					)}
+				/>
+			</Panel>
+		</Collapse>
+	);
+};
+
+export default Widget;

+ 25 - 0
dashboard/src/components/channel/ChannelListItem.tsx

@@ -0,0 +1,25 @@
+import { Space } from "antd";
+import { Avatar } from "antd";
+import type { ChannelInfoProps } from "../api/Channel";
+
+type IWidgetChannelListItem = {
+	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>
+		</>
+	);
+};
+
+export default Widget;

+ 91 - 0
dashboard/src/components/corpus/BookTree.tsx

@@ -0,0 +1,91 @@
+import { useIntl } from "react-intl";
+import { useState, useEffect } from "react";
+import { DownOutlined } from "@ant-design/icons";
+import { Layout, Space, Tree } from "antd";
+import { Select } from "antd";
+import { Typography } from "antd";
+import type { TreeProps } from "antd/es/tree";
+import { Collapse } from "antd";
+import { ApiFetch } from "../../utils";
+
+const { Panel } = Collapse;
+const { Text } = Typography;
+
+const { Option } = Select;
+interface IWidgetBookTree {
+	root?: string;
+	path?: string[];
+}
+const Widget = (prop: IWidgetBookTree) => {
+	//Library foot bar
+	const intl = useIntl(); //i18n
+	const defaultTreeData: NewTree[] = [];
+	const [treeData, setTreeData] = useState(defaultTreeData);
+
+	useEffect(() => {
+		if (typeof prop.root !== "undefined") fetchBookTree(prop.root);
+	}, [prop.root]);
+
+	type OrgTree = {
+		name: string;
+		tag: string[];
+		children: OrgTree[];
+	};
+	type NewTree = {
+		title: string;
+		key: string;
+		tag: string[];
+		children: NewTree[];
+	};
+	const onSelect: TreeProps["onSelect"] = (selectedKeys, info) => {
+		//let aaa: NewTree = info.node;
+		//console.log("selected", aaa.tag);
+	};
+
+	function fetchBookTree(value: string) {
+		function treeMap(params: OrgTree): NewTree {
+			return {
+				title: params.name,
+				key: params.tag.join(),
+				tag: params.tag,
+				children: Array.isArray(params.children) ? params.children.map(treeMap) : [],
+			};
+		}
+		let url = `/palibook/${value}`;
+		ApiFetch(url).then((response) => {
+			const myJson = response as unknown as OrgTree[];
+			let newTree = myJson.map(treeMap);
+			setTreeData(newTree);
+		});
+	}
+	const handleChange = (value: string) => {
+		console.log(`selected ${value}`);
+		fetchBookTree(value);
+	};
+
+	// TODO
+	return (
+		<Layout>
+			<Collapse defaultActiveKey={["1"]} expandIconPosition="start">
+				<Panel header="目录" key="1">
+					<Space>
+						<Text>目录风格</Text>
+						<Select defaultValue={prop.root} loading={false} onChange={handleChange}>
+							<Option value="defualt">Defualt</Option>
+							<Option value="cscd">CSCD</Option>
+						</Select>
+					</Space>
+					<Tree
+						showLine
+						switcherIcon={<DownOutlined />}
+						defaultExpandedKeys={["sutta"]}
+						onSelect={onSelect}
+						treeData={treeData}
+					/>
+				</Panel>
+			</Collapse>
+		</Layout>
+	);
+};
+
+export default Widget;

+ 129 - 0
dashboard/src/components/corpus/BookTreeList.tsx

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

+ 48 - 0
dashboard/src/components/corpus/BookViewer.tsx

@@ -0,0 +1,48 @@
+import { useState, useEffect } from "react";
+
+import PaliChapterChannelList from "./PaliChapterChannelList";
+import PaliChapterListByPara from "./PaliChapterListByPara";
+import PaliChapterHead from "./PaliChapterHead";
+import { IPaliChapterData } from "./PaliChapterCard";
+import { IChapterClickEvent } from "./PaliChapterList";
+
+export interface IParagraph {
+	book: number;
+	para: number;
+}
+
+interface IWidgetBookViewer {
+	para: IParagraph;
+	onChange?: Function;
+}
+const Widget = (prop: IWidgetBookViewer) => {
+	const [para, setPara] = useState(prop.para);
+	useEffect(() => {
+		if (typeof prop.onChange !== "undefined") {
+			prop.onChange(para);
+		}
+	}, [para]);
+	useEffect(() => {
+		setPara(prop.para);
+	}, [prop.para]);
+	return (
+		<>
+			<PaliChapterHead
+				onChange={(e: IParagraph) => {
+					setPara(e);
+				}}
+				para={para}
+			/>
+			<PaliChapterChannelList para={para} />
+			<PaliChapterListByPara
+				para={para}
+				onChapterClick={(e: IChapterClickEvent) => {
+					setPara({ book: e.para.Book, para: e.para.Paragraph });
+					console.log("PaliChapterListByPara", "onchange", e);
+				}}
+			/>
+		</>
+	);
+};
+
+export default Widget;

+ 81 - 0
dashboard/src/components/corpus/ChapterCard.tsx

@@ -0,0 +1,81 @@
+import { Row, Col } from "antd";
+import { Typography } from "antd";
+import TimeShow from "../utilities/TimeShow";
+import TocPath from "../corpus/TocPath";
+import TagArea from "../tag/TagArea";
+import type { TagNode } from "../tag/TagArea";
+import type { ChannelInfoProps } from "../api/Channel";
+import ChannelListItem from "../channel/ChannelListItem";
+
+const { Title, Paragraph, Link } = 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;
+}
+
+interface IWidgetChapterCard {
+	data: ChapterData;
+}
+
+const Widget = (prop: IWidgetChapterCard) => {
+	const path = JSON.parse(prop.data.Path);
+	const tags = prop.data.Tag;
+	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>{prop.data.Summary}</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>
+		</>
+	);
+};
+
+export default Widget;

+ 29 - 0
dashboard/src/components/corpus/ChapterFilter.tsx

@@ -0,0 +1,29 @@
+import { Layout, Row, Col } from "antd";
+import { Button } from "antd";
+import ChapterFilterType from "./ChapterFilterType";
+import ChapterFilterLang from "./ChapterFilterLang";
+import ChapterFilterProgress from "./ChapterFilterProgress";
+
+const Widget = () => {
+	return (
+		<Layout>
+			<Row>
+				<Col>
+					<ChapterFilterType />
+				</Col>
+				<Col>
+					<ChapterFilterLang />
+				</Col>
+				<Col>
+					<ChapterFilterProgress />
+				</Col>
+				<Col>
+					<Button>Search</Button>
+					<Button>Reset</Button>
+				</Col>
+			</Row>
+		</Layout>
+	);
+};
+
+export default Widget;

+ 29 - 0
dashboard/src/components/corpus/ChapterFilterLang.tsx

@@ -0,0 +1,29 @@
+import { Select } from "antd";
+import React from "react";
+
+const { Option } = Select;
+
+const children: React.ReactNode[] = [];
+for (let i = 10; i < 36; i++) {
+	children.push(<Option key={i.toString(36) + i}>{i.toString(36) + i}</Option>);
+}
+
+const handleChange = (value: string[]) => {
+	console.log(`selected ${value}`);
+};
+const Widget = () => {
+	return (
+		<Select
+			mode="multiple"
+			allowClear
+			style={{ minWidth: 100 }}
+			placeholder="Language"
+			defaultValue={[]}
+			onChange={handleChange}
+		>
+			{children}
+		</Select>
+	);
+};
+
+export default Widget;

+ 27 - 0
dashboard/src/components/corpus/ChapterFilterProgress.tsx

@@ -0,0 +1,27 @@
+import { Select } from "antd";
+import React from "react";
+
+const { Option } = Select;
+
+const children: React.ReactNode[] = [];
+for (let i = 10; i < 36; i++) {
+	children.push(<Option key={i.toString(36) + i}>{i.toString(36) + i}</Option>);
+}
+
+const handleChange = (value: string[]) => {
+	console.log(`selected ${value}`);
+};
+const Widget = () => {
+	return (
+		<Select style={{ width: 100 }} placeholder="完成度" defaultValue={["90"]} onChange={handleChange}>
+			<Option key="0.9">90%</Option>
+			<Option key="0.8">80%</Option>
+			<Option key="0.7">70%</Option>
+			<Option key="0.6">60%</Option>
+			<Option key="0.5">50%</Option>
+			<Option key="0.01">1%</Option>
+		</Select>
+	);
+};
+
+export default Widget;

+ 22 - 0
dashboard/src/components/corpus/ChapterFilterType.tsx

@@ -0,0 +1,22 @@
+import { Select } from "antd";
+import React from "react";
+
+const { Option } = Select;
+
+const children: React.ReactNode[] = [];
+for (let i = 1; i < 5; i++) {
+	children.push(<Option key={i.toString(5) + i}>{i.toString(5) + i}</Option>);
+}
+
+const handleChange = (value: string[]) => {
+	console.log(`selected ${value}`);
+};
+const Widget = () => {
+	return (
+		<Select style={{ width: 100 }} allowClear placeholder="Type" defaultValue={[]} onChange={handleChange}>
+			{children}
+		</Select>
+	);
+};
+
+export default Widget;

+ 25 - 0
dashboard/src/components/corpus/ChapterHead.tsx

@@ -0,0 +1,25 @@
+import { Typography } from "antd";
+
+const { Title, Text } = Typography;
+
+export interface IChapterInfo {
+	title: string;
+	subTitle?: string;
+	summary?: string;
+	cover?: string;
+}
+interface IWidgetPaliChapterHeading {
+	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>
+		</>
+	);
+};
+
+export default Widget;

+ 48 - 0
dashboard/src/components/corpus/ChapterInChannel.tsx

@@ -0,0 +1,48 @@
+import { Col, Layout, Progress, Row, Space } 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";
+
+const { Text } = Typography;
+
+export interface IChapterChannelData {
+	channel: ChannelInfoProps;
+	progress: number;
+	hit: number;
+	like: number;
+	updatedAt: string;
+}
+interface IWidgetChapterInChannel {
+	data: IChapterChannelData[];
+}
+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>
+
+				<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}</>;
+};
+
+export default Widget;

+ 92 - 0
dashboard/src/components/corpus/ChapterList.tsx

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

+ 67 - 0
dashboard/src/components/corpus/PaliChapterCard.tsx

@@ -0,0 +1,67 @@
+import { Row, Col } from "antd";
+import { Typography } from "antd";
+import TocPath from "./TocPath";
+
+const { Title, Link } = Typography;
+
+export interface IPaliChapterData {
+	Title: string;
+	PaliTitle: string;
+	Path: string;
+	Book: number;
+	Paragraph: number;
+}
+
+interface IWidgetPaliChapterCard {
+	data: IPaliChapterData;
+	onTitleClick?: Function;
+}
+
+const Widget = (prop: IWidgetPaliChapterCard) => {
+	const path = JSON.parse(prop.data.Path);
+
+	return (
+		<>
+			<Row>
+				<Col span={3}>封面</Col>
+				<Col span={21}>
+					<Row>
+						<Col span={16}>
+							<Row>
+								<Col>
+									<Title
+										level={5}
+										onClick={(e) => {
+											if (typeof prop.onTitleClick !== "undefined") {
+												prop.onTitleClick(e);
+											}
+										}}
+									>
+										<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></Col>
+					</Row>
+					<Row>
+						<Col span={16}></Col>
+					</Row>
+				</Col>
+			</Row>
+		</>
+	);
+};
+
+export default Widget;

+ 47 - 0
dashboard/src/components/corpus/PaliChapterChannelList.tsx

@@ -0,0 +1,47 @@
+import { useState, useEffect } from "react";
+import { ApiFetch } from "../../utils";
+import { IApiResponseChapterChannelList } from "../api/Corpus";
+import { IParagraph } from "./BookViewer";
+import ChapterInChannel, { IChapterChannelData } from "./ChapterInChannel";
+
+interface IWidgetPaliChapterChannelList {
+	para: IParagraph;
+}
+const defaultData: IChapterChannelData[] = [];
+const Widget = (prop: 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]);
+
+	return (
+		<>
+			<ChapterInChannel data={tableData} />
+		</>
+	);
+};
+
+export default Widget;

+ 70 - 0
dashboard/src/components/corpus/PaliChapterHead.tsx

@@ -0,0 +1,70 @@
+import { useState, useEffect } from "react";
+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";
+
+interface IWidgetPaliChapterHead {
+	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]);
+
+	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} />
+		</>
+	);
+};
+
+export default Widget;

+ 38 - 0
dashboard/src/components/corpus/PaliChapterList.tsx

@@ -0,0 +1,38 @@
+import { List } from "antd";
+import PaliChapterCard, { IPaliChapterData } from "./PaliChapterCard";
+
+interface IWidgetPaliChapterList {
+	data: IPaliChapterData[];
+	onChapterClick?: Function;
+}
+
+export interface IChapterClickEvent {
+	para: IPaliChapterData;
+	event: React.MouseEvent<HTMLDivElement, MouseEvent>;
+}
+const Widget = (prop: IWidgetPaliChapterList) => {
+	return (
+		<List
+			itemLayout="vertical"
+			size="large"
+			dataSource={prop.data}
+			renderItem={(item) => (
+				<List.Item>
+					<PaliChapterCard
+						onTitleClick={(e: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
+							if (typeof prop.onChapterClick !== "undefined") {
+								prop.onChapterClick({
+									para: item,
+									event: e,
+								});
+							}
+						}}
+						data={item}
+					/>
+				</List.Item>
+			)}
+		/>
+	);
+};
+
+export default Widget;

+ 49 - 0
dashboard/src/components/corpus/PaliChapterListByPara.tsx

@@ -0,0 +1,49 @@
+import { useState, useEffect } from "react";
+import { ApiFetch } from "../../utils";
+import { IApiResponcePaliChapterList } from "../api/Corpus";
+import { IParagraph } from "./BookViewer";
+import { IPaliChapterData } from "./PaliChapterCard";
+import PaliChapterList, { IChapterClickEvent } from "./PaliChapterList";
+
+interface IWidgetPaliChapterListByPara {
+	para: IParagraph;
+	onChapterClick?: Function;
+}
+const defaultData: IPaliChapterData[] = [];
+const Widget = (prop: IWidgetPaliChapterListByPara) => {
+	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]);
+
+	return (
+		<>
+			<PaliChapterList
+				onChapterClick={(e: IChapterClickEvent) => {
+					if (prop.onChapterClick) {
+						prop.onChapterClick(e);
+					}
+				}}
+				data={tableData}
+			/>
+		</>
+	);
+};
+
+export default Widget;

+ 49 - 0
dashboard/src/components/corpus/PaliChapterListByTag.tsx

@@ -0,0 +1,49 @@
+import { useState, useEffect } from "react";
+import { ApiFetch } from "../../utils";
+import { IApiResponcePaliChapterList } from "../api/Corpus";
+import { IPaliChapterData } from "./PaliChapterCard";
+import PaliChapterList, { IChapterClickEvent } from "./PaliChapterList";
+
+interface IWidgetPaliChapterListByTag {
+	tag: string[];
+	onChapterClick?: Function;
+}
+const defaultData: IPaliChapterData[] = [];
+const Widget = (prop: IWidgetPaliChapterListByTag) => {
+	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]);
+
+	return (
+		<>
+			<PaliChapterList
+				data={tableData}
+				onChapterClick={(e: IChapterClickEvent) => {
+					if (typeof prop.onChapterClick !== "undefined") {
+						prop.onChapterClick(e);
+					}
+				}}
+			/>
+		</>
+	);
+};
+
+export default Widget;

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

@@ -0,0 +1,56 @@
+import { Link } from "react-router-dom";
+import { Breadcrumb } from "antd";
+
+export interface ITocPathNode {
+	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;
+}
+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>
+		</>
+	);
+};
+
+export default Widget;

+ 31 - 0
dashboard/src/components/dict/CaseList.tsx

@@ -0,0 +1,31 @@
+import { List, Card } from "antd";
+import { Row, Col } from "antd";
+
+export interface ICaseListData {
+	word: string;
+	count: number;
+}
+interface IWidgetCaseList {
+	data: ICaseListData[];
+}
+const Widget = (prop: IWidgetCaseList) => {
+	return (
+		<Card title="Case List">
+			<List
+				footer={"共计30"}
+				size="small"
+				dataSource={prop.data}
+				renderItem={(item) => (
+					<List.Item>
+						<Row>
+							<Col>{item.word}</Col>
+							<Col>{item.count}</Col>
+						</Row>
+					</List.Item>
+				)}
+			/>
+		</Card>
+	);
+};
+
+export default Widget;

+ 46 - 0
dashboard/src/components/dict/DictContent.tsx

@@ -0,0 +1,46 @@
+import { Col, Row } from "antd";
+
+import WordCard from "./WordCard";
+import CaseList from "./CaseList";
+import DictList from "./DictList";
+
+import type { IAnchorData } from "./DictList";
+import type { IWidgetWordCardData } from "./WordCard";
+import type { ICaseListData } from "./CaseList";
+
+export interface IWidgetDictContentData {
+	dictlist: IAnchorData[];
+	words: IWidgetWordCardData[];
+	caselist: ICaseListData[];
+}
+export interface IApiDictContentData {
+	ok: boolean;
+	message: string;
+	data: IWidgetDictContentData;
+}
+
+interface IWidgetDictContent {
+	data: IWidgetDictContentData;
+}
+
+const Widget = (prop: IWidgetDictContent) => {
+	return (
+		<>
+			<Row>
+				<Col flex="200px">
+					<DictList data={prop.data.dictlist} />
+				</Col>
+				<Col flex="760px">
+					{prop.data.words.map((it, id) => {
+						return <WordCard key={id} data={it} />;
+					})}
+				</Col>
+				<Col flex="200px">
+					<CaseList data={prop.data.caselist} />
+				</Col>
+			</Row>
+		</>
+	);
+};
+
+export default Widget;

+ 29 - 0
dashboard/src/components/dict/DictList.tsx

@@ -0,0 +1,29 @@
+import { Anchor } from "antd";
+const { Link } = Anchor;
+export interface IAnchorData {
+	href: string;
+	title: string;
+	children?: IAnchorData[];
+}
+interface IWidgetDictList {
+	data: IAnchorData[];
+}
+const Widget = (prop: IWidgetDictList) => {
+	function GetLink(anchors: IAnchorData[]) {
+		return anchors.map((it, id) => {
+			return (
+				<Link key={id} href={it.href} title={it.title}>
+					{it.children ? GetLink(it.children) : ""}
+				</Link>
+			);
+		});
+	}
+
+	return (
+		<>
+			<Anchor offsetTop={50}>{GetLink(prop.data)}</Anchor>
+		</>
+	);
+};
+
+export default Widget;

+ 41 - 0
dashboard/src/components/dict/DictSearch.tsx

@@ -0,0 +1,41 @@
+import { useState, useEffect } from "react";
+
+import DictContent from "./DictContent";
+import { ApiFetch } from "../../utils";
+import type { IWidgetDictContentData, IApiDictContentData } from "./DictContent";
+
+interface IWidgetDictSearch {
+	word: string | undefined;
+}
+
+const Widget = (prop: IWidgetDictSearch) => {
+	const defaultData: IWidgetDictContentData = {
+		dictlist: [],
+		words: [],
+		caselist: [],
+	};
+	const [tableData, setTableData] = useState(defaultData);
+
+	useEffect(() => {
+		console.log("useEffect");
+		const url = `/dict?word=${prop.word}`;
+		console.log("url", url);
+		ApiFetch(url)
+			.then((response) => {
+				const json = response as unknown as IApiDictContentData;
+				console.log("data", json);
+				setTableData(json.data);
+			})
+			.catch((error) => {
+				console.error(error);
+			});
+	}, [prop.word, setTableData]);
+
+	return (
+		<>
+			<DictContent data={tableData} />
+		</>
+	);
+};
+
+export default Widget;

+ 51 - 0
dashboard/src/components/dict/GrammarPop.tsx

@@ -0,0 +1,51 @@
+/* eslint-disable jsx-a11y/anchor-is-valid */
+import { useState } from "react";
+import { Popover } from "antd";
+import { ProCard } from "@ant-design/pro-components";
+import ReactMarkdown from "react-markdown";
+
+import { ApiGetText } from "../../utils";
+
+interface IWidgetGrammarPop {
+	text: string;
+	gid: string;
+}
+const Widget = (prop: IWidgetGrammarPop) => {
+	const [guide, setGuide] = useState("Loading");
+	const grammarProfix = "guide-grammar-";
+	const handleMouseMouseEnter = () => {
+		console.log("mouseenter", prop.gid);
+		//sessionStorage缓存
+		const value = sessionStorage.getItem(grammarProfix + prop.gid);
+		if (value === null) {
+			fetchData(prop.gid);
+		} else {
+			const sGuide: string = value ? value : "";
+			setGuide(sGuide);
+		}
+	};
+	const userCard = (
+		<>
+			<ProCard style={{ maxWidth: 500, minWidth: 300, margin: 0 }}>
+				<ReactMarkdown>{guide}</ReactMarkdown>
+			</ProCard>
+		</>
+	);
+	function fetchData(key: string) {
+		const url = `/guide/zh-cn/${key}`;
+		ApiGetText(url).then((response: String) => {
+			const text = response as unknown as string;
+			sessionStorage.setItem(grammarProfix + key, text);
+			setGuide(text);
+		});
+	}
+	return (
+		<Popover content={userCard} placement="bottom">
+			<a href="#" onMouseEnter={handleMouseMouseEnter}>
+				{prop.text}
+			</a>
+		</Popover>
+	);
+};
+
+export default Widget;

+ 56 - 0
dashboard/src/components/dict/WordCard.tsx

@@ -0,0 +1,56 @@
+/* eslint-disable jsx-a11y/anchor-is-valid */
+import { Typography } from "antd";
+import IWidgetGrammarPop from "./GrammarPop";
+import WordCardByDict from "./WordCardByDict";
+
+import type { IWordByDict } from "./WordCardByDict";
+
+const { Title, Text } = Typography;
+
+export interface IWidgetWordCardData {
+	word: string;
+	factors: string;
+	parents: string;
+	case: string[];
+	anchor: string;
+	dict: IWordByDict[];
+}
+interface IWidgetWordCard {
+	data: IWidgetWordCardData;
+}
+const Widget = (prop: IWidgetWordCard) => {
+	const caseList = prop.data.case.map((element) => {
+		return element.split("|").map((it, id) => {
+			if (it.slice(0, 1) === "@") {
+				const [showText, keyText] = it.slice(1).split("-");
+				return <IWidgetGrammarPop key={id} gid={keyText} text={showText} />;
+			} else {
+				return <span key={id * 200}>{it}</span>;
+			}
+		});
+	});
+	return (
+		<>
+			<Title level={4} id={prop.data.anchor}>
+				{prop.data.word}
+			</Title>
+
+			<div>
+				<Text>{prop.data.factors}</Text>
+			</div>
+			<div>
+				<Text>{prop.data.parents}</Text>
+			</div>
+			<div>
+				<Text>{caseList}</Text>
+			</div>
+			<div>
+				{prop.data.dict.map((it, id) => {
+					return <WordCardByDict key={id} data={it} />;
+				})}
+			</div>
+		</>
+	);
+};
+
+export default Widget;

+ 39 - 0
dashboard/src/components/dict/WordCardByDict.tsx

@@ -0,0 +1,39 @@
+/* eslint-disable jsx-a11y/anchor-is-valid */
+import { Card } from "antd";
+import { Typography } from "antd";
+import IWidgetGrammarPop from "./GrammarPop";
+
+const { Title, Text } = Typography;
+
+export interface IWordByDict {
+	dictname: string;
+	word: string;
+	note: string;
+	anchor: string;
+}
+interface IWidgetWordCardByDict {
+	data: IWordByDict;
+}
+const Widget = (prop: IWidgetWordCardByDict) => {
+	return (
+		<Card>
+			<Title level={5} id={prop.data.anchor}>
+				{prop.data.dictname}
+			</Title>
+			<div>
+				<Text>
+					{prop.data.note.split("|").map((it, id) => {
+						if (it.slice(0, 1) === "@") {
+							const [showText, keyText] = it.slice(1).split("-");
+							return <IWidgetGrammarPop key={id} gid={keyText} text={showText} />;
+						} else {
+							return <span key={id * 200}>{it}</span>;
+						}
+					})}
+				</Text>
+			</div>
+		</Card>
+	);
+};
+
+export default Widget;

+ 39 - 0
dashboard/src/components/general/UiLangSelect.tsx

@@ -0,0 +1,39 @@
+import { Dropdown, Menu, 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 Widget = () => {
+	// TODO
+	return (
+		<Dropdown overlay={menu} placement="bottomRight">
+			<Button ghost icon={<GlobalOutlined />}>
+				简体中文
+			</Button>
+		</Dropdown>
+	);
+};
+
+export default Widget;

+ 0 - 26
dashboard/src/components/library/Footer.tsx

@@ -1,26 +0,0 @@
-import { Link } from "react-router-dom";
-import { Space } from "antd";
-import { useIntl } from "react-intl";
-
-const Widget = () => {
-	//Library foot bar
-	const intl = useIntl();//i18n
-	// TODO
-  return (
-	<div>
-		<p>底部区域</p>
-		<Space>
-
-			<Link to="/">
-				联系方式
-			</Link>
-			<Link to="/">
-				{intl.formatMessage({ id: "columns.library.palicanon.title" })}
-			</Link>
-		</Space>		
-	</div>
-
-  );
-};
-
-export default Widget;

+ 26 - 0
dashboard/src/components/library/FooterBar.tsx

@@ -0,0 +1,26 @@
+import { Link } from "react-router-dom";
+import { Layout, Row, Col } from "antd";
+import { useIntl } from "react-intl";
+
+const { Footer } = Layout;
+
+const Widget = () => {
+	//Library foot bar
+	const intl = useIntl(); //i18n
+	// TODO
+	return (
+		<Footer>
+			<Row>
+				<Col span={8}>相关链接</Col>
+				<Col span={16}>
+					问题收集<Link to="/">{intl.formatMessage({ id: "columns.library.palicanon.title" })}</Link>
+				</Col>
+			</Row>
+			<Row>
+				<Col>Powered by PCDS</Col>
+			</Row>
+		</Footer>
+	);
+};
+
+export default Widget;

+ 82 - 44
dashboard/src/components/library/HeadBar.tsx

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

+ 0 - 0
dashboard/src/components/library/palicanon/.txt


+ 21 - 18
dashboard/src/components/studio/EditableTree.tsx

@@ -9,7 +9,7 @@ type TreeNodeData = {
 	level: number;
 };
 export type ListNodeData = {
-	article: string;
+	key: string;
 	title: string;
 	level: number;
 };
@@ -27,7 +27,7 @@ function tocGetTreeData(articles: ListNodeData[], active = "") {
 	for (let index = 0; index < articles.length; index++) {
 		const element = articles[index];
 
-		let newNode: TreeNodeData = { key: element.article, 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";
@@ -53,7 +53,7 @@ function tocGetTreeData(articles: ListNodeData[], active = "") {
 		lastInsNode = newNode;
 		iCurrLevel = newNode.level;
 
-		if (active === element.article) {
+		if (active === element.key) {
 			tocActivePath = [];
 			for (let index = 1; index < treeParents.length; index++) {
 				//treeParents[index]["expanded"] = true;
@@ -65,12 +65,14 @@ function tocGetTreeData(articles: ListNodeData[], active = "") {
 }
 
 type IWidgetEditableTree = {
-	treeData?: ListNodeData[];
+	treeData: ListNodeData[];
 };
-const Widget = ({ treeData = [] }: IWidgetEditableTree) => {
-	const data = tocGetTreeData(treeData);
+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"]);
+
+	//const [expandedKeys] = useState(["0-0", "0-0-0", "0-0-0-0"]);
 
 	const onDragEnter: TreeProps["onDragEnter"] = (info) => {
 		console.log(info);
@@ -141,19 +143,20 @@ const Widget = ({ treeData = [] }: IWidgetEditableTree) => {
 			}
 		}
 		setGData(data);
-		console.log(gData);
 	};
+
 	return (
-		<Tree
-			className="draggable-tree"
-			defaultExpandedKeys={expandedKeys}
-			draggable
-			blockNode
-			multiple
-			onDragEnter={onDragEnter}
-			onDrop={onDrop}
-			treeData={gData}
-		/>
+		<>
+			<Tree
+				rootClassName="draggable-tree"
+				//defaultExpandedKeys={expandedKeys}
+				draggable
+				blockNode
+				onDragEnter={onDragEnter}
+				onDrop={onDrop}
+				treeData={gData}
+			/>
+		</>
 	);
 };
 

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

@@ -1,7 +1,9 @@
 import { Link } from "react-router-dom";
-import { Col, Row, Input, Layout } from "antd";
+import { Col, Row, Input, Layout, Button } from "antd";
 
 import img_banner from "../../assets/studio/images/wikipali_banner.svg";
+import UiLangSelect from "../general/UiLangSelect";
+import SignInAvatar from "../auth/SignInAvatar";
 
 const { Search } = Input;
 const { Header } = Layout;
@@ -11,16 +13,22 @@ const onSearch = (value: string) => console.log(value);
 const Widget = () => {
 	return (
 		<Header className="header">
-			<Row>
-				<Col flex="100px">
+			<Row justify="space-between">
+				<Col flex="80px">
 					<Link to="/">
-						<img alt="code" src={img_banner} />
+						<img alt="code" style={{ height: "3em" }} src={img_banner} />
 					</Link>
 				</Col>
-				<Col flex="auto">
-					<Search placeholder="input search text" onSearch={onSearch} style={{ width: 200 }} />
+				<Col span={8}>
+					<Search placeholder="input search text" onSearch={onSearch} style={{ width: "100%" }} />
+				</Col>
+				<Col span={4}>
+					<Link to="\">
+						<Button>藏经阁</Button>
+					</Link>
+					<SignInAvatar />
+					<UiLangSelect />
 				</Col>
-				<Col flex="200px">登录信息</Col>
 			</Row>
 		</Header>
 	);

+ 4 - 0
dashboard/src/components/studio/SelectCase.tsx

@@ -50,6 +50,10 @@ const Widget = () => {
 			label: intl.formatMessage({ id: "dict.fields.type.pl.label" }),
 			children: case8,
 		},
+		{
+			value: "base",
+			label: intl.formatMessage({ id: "dict.fields.type.base.label" }),
+		},
 	];
 	const case3 = [
 		{

+ 5 - 1
dashboard/src/components/studio/SelectLang.tsx

@@ -19,7 +19,11 @@ const Widget = () => {
 		{ 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) => <Option value={d.value}>{d.lable}</Option>);
+	const langOptions = data.map((d, id) => (
+		<Option key={id} value={d.value}>
+			{d.lable}
+		</Option>
+	));
 	return (
 		<Select
 			showSearch

+ 2 - 1
dashboard/src/components/studio/dict/DictCreate.tsx

@@ -24,7 +24,7 @@ type IWidgetDictCreate = {
 };
 const Widget = (param: IWidgetDictCreate) => {
 	const intl = useIntl();
-
+	/*
 	const onLangChange = (value: string) => {
 		console.log(`selected ${value}`);
 	};
@@ -32,6 +32,7 @@ const Widget = (param: IWidgetDictCreate) => {
 	const onLangSearch = (value: string) => {
 		console.log("search:", value);
 	};
+	*/
 	return (
 		<Layout>
 			<ProForm<IFormData>

+ 41 - 0
dashboard/src/components/studio/group/GroupCreate.tsx

@@ -0,0 +1,41 @@
+import { ProForm, ProFormText } from "@ant-design/pro-components";
+import { useIntl } from "react-intl";
+import { message } from "antd";
+
+interface IFormData {
+	name: string;
+}
+
+type IWidgetGroupCreate = {
+	studio: string | undefined;
+};
+const Widget = (param: IWidgetGroupCreate) => {
+	const intl = useIntl();
+
+	return (
+		<ProForm<IFormData>
+			onFinish={async (values: IFormData) => {
+				// TODO
+				console.log(values);
+				message.success(intl.formatMessage({ id: "flashes.success" }));
+			}}
+		>
+			<ProForm.Group>
+				<ProFormText
+					width="md"
+					name="name"
+					required
+					label={intl.formatMessage({ id: "channel.name" })}
+					rules={[
+						{
+							required: true,
+							message: intl.formatMessage({ id: "channel.create.message.noname" }),
+						},
+					]}
+				/>
+			</ProForm.Group>
+		</ProForm>
+	);
+};
+
+export default Widget;

+ 78 - 84
dashboard/src/components/studio/group/GroupFile.tsx

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

+ 73 - 74
dashboard/src/components/studio/group/GroupMember.tsx

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

+ 95 - 0
dashboard/src/components/studio/term/TermCreate.tsx

@@ -0,0 +1,95 @@
+import { ProForm, ProFormText, ProFormTextArea } from "@ant-design/pro-components";
+import { Layout } from "antd";
+import { useIntl } from "react-intl";
+import { message } from "antd";
+
+import SelectLang from "../SelectLang";
+
+interface IFormData {
+	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 default Widget;

+ 23 - 0
dashboard/src/components/tag/TagArea.tsx

@@ -0,0 +1,23 @@
+import { Tag } from "antd";
+
+export interface TagNode {
+	id: string;
+	name: string;
+	description?: string;
+}
+interface IWidgetTagArea {
+	data: TagNode[];
+}
+const Widget = (prop: IWidgetTagArea) => {
+	// TODO
+	const tags = prop.data.map((item, id) => {
+		return (
+			<Tag color="green" key={id}>
+				{item.name}
+			</Tag>
+		);
+	});
+	return <>{tags}</>;
+};
+
+export default Widget;

+ 73 - 0
dashboard/src/components/utilities/TimeShow.tsx

@@ -0,0 +1,73 @@
+import { Space, Tooltip } from "antd";
+import { useIntl } from "react-intl";
+import { FieldTimeOutlined } from "@ant-design/icons";
+
+interface IWidgetTimeShow {
+	showIcon?: boolean;
+	showTitle?: boolean;
+	showTooltip?: boolean;
+	time: string;
+	title: string;
+}
+
+const Widget = ({ showIcon = true, showTitle = false, showTooltip = true, time, title }: IWidgetTimeShow) => {
+	const intl = useIntl(); //i18n
+
+	const icon = showIcon ? <FieldTimeOutlined /> : <></>;
+	const strTitle = showTitle ? title : "";
+
+	const passTime: string = getPassDataTime(time);
+	const tooltip: string = getFullDataTime(time);
+	const color = "lime";
+	function getPassDataTime(t: string): string {
+		let currDate = new Date();
+		const time = new Date(t);
+		let pass = currDate.getTime() - time.getTime();
+		let strPassTime = "";
+		if (pass < 120 * 1000) {
+			//二分钟内
+			strPassTime = Math.floor(pass / 1000) + intl.formatMessage({ id: "utilities.time.secs_ago" });
+		} else if (pass < 7200 * 1000) {
+			//二小时内
+			strPassTime = Math.floor(pass / 1000 / 60) + intl.formatMessage({ id: "utilities.time.mins_ago" });
+		} else if (pass < 3600 * 48 * 1000) {
+			//二天内
+			strPassTime = Math.floor(pass / 1000 / 3600) + intl.formatMessage({ id: "utilities.time.hs_ago" });
+		} else if (pass < 3600 * 24 * 14 * 1000) {
+			//二周内
+			strPassTime = Math.floor(pass / 1000 / 3600 / 24) + intl.formatMessage({ id: "utilities.time.days_ago" });
+		} else if (pass < 3600 * 24 * 60 * 1000) {
+			//二个月内
+			strPassTime =
+				Math.floor(pass / 1000 / 3600 / 24 / 7) + intl.formatMessage({ id: "utilities.time.weeks_ago" });
+		} else if (pass < 3600 * 24 * 365 * 1000) {
+			//一年内
+			strPassTime =
+				Math.floor(pass / 1000 / 3600 / 24 / 30) + intl.formatMessage({ id: "utilities.time.months_ago" });
+		} else if (pass < 3600 * 24 * 730 * 1000) {
+			//超过1年小于2年
+			strPassTime =
+				Math.floor(pass / 1000 / 3600 / 24 / 365) + intl.formatMessage({ id: "utilities.time.year_ago" });
+		} else {
+			strPassTime =
+				Math.floor(pass / 1000 / 3600 / 24 / 365) + intl.formatMessage({ id: "utilities.time.years_ago" });
+		}
+		return strPassTime;
+	}
+	function getFullDataTime(t: string) {
+		let inputDate = new Date(t);
+		return inputDate.toLocaleString();
+	}
+
+	return (
+		<Tooltip title={tooltip} color={color} key={color}>
+			<Space>
+				{icon}
+				{strTitle}
+				{passTime}
+			</Space>
+		</Tooltip>
+	);
+};
+
+export default Widget;

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

@@ -27,6 +27,7 @@ const items = {
 	"dict.fields.type.inst.label": "工具格",
 	"dict.fields.type.voc.label": "呼格",
 	"dict.fields.type.abl.label": "来源格",
+	"dict.fields.type.base.label": "词干",
 };
 
 export default items;

+ 12 - 9
dashboard/src/locales/zh-Hans/term/index.ts

@@ -1,13 +1,16 @@
 const items = {
-  "term": "字典",
-  "term.fields.sn.label": "序号",
-  "term.fields.word.label": "词头",
-  "term.fields.description.label": "标签",
-  "term.fields.description.tooltip": "单词说明,可以用来区分相同拼写的不同单词。",
-  "term.fields.channel.label": "所属版本",
-  "term.fields.meaning.label": "意思",
-  "term.fields.meaning2.label": "其他意思",
-  "term.fields.note.label": "注释",
+	term: "字典",
+	"term.fields.sn.label": "序号",
+	"term.fields.word.label": "词头",
+	"term.fields.description.label": "标签",
+	"term.fields.description.tooltip": "单词说明,可以用来区分相同拼写的不同单词。",
+	"term.fields.channel.label": "所属版本",
+	"term.fields.channel.tooltip": "该术语仅仅用于某个版本",
+	"term.fields.meaning.label": "意思",
+	"term.fields.meaning.tooltip": "单词的首选意思",
+	"term.fields.meaning2.label": "其他意思",
+	"term.fields.meaning2.tooltip": "其他意思将出现在后面的括号里",
+	"term.fields.note.label": "注释",
 };
 
 export default items;

+ 12 - 12
dashboard/src/pages/library/anthology/article.tsx

@@ -2,22 +2,22 @@ import { Space } from "antd";
 import { useParams } from "react-router-dom";
 
 import HeadBar from "../../../components/library/HeadBar";
-import Footer from "../../../components/library/Footer";
+import FooterBar from "../../../components/library/FooterBar";
 
 const Widget = () => {
 	// TODO
-	const { article_id } = useParams();//url 参数
+	const { article_id } = useParams(); //url 参数
 
-  return (
-    <div>
-		<HeadBar />
-      <div>文章阅读器{article_id}</div>
-      <div>
-		<Space>主显示区</Space>
-      </div>
-		<Footer />
-    </div>
-  );
+	return (
+		<div>
+			<HeadBar />
+			<div>文章阅读器{article_id}</div>
+			<div>
+				<Space>主显示区</Space>
+			</div>
+			<FooterBar />
+		</div>
+	);
 };
 
 export default Widget;

+ 54 - 16
dashboard/src/pages/library/anthology/index.tsx

@@ -1,24 +1,62 @@
-import { Space } from "antd";
-import { Link } from "react-router-dom";
+import { Space, Input } from "antd";
+import { Layout, Affix, Col, Row } from "antd";
 
 import HeadBar from "../../../components/library/HeadBar";
-import Footer from "../../../components/library/Footer";
+import FooterBar from "../../../components/library/FooterBar";
+import AnthologyList from "../../../components/article/AnthologyList";
+import AnthologStudioList from "../../../components/article/AnthologStudioList";
+
+const { Content, Header } = Layout;
+const { Search } = Input;
 
 const Widget = () => {
 	// TODO
-  return (
-    <div>
-		<HeadBar selectedKeys="anthology"/>
-      <div>文集首页</div>
-	  <div>
-	  	<Space>
-			<Link to="/anthology/show/12345">文集1</Link>
-			<Link to="/article/show/23456">文章1</Link>
-		</Space>
-	  </div>
-		<Footer />
-    </div>
-  );
+	const onSearch = (value: string) => console.log(value);
+	return (
+		<Layout>
+			<HeadBar selectedKeys="anthology" />
+			<Layout>
+				<Header style={{ color: "#FFF" }}>
+					<h2>composition</h2>
+					<p>
+						Make the Pāḷi easy to read <br />
+						solution of Pāḷi glossary For translating <br />
+						Pāḷi in Group Show the source reference in Pāḷi
+					</p>
+				</Header>
+				<Affix offsetTop={0}>
+					<Header style={{ backgroundColor: "gray", height: "3.5em" }}>
+						<Row style={{ paddingTop: "0.5em" }}>
+							<Col span="8" offset={8}>
+								<Search placeholder="input search text" onSearch={onSearch} style={{ width: "100%" }} />
+							</Col>
+						</Row>
+					</Header>
+				</Affix>
+
+				<Content>
+					<Row>
+						<Col flex="auto"></Col>
+						<Col flex="1260px">
+							<Row>
+								<Col span="18">
+									<AnthologyList />
+								</Col>
+								<Col span="6">
+									<AnthologStudioList />
+								</Col>
+							</Row>
+						</Col>
+						<Col flex="auto"></Col>
+					</Row>
+
+					<Space></Space>
+				</Content>
+			</Layout>
+
+			<FooterBar />
+		</Layout>
+	);
 };
 
 export default Widget;

+ 45 - 14
dashboard/src/pages/library/anthology/show.tsx

@@ -1,22 +1,53 @@
-import { Space } from "antd";
 import { useParams } from "react-router-dom";
-
+import { Layout, Affix, Col, Row } from "antd";
 import HeadBar from "../../../components/library/HeadBar";
-import Footer from "../../../components/library/Footer";
+import FooterBar from "../../../components/library/FooterBar";
+import AnthologyDetail from "../../../components/article/AnthologyDetail";
+
+const { Content, Header } = Layout;
 
 const Widget = () => {
 	// TODO
-	const { anthology_id } = useParams();//url 参数
-  return (
-    <div>
-		<HeadBar selectedKeys="anthology"/>
-      <div>文集{anthology_id}详情</div>
-      <div>
-		<Space>主显示区</Space>
-      </div>
-		<Footer />
-    </div>
-  );
+	const { id, tags } = useParams(); //url 参数
+	let aid = id ? id : "";
+	let channel = tags ? tags : "";
+
+	const pageMaxWidth = "1260px";
+	return (
+		<Layout>
+			<HeadBar selectedKeys="anthology" />
+			<Layout>
+				<Affix offsetTop={0}>
+					<Header style={{ backgroundColor: "gray", height: "3.5em" }}>
+						<Col flex="auto"></Col>
+						<Col flex={pageMaxWidth}>
+							<div>
+								{aid}@{channel}
+							</div>
+						</Col>
+						<Col flex="auto"></Col>
+					</Header>
+				</Affix>
+
+				<Content>
+					<Row>
+						<Col flex="auto"></Col>
+						<Col flex={pageMaxWidth}>
+							<Row>
+								<Col span="18">
+									<AnthologyDetail aid={aid} />
+								</Col>
+								<Col span="6"></Col>
+							</Row>
+						</Col>
+						<Col flex="auto"></Col>
+					</Row>
+				</Content>
+			</Layout>
+
+			<FooterBar />
+		</Layout>
+	);
 };
 
 export default Widget;

+ 15 - 15
dashboard/src/pages/library/blog/anthology.tsx

@@ -1,23 +1,23 @@
 import { Space } from "antd";
 import { useParams, Link } from "react-router-dom";
 import HeadBar from "../../../components/library/blog/HeadBar";
-import Footer from "../../../components/library/Footer";
+import FooterBar from "../../../components/library/FooterBar";
 const Widget = () => {
 	// TODO
-	const { courseid } = useParams();//url 参数
-  return (
-    <div>
-	<HeadBar />
-      <div>课程{courseid} 详情</div>
-	  <div>
-	  	<Space>
-			<Link to="/course/lesson/12345">lesson 1</Link>
-			<Link to="/course/lesson/23456">lesson 2</Link>
-		</Space>
-	  </div>
-	  <Footer />
-    </div>
-  );
+	const { courseid } = useParams(); //url 参数
+	return (
+		<div>
+			<HeadBar />
+			<div>课程{courseid} 详情</div>
+			<div>
+				<Space>
+					<Link to="/course/lesson/12345">lesson 1</Link>
+					<Link to="/course/lesson/23456">lesson 2</Link>
+				</Space>
+			</div>
+			<FooterBar />
+		</div>
+	);
 };
 
 export default Widget;

+ 12 - 12
dashboard/src/pages/library/blog/course.tsx

@@ -1,22 +1,22 @@
 import { Space } from "antd";
 import { useParams } from "react-router-dom";
 import HeadBar from "../../../components/library/blog/HeadBar";
-import Footer from "../../../components/library/Footer";
+import FooterBar from "../../../components/library/FooterBar";
 
 const Widget = () => {
 	// TODO
-	const { lessonid } = useParams();//url 参数
+	const { lessonid } = useParams(); //url 参数
 
-  return (
-    <div>
-		<HeadBar />
-      <div>课 {lessonid} 详情</div>
-      <div>
-		<Space>主显示区</Space>
-      </div>
-	  <Footer />
-    </div>
-  );
+	return (
+		<div>
+			<HeadBar />
+			<div>课 {lessonid} 详情</div>
+			<div>
+				<Space>主显示区</Space>
+			</div>
+			<FooterBar />
+		</div>
+	);
 };
 
 export default Widget;

+ 14 - 14
dashboard/src/pages/library/blog/index.tsx

@@ -1,23 +1,23 @@
 import { Space } from "antd";
 import { Link } from "react-router-dom";
 import HeadBar from "../../../components/library/blog/HeadBar";
-import Footer from "../../../components/library/Footer";
+import FooterBar from "../../../components/library/FooterBar";
 
 const Widget = () => {
 	// TODO
-  return (
-    <div>
-		<HeadBar />
-      <div>课程首页</div>
-	  <div>
-	  	<Space>
-			<Link to="/course/show/12345">课程1</Link>
-			<Link to="/course/show/23456">课程2</Link>
-		</Space>
-	  </div>
-		<Footer />
-    </div>
-  );
+	return (
+		<div>
+			<HeadBar />
+			<div>课程首页</div>
+			<div>
+				<Space>
+					<Link to="/course/show/12345">课程1</Link>
+					<Link to="/course/show/23456">课程2</Link>
+				</Space>
+			</div>
+			<FooterBar />
+		</div>
+	);
 };
 
 export default Widget;

+ 12 - 12
dashboard/src/pages/library/blog/term.tsx

@@ -1,22 +1,22 @@
 import { Space } from "antd";
 import { useParams } from "react-router-dom";
 import HeadBar from "../../../components/library/blog/HeadBar";
-import Footer from "../../../components/library/Footer";
+import FooterBar from "../../../components/library/FooterBar";
 
 const Widget = () => {
 	// TODO
-	const { lessonid } = useParams();//url 参数
+	const { lessonid } = useParams(); //url 参数
 
-  return (
-    <div>
-		<HeadBar />
-      <div>课 {lessonid} 详情</div>
-      <div>
-		<Space>主显示区</Space>
-      </div>
-	  <Footer />
-    </div>
-  );
+	return (
+		<div>
+			<HeadBar />
+			<div>课 {lessonid} 详情</div>
+			<div>
+				<Space>主显示区</Space>
+			</div>
+			<FooterBar />
+		</div>
+	);
 };
 
 export default Widget;

+ 15 - 15
dashboard/src/pages/library/blog/translation.tsx

@@ -1,23 +1,23 @@
 import { Space } from "antd";
 import { useParams, Link } from "react-router-dom";
 import HeadBar from "../../../components/library/blog/HeadBar";
-import Footer from "../../../components/library/Footer";
+import FooterBar from "../../../components/library/FooterBar";
 const Widget = () => {
 	// TODO
-	const { courseid } = useParams();//url 参数
-  return (
-    <div>
-	<HeadBar />
-      <div>课程{courseid} 详情</div>
-	  <div>
-	  	<Space>
-			<Link to="/course/lesson/12345">lesson 1</Link>
-			<Link to="/course/lesson/23456">lesson 2</Link>
-		</Space>
-	  </div>
-	  <Footer />
-    </div>
-  );
+	const { courseid } = useParams(); //url 参数
+	return (
+		<div>
+			<HeadBar />
+			<div>课程{courseid} 详情</div>
+			<div>
+				<Space>
+					<Link to="/course/lesson/12345">lesson 1</Link>
+					<Link to="/course/lesson/23456">lesson 2</Link>
+				</Space>
+			</div>
+			<FooterBar />
+		</div>
+	);
 };
 
 export default Widget;

+ 18 - 12
dashboard/src/pages/library/community/index.tsx

@@ -1,19 +1,25 @@
-import { Space } from "antd";
+import { Outlet } from "react-router-dom";
+import { Layout, Row, Col } from "antd";
+
 import HeadBar from "../../../components/library/HeadBar";
-import Footer from "../../../components/library/Footer";
+import FooterBar from "../../../components/library/FooterBar";
 
 const Widget = () => {
 	// TODO
-  return (
-    <div>
-		<HeadBar selectedKeys="community" />
-      <div>最新译文</div>
-      <div>
-		<Space>主显示区</Space>
-      </div>
-		<Footer />
-    </div>
-  );
+	return (
+		<Layout>
+			<HeadBar selectedKeys="community" />
+			<Row>
+				<Col flex="auto"></Col>
+
+				<Col flex="1260px">
+					<Outlet />
+				</Col>
+				<Col flex="auto"></Col>
+			</Row>
+			<FooterBar />
+		</Layout>
+	);
 };
 
 export default Widget;

+ 34 - 0
dashboard/src/pages/library/community/list.tsx

@@ -0,0 +1,34 @@
+import { Layout, Affix, Row, Col } from "antd";
+
+import HeadBar from "../../../components/library/HeadBar";
+import FooterBar from "../../../components/library/FooterBar";
+import ChannelList from "../../../components/channel/ChannelList";
+import BookTree from "../../../components/corpus/BookTree";
+import ChapterFileter from "../../../components/corpus/ChapterFilter";
+import ChapterList from "../../../components/corpus/ChapterList";
+const { Sider, Content } = Layout;
+
+const Widget = () => {
+	// TODO
+	return (
+		<Row>
+			<Col xs={0} xl={6}>
+				<Affix offsetTop={0}>
+					<Layout style={{ height: "100vh", overflowY: "scroll" }}>
+						<BookTree />
+						<ChannelList />
+					</Layout>
+				</Affix>
+			</Col>
+			<Col xs={24} xl={14}>
+				<ChapterFileter />
+				<ChapterList />
+			</Col>
+			<Col xs={0} xl={4}>
+				侧边栏 侧边栏 侧边栏 侧边栏 侧边栏
+			</Col>
+		</Row>
+	);
+};
+
+export default Widget;

+ 15 - 15
dashboard/src/pages/library/course/course.tsx

@@ -1,23 +1,23 @@
 import { Space } from "antd";
 import { useParams, Link } from "react-router-dom";
 import HeadBar from "../../../components/library/HeadBar";
-import Footer from "../../../components/library/Footer";
+import FooterBar from "../../../components/library/FooterBar";
 const Widget = () => {
 	// TODO
-	const { courseid } = useParams();//url 参数
-  return (
-    <div>
-	<HeadBar  selectedKeys="course"/>
-      <div>课程{courseid} 详情</div>
-	  <div>
-	  	<Space>
-			<Link to="/course/lesson/12345">lesson 1</Link>
-			<Link to="/course/lesson/23456">lesson 2</Link>
-		</Space>
-	  </div>
-	  <Footer />
-    </div>
-  );
+	const { courseid } = useParams(); //url 参数
+	return (
+		<div>
+			<HeadBar selectedKeys="course" />
+			<div>课程{courseid} 详情</div>
+			<div>
+				<Space>
+					<Link to="/course/lesson/12345">lesson 1</Link>
+					<Link to="/course/lesson/23456">lesson 2</Link>
+				</Space>
+			</div>
+			<FooterBar />
+		</div>
+	);
 };
 
 export default Widget;

+ 14 - 14
dashboard/src/pages/library/course/index.tsx

@@ -1,23 +1,23 @@
 import { Space } from "antd";
 import { Link } from "react-router-dom";
 import HeadBar from "../../../components/library/HeadBar";
-import Footer from "../../../components/library/Footer";
+import FooterBar from "../../../components/library/FooterBar";
 
 const Widget = () => {
 	// TODO
-  return (
-    <div>
-		<HeadBar selectedKeys="course"/>
-      <div>课程首页</div>
-	  <div>
-	  	<Space>
-			<Link to="/course/show/12345">课程1</Link>
-			<Link to="/course/show/23456">课程2</Link>
-		</Space>
-	  </div>
-		<Footer />
-    </div>
-  );
+	return (
+		<div>
+			<HeadBar selectedKeys="course" />
+			<div>课程首页</div>
+			<div>
+				<Space>
+					<Link to="/course/show/12345">课程1</Link>
+					<Link to="/course/show/23456">课程2</Link>
+				</Space>
+			</div>
+			<FooterBar />
+		</div>
+	);
 };
 
 export default Widget;

+ 12 - 12
dashboard/src/pages/library/course/lesson.tsx

@@ -1,22 +1,22 @@
 import { Space } from "antd";
 import { useParams } from "react-router-dom";
 import HeadBar from "../../../components/library/HeadBar";
-import Footer from "../../../components/library/Footer";
+import FooterBar from "../../../components/library/FooterBar";
 
 const Widget = () => {
 	// TODO
-	const { lessonid } = useParams();//url 参数
+	const { lessonid } = useParams(); //url 参数
 
-  return (
-    <div>
-		<HeadBar  selectedKeys="course"/>
-      <div>课 {lessonid} 详情</div>
-      <div>
-		<Space>主显示区</Space>
-      </div>
-	  <Footer />
-    </div>
-  );
+	return (
+		<div>
+			<HeadBar selectedKeys="course" />
+			<div>课 {lessonid} 详情</div>
+			<div>
+				<Space>主显示区</Space>
+			</div>
+			<FooterBar />
+		</div>
+	);
 };
 
 export default Widget;

+ 11 - 12
dashboard/src/pages/library/dict/index.tsx

@@ -1,19 +1,18 @@
-import { Space } from "antd";
+import { Outlet } from "react-router-dom";
+import { Layout } from "antd";
+
 import HeadBar from "../../../components/library/HeadBar";
-import Footer from "../../../components/library/Footer";
+import FooterBar from "../../../components/library/FooterBar";
 
 const Widget = () => {
 	// TODO
-  return (
-    <div>
-		<HeadBar  selectedKeys="dict"/>
-      <div>字典首页</div>
-      <div>
-		<Space>主显示区</Space>
-      </div>
-		<Footer />
-    </div>
-  );
+	return (
+		<Layout>
+			<HeadBar selectedKeys="dict" />
+			<Outlet />
+			<FooterBar />
+		</Layout>
+	);
 };
 
 export default Widget;

+ 36 - 0
dashboard/src/pages/library/dict/recent.tsx

@@ -0,0 +1,36 @@
+import { useNavigate } from "react-router-dom";
+import { Layout, Affix, Col, Row } from "antd";
+import { Input } from "antd";
+
+const { Content, Header } = Layout;
+const { Search } = Input;
+
+const Widget = () => {
+	const navigate = useNavigate();
+
+	const onSearch = (value: string) => {
+		navigate("/dict/" + value);
+	};
+	return (
+		<Layout>
+			<Affix offsetTop={0}>
+				<Header style={{ backgroundColor: "gray", height: "3.5em" }}>
+					<Row style={{ paddingTop: "0.5em" }}>
+						<Col span="8" offset={8}>
+							<Search placeholder="input search text" onSearch={onSearch} style={{ width: "100%" }} />
+						</Col>
+					</Row>
+				</Header>
+			</Affix>
+			<Content>
+				<Row>
+					<Col flex="auto"></Col>
+					<Col flex="1260px">最近搜索列表</Col>
+					<Col flex="auto"></Col>
+				</Row>
+			</Content>
+		</Layout>
+	);
+};
+
+export default Widget;

+ 40 - 15
dashboard/src/pages/library/dict/show.tsx

@@ -1,22 +1,47 @@
-import { Space } from "antd";
 import { useParams } from "react-router-dom";
+import { useNavigate } from "react-router-dom";
+import { Layout, Affix, Col, Row } from "antd";
+import { Input } from "antd";
 
-import HeadBar from "../../../components/library/HeadBar";
-import Footer from "../../../components/library/Footer";
+import { useState } from "react";
+import DictSearch from "../../../components/dict/DictSearch";
+const { Content, Header } = Layout;
+const { Search } = Input;
 
 const Widget = () => {
-	// TODO
-	const { word } = useParams();//url 参数
-  return (
-    <div>
-		<HeadBar selectedKeys="dict"/>
-      <div>字典-单词-{word}</div>
-      <div>
-		<Space>主显示区</Space>
-      </div>
-		<Footer />
-    </div>
-  );
+	const navigate = useNavigate();
+	const { word } = useParams(); //url 参数
+	const [wordSearch, setWordSearch] = useState(word);
+
+	const onSearch = (value: string) => {
+		console.log("onSearch", value);
+		setWordSearch(value);
+		navigate("/dict/" + value);
+	};
+	return (
+		<>
+			<Layout>
+				<Affix offsetTop={0}>
+					<Header style={{ backgroundColor: "gray", height: "3.5em" }}>
+						<Row style={{ paddingTop: "0.5em" }}>
+							<Col span="8" offset={8}>
+								<Search placeholder="input search text" onSearch={onSearch} style={{ width: "100%" }} />
+							</Col>
+						</Row>
+					</Header>
+				</Affix>
+				<Content>
+					<Row>
+						<Col flex="auto"></Col>
+						<Col flex="1260px">
+							<DictSearch word={wordSearch} />
+						</Col>
+						<Col flex="auto"></Col>
+					</Row>
+				</Content>
+			</Layout>
+		</>
+	);
 };
 
 export default Widget;

+ 110 - 0
dashboard/src/pages/library/palicanon/bypath.tsx

@@ -0,0 +1,110 @@
+import { useNavigate } from "react-router-dom";
+import { useParams } from "react-router-dom";
+import { useEffect, useState } from "react";
+import { Layout, Affix, Drawer, Row, Col } from "antd";
+import { message } from "antd";
+
+import BookTree from "../../../components/corpus/BookTree";
+import BookTreeList from "../../../components/corpus/BookTreeList";
+import type { IEventBookTreeOnchange } from "../../../components/corpus/BookTreeList";
+import PaliChapterListByTag from "../../../components/corpus/PaliChapterListByTag";
+import BookViewer from "../../../components/corpus/BookViewer";
+import { IChapterClickEvent } from "../../../components/corpus/PaliChapterList";
+
+const Widget = () => {
+	const { root, path, tag } = useParams();
+	const navigate = useNavigate();
+
+	const [bookRoot, setBookRoot] = useState(root);
+	const defaultPath: string[] = path ? path.split("-") : [];
+	const [bookPath, setBookPath] = useState(defaultPath);
+	const [bookTag, setBookTag] = useState([""]);
+	const [isModalOpen, setIsModalOpen] = useState(false);
+	const [openPara, setOpenPara] = useState({ book: 0, para: 0 });
+	const [drawerTitle, setDrawerTitle] = useState("");
+	useEffect(() => {
+		if (typeof root === "undefined") {
+			const currRoot = localStorage.getItem("pali_path_root");
+			if (currRoot === null) {
+				navigate("/palicanon/list/defualt");
+			} else {
+				navigate("/palicanon/list/" + currRoot);
+			}
+		} else {
+			localStorage.setItem("pali_path_root", root);
+			const arrPath = path ? path.split("-") : [];
+			setBookPath(arrPath);
+			setBookRoot(root);
+			console.log("index-load", root);
+		}
+	}, [root]);
+
+	// TODO
+	return (
+		<>
+			<Row>
+				<Col flex="auto"></Col>
+				<Col flex="1260px">
+					<Row>
+						<Col xs={0} xl={6}>
+							<Affix offsetTop={0}>
+								<Layout style={{ height: "100vh", overflowY: "scroll" }}>
+									<BookTree root={bookRoot} path={bookPath} />
+								</Layout>
+							</Affix>
+						</Col>
+						<Col xs={24} xl={14}>
+							<BookTreeList
+								root={bookRoot}
+								path={bookPath}
+								onChange={(e: IEventBookTreeOnchange) => {
+									navigate(`/palicanon/list/${bookRoot}/${e.path.join("-")}`);
+									message.info(e.tag.join());
+									setBookTag(e.tag);
+								}}
+							/>
+							<PaliChapterListByTag
+								tag={bookTag}
+								onChapterClick={(e: IChapterClickEvent) => {
+									if (e.event.ctrlKey) {
+										window.open(
+											`/my/palicanon/chapter/${e.para.Book}-${e.para.Paragraph}`,
+											"_blank"
+										);
+									} else {
+										setIsModalOpen(true);
+										setOpenPara({ book: e.para.Book, para: e.para.Paragraph });
+										setDrawerTitle(e.para.Title);
+									}
+								}}
+							/>
+						</Col>
+						<Col xs={0} xl={4}>
+							侧边栏 侧边栏 侧边栏 侧边栏 侧边栏
+						</Col>
+					</Row>
+				</Col>
+				<Col flex="auto"></Col>
+			</Row>
+
+			<Drawer
+				title={drawerTitle}
+				placement="right"
+				open={isModalOpen}
+				onClose={() => {
+					setIsModalOpen(false);
+				}}
+				size="large"
+				style={{ minWidth: 736, maxWidth: "100%" }}
+				contentWrapperStyle={{ overflowY: "auto" }}
+				footer={null}
+			>
+				<div>
+					<BookViewer para={openPara} />
+				</div>
+			</Drawer>
+		</>
+	);
+};
+
+export default Widget;

+ 33 - 0
dashboard/src/pages/library/palicanon/chapter.tsx

@@ -0,0 +1,33 @@
+import { useParams } from "react-router-dom";
+import { useNavigate } from "react-router-dom";
+
+import { Layout } from "antd";
+
+import BookViewer, { IParagraph } from "../../../components/corpus/BookViewer";
+
+const { Sider, Content } = Layout;
+
+const Widget = () => {
+	// TODO
+	const { id } = useParams(); //url 参数
+	const navigate = useNavigate();
+
+	const arrPara = id ? id.split("-") : ["0", "0"];
+	const para: IParagraph = { book: parseInt(arrPara[0]), para: parseInt(arrPara[1]) };
+	console.log(para);
+	return (
+		<Layout>
+			<Sider width={300} style={{ height: "90%", overflowY: "auto" }} breakpoint="lg"></Sider>
+			<Content>
+				<BookViewer
+					para={para}
+					onChange={(e: IParagraph) => {
+						navigate(`/palicanon/chapter/${e.book}-${e.para}`);
+					}}
+				/>
+			</Content>
+		</Layout>
+	);
+};
+
+export default Widget;

+ 13 - 12
dashboard/src/pages/library/palicanon/index.tsx

@@ -1,19 +1,20 @@
-import { Space } from "antd";
+import { Layout } from "antd";
+
 import HeadBar from "../../../components/library/HeadBar";
-import Footer from "../../../components/library/Footer";
+import FooterBar from "../../../components/library/FooterBar";
+import { Outlet } from "react-router-dom";
 
 const Widget = () => {
 	// TODO
-  return (
-    <div>
-		<HeadBar selectedKeys="palicanon" />
-      <div>圣典分类</div>
-      <div>
-		<Space>主显示区</Space>
-      </div>
-		<Footer />
-    </div>
-  );
+	return (
+		<>
+			<Layout>
+				<HeadBar selectedKeys="palicanon" />
+				<Outlet />
+				<FooterBar />
+			</Layout>
+		</>
+	);
 };
 
 export default Widget;

+ 12 - 12
dashboard/src/pages/library/term/show.tsx

@@ -2,21 +2,21 @@ import { Space } from "antd";
 import { useParams } from "react-router-dom";
 
 import HeadBar from "../../../components/library/HeadBar";
-import Footer from "../../../components/library/Footer";
+import FooterBar from "../../../components/library/FooterBar";
 
 const Widget = () => {
 	// TODO
-	const { word } = useParams();//url 参数
-  return (
-    <div>
-		<HeadBar />
-      <div>术语百科 单词-{word}</div>
-      <div>
-		<Space>主显示区</Space>
-      </div>
-		<Footer />
-    </div>
-  );
+	const { word } = useParams(); //url 参数
+	return (
+		<div>
+			<HeadBar />
+			<div>术语百科 单词-{word}</div>
+			<div>
+				<Space>主显示区</Space>
+			</div>
+			<FooterBar />
+		</div>
+	);
 };
 
 export default Widget;

+ 8 - 9
dashboard/src/pages/nut/index.tsx

@@ -1,15 +1,14 @@
 import HeadBar from "../../components/library/HeadBar";
-import Footer from "../../components/library/Footer";
+import FooterBar from "../../components/library/FooterBar";
 
 const Widget = () => {
-  return (
-	<div>
-		<HeadBar  />
-		<div>Home Page</div>
-		<Footer />
-	</div>
-  	
-  );
+	return (
+		<div>
+			<HeadBar />
+			<div>Home Page</div>
+			<FooterBar />
+		</div>
+	);
 };
 
 export default Widget;

+ 1 - 1
dashboard/src/pages/studio/analysis/index.tsx

@@ -1,4 +1,4 @@
-import { useParams, Link } from "react-router-dom";
+import { useParams } from "react-router-dom";
 import { useIntl } from "react-intl";
 import { Layout } from "antd";
 import { useState, useEffect } from "react";

+ 23 - 4
dashboard/src/pages/studio/anthology/edit.tsx

@@ -26,11 +26,30 @@ interface IFormData {
 const Widget = () => {
 	const intl = useIntl();
 	const { studioname, anthology_id } = useParams(); //url 参数
+	/*
 	const listdata: ListNodeData[] = [
-		{ article: "1", title: "title1", level: 1 },
-		{ article: "2", title: "title2", level: 2 },
-		{ article: "3", title: "title3", level: 1 },
-		{ article: "4", title: "title4", level: 2 },
+		{ key: "1", title: "title1", level: 1 },
+		{ key: "2", title: "title2", level: 2 },
+		{ key: "3", title: "title3", level: 1 },
+		{ key: "4", title: "title4", level: 2 },
+	];
+	*/
+	const listdata: ListNodeData[] = [
+		{
+			key: "d391c9c4-60bc-4bf5-8f5a-65d55743904c",
+			title: "比库尼八敬法--Aṭṭhagarudhammā",
+			level: 1,
+		},
+		{
+			key: "4b741bea-811e-4053-a94c-852a58161b8f",
+			title: "逐出比库尼僧团之《第一极重罪》",
+			level: 1,
+		},
+		{
+			key: "33ff0ec7-2cf9-4e3a-b88f-e713c7a1eaa5",
+			title: "Aṭṭhagarudhammā",
+			level: 1,
+		},
 	];
 	return (
 		<Layout>

+ 17 - 3
dashboard/src/pages/studio/group/index.tsx

@@ -2,11 +2,14 @@ import { useParams, Link } from "react-router-dom";
 import { useIntl } from "react-intl";
 import { useState } from "react";
 import { ProList } from "@ant-design/pro-components";
-import { Space, Tag, Button, Layout, Breadcrumb } from "antd";
+import { Space, Tag, Button, Layout, Breadcrumb, Popover } from "antd";
+import { PlusOutlined } from "@ant-design/icons";
 
 import HeadBar from "../../../components/studio/HeadBar";
 import LeftSider from "../../../components/studio/LeftSider";
 import Footer from "../../../components/studio/Footer";
+import GroupCreate from "../../../components/studio/group/GroupCreate";
+
 const { Content } = Layout;
 
 const defaultData = [
@@ -91,6 +94,13 @@ const Widget = () => {
 								},
 							}}
 							onDataSourceChange={setDataSource}
+							toolBarRender={() => [
+								<Popover content={<GroupCreate studio={studioname} />} placement="bottomRight">
+									<Button key="button" icon={<PlusOutlined />} type="primary">
+										{intl.formatMessage({ id: "buttons.create" })}
+									</Button>
+								</Popover>,
+							]}
 							metas={{
 								title: {
 									dataIndex: "name",
@@ -111,8 +121,12 @@ const Widget = () => {
 								},
 								subTitle: {
 									render: (text, row, index, action) => {
-										const showtag = row.tag.map((item, key) => {
-											return <Tag color={item.color}>{item.title}</Tag>;
+										const showtag = row.tag.map((item, id) => {
+											return (
+												<Tag color={item.color} key={id}>
+													{item.title}
+												</Tag>
+											);
 										});
 										return <Space size={0}>{showtag}</Space>;
 									},

+ 8 - 4
dashboard/src/pages/studio/term/index.tsx

@@ -2,12 +2,13 @@ import { useParams } from "react-router-dom";
 import { ProTable } from "@ant-design/pro-components";
 import { useIntl } from "react-intl";
 import { Link } from "react-router-dom";
-import { Button, Layout, Space, Table } from "antd";
+import { Button, Layout, Space, Table, Popover } from "antd";
 import { PlusOutlined } from "@ant-design/icons";
 
 import HeadBar from "../../../components/studio/HeadBar";
 import LeftSider from "../../../components/studio/LeftSider";
 import Footer from "../../../components/studio/Footer";
+import TermCreate from "../../../components/studio/term/TermCreate";
 
 const { Content } = Layout;
 
@@ -25,6 +26,7 @@ interface IItem {
 const Widget = () => {
 	const intl = useIntl();
 	const { studioname } = useParams();
+
 	return (
 		<Layout>
 			<HeadBar />
@@ -162,9 +164,11 @@ const Widget = () => {
 						}}
 						headerTitle={intl.formatMessage({ id: "dict" })}
 						toolBarRender={() => [
-							<Button key="button" icon={<PlusOutlined />} type="primary">
-								新建
-							</Button>,
+							<Popover content={<TermCreate studio={studioname} />} placement="bottomRight">
+								<Button key="button" icon={<PlusOutlined />} type="primary">
+									{intl.formatMessage({ id: "buttons.create" })}
+								</Button>
+							</Popover>,
 						]}
 						search={{
 							filterType: "light",

+ 52 - 0
dashboard/src/utils.ts

@@ -0,0 +1,52 @@
+import { stringOrDate } from "react-big-calendar";
+
+export function ApiFetch(url: string, method = "GET", data: any = null): Promise<Response> {
+	const apiHost = process.env.REACT_APP_API_HOST;
+	let body: string = "{}";
+	if (data) {
+		body = JSON.stringify(data);
+	}
+	return new Promise((resolve, reject) => {
+		let apiUrl = apiHost + url;
+		console.log("api", apiUrl);
+		fetch(apiUrl, {
+			method: method,
+		})
+			.then((response) => response.json())
+			.then((response) => {
+				resolve(response);
+			})
+			.catch((error) => {
+				reject(error);
+			});
+	});
+}
+
+export function ApiGetText(url: stringOrDate): Promise<String> {
+	const apiHost = "http://127.0.0.1:8000/api/";
+	return new Promise((resolve, reject) => {
+		let apiUrl = apiHost + url;
+		console.log("api", apiUrl);
+		fetch(apiUrl)
+			.then((response) => response.text())
+			.then((response) => {
+				resolve(response);
+			})
+			.catch((error) => {
+				reject(error);
+			});
+	});
+}
+
+export function PaliToEn(pali: string): string {
+	let output: string = pali.toLowerCase();
+	output = output.replaceAll(" ", "_");
+	output = output.replaceAll("-", "_");
+	output = output.replaceAll("ā", "a");
+	output = output.replaceAll("ī", "i");
+	output = output.replaceAll("ū", "u");
+	output = output.replaceAll("ḍ", "d");
+	output = output.replaceAll("ṭ", "t");
+	output = output.replaceAll("ḷ", "l");
+	return output;
+}