Przeglądaj źródła

Merge pull request #1029 from visuddhinanda/agile

改为嵌套路由
visuddhinanda 3 lat temu
rodzic
commit
167afa9477
100 zmienionych plików z 4559 dodań i 483 usunięć
  1. 89 0
      dashboard-fluent/src/assets/general/images/wikipali_logo.svg
  2. 22 0
      dashboard-fluent/src/assets/library/images/wikipali_logo_library.svg
  3. BIN
      dashboard-fluent/src/assets/nut/code.png
  4. 22 0
      dashboard-fluent/src/assets/studio/images/wikipali_banner.svg
  5. 29 0
      dashboard-fluent/src/components/api/Article.ts
  6. 12 0
      dashboard-fluent/src/components/api/Auth.ts
  7. 8 0
      dashboard-fluent/src/components/api/Channel.ts
  8. 75 0
      dashboard-fluent/src/components/api/Corpus.ts
  9. 23 0
      dashboard-fluent/src/components/library/FooterBar.tsx
  10. 145 0
      dashboard-fluent/src/components/library/HeadBar.tsx
  11. 3 0
      dashboard-fluent/src/locales/README.md
  12. 3 0
      dashboard-fluent/src/locales/en-US/buttons.ts
  13. 3 0
      dashboard-fluent/src/locales/en-US/forms.ts
  14. 6 0
      dashboard-fluent/src/locales/zh-Hans/article/index.ts
  15. 7 0
      dashboard-fluent/src/locales/zh-Hans/buttons.ts
  16. 15 0
      dashboard-fluent/src/locales/zh-Hans/channel/index.ts
  17. 33 0
      dashboard-fluent/src/locales/zh-Hans/dict/index.ts
  18. 28 0
      dashboard-fluent/src/locales/zh-Hans/forms.ts
  19. 8 0
      dashboard-fluent/src/locales/zh-Hans/group/index.ts
  20. 7 0
      dashboard-fluent/src/locales/zh-Hans/nut/index.ts
  21. 3 0
      dashboard-fluent/src/locales/zh-Hans/tables.ts
  22. 16 0
      dashboard-fluent/src/locales/zh-Hans/term/index.ts
  23. 12 0
      dashboard-fluent/src/locales/zh-Hans/utilities.ts
  24. 3 0
      dashboard-fluent/src/locales/zh-Hant/buttons.ts
  25. 3 0
      dashboard-fluent/src/locales/zh-Hant/forms.ts
  26. 3 0
      dashboard-fluent/src/locales/zh-Hant/nut/index.ts
  27. 123 78
      dashboard/src/Router.tsx
  28. 89 0
      dashboard/src/assets/general/images/wikipali_logo.svg
  29. 22 0
      dashboard/src/assets/library/images/wikipali_logo_library.svg
  30. 22 0
      dashboard/src/assets/studio/images/wikipali_banner.svg
  31. 29 0
      dashboard/src/components/api/Article.ts
  32. 12 0
      dashboard/src/components/api/Auth.ts
  33. 8 0
      dashboard/src/components/api/Channel.ts
  34. 110 0
      dashboard/src/components/api/Corpus.ts
  35. 65 0
      dashboard/src/components/article/AnthologStudioList.tsx
  36. 61 0
      dashboard/src/components/article/AnthologyCard.tsx
  37. 86 0
      dashboard/src/components/article/AnthologyDetail.tsx
  38. 66 0
      dashboard/src/components/article/AnthologyList.tsx
  39. 80 0
      dashboard/src/components/article/TocTree.tsx
  40. 34 0
      dashboard/src/components/auth/SignInAvatar.tsx
  41. 22 0
      dashboard/src/components/auth/StudioName.tsx
  42. 62 0
      dashboard/src/components/channel/ChannelList.tsx
  43. 25 0
      dashboard/src/components/channel/ChannelListItem.tsx
  44. 85 0
      dashboard/src/components/corpus/BookTree.tsx
  45. 129 0
      dashboard/src/components/corpus/BookTreeList.tsx
  46. 48 0
      dashboard/src/components/corpus/BookViewer.tsx
  47. 92 0
      dashboard/src/components/corpus/ChapterCard.tsx
  48. 29 0
      dashboard/src/components/corpus/ChapterFilter.tsx
  49. 29 0
      dashboard/src/components/corpus/ChapterFilterLang.tsx
  50. 27 0
      dashboard/src/components/corpus/ChapterFilterProgress.tsx
  51. 22 0
      dashboard/src/components/corpus/ChapterFilterType.tsx
  52. 25 0
      dashboard/src/components/corpus/ChapterHead.tsx
  53. 48 0
      dashboard/src/components/corpus/ChapterInChannel.tsx
  54. 96 0
      dashboard/src/components/corpus/ChapterList.tsx
  55. 64 0
      dashboard/src/components/corpus/ChapterTagList.tsx
  56. 67 0
      dashboard/src/components/corpus/PaliChapterCard.tsx
  57. 47 0
      dashboard/src/components/corpus/PaliChapterChannelList.tsx
  58. 70 0
      dashboard/src/components/corpus/PaliChapterHead.tsx
  59. 38 0
      dashboard/src/components/corpus/PaliChapterList.tsx
  60. 49 0
      dashboard/src/components/corpus/PaliChapterListByPara.tsx
  61. 49 0
      dashboard/src/components/corpus/PaliChapterListByTag.tsx
  62. 56 0
      dashboard/src/components/corpus/TocPath.tsx
  63. 31 0
      dashboard/src/components/dict/CaseList.tsx
  64. 46 0
      dashboard/src/components/dict/DictContent.tsx
  65. 29 0
      dashboard/src/components/dict/DictList.tsx
  66. 41 0
      dashboard/src/components/dict/DictSearch.tsx
  67. 51 0
      dashboard/src/components/dict/GrammarPop.tsx
  68. 56 0
      dashboard/src/components/dict/WordCard.tsx
  69. 39 0
      dashboard/src/components/dict/WordCardByDict.tsx
  70. 39 0
      dashboard/src/components/general/UiLangSelect.tsx
  71. 0 26
      dashboard/src/components/library/Footer.tsx
  72. 26 0
      dashboard/src/components/library/FooterBar.tsx
  73. 82 44
      dashboard/src/components/library/HeadBar.tsx
  74. 63 0
      dashboard/src/components/library/blog/BlogNav.tsx
  75. 0 33
      dashboard/src/components/library/blog/HeadBar.tsx
  76. 20 0
      dashboard/src/components/library/blog/Profile.tsx
  77. 47 0
      dashboard/src/components/library/blog/TimeLine.tsx
  78. 73 0
      dashboard/src/components/library/blog/TopArticleCard.tsx
  79. 30 0
      dashboard/src/components/library/blog/TopArticles.tsx
  80. 0 0
      dashboard/src/components/library/palicanon/.txt
  81. 16 12
      dashboard/src/components/nut/Home.tsx
  82. 23 0
      dashboard/src/components/studio/Confidence.tsx
  83. 163 0
      dashboard/src/components/studio/EditableTree.tsx
  84. 28 15
      dashboard/src/components/studio/HeadBar.tsx
  85. 158 105
      dashboard/src/components/studio/LeftSider.tsx
  86. 96 0
      dashboard/src/components/studio/SelectCase.tsx
  87. 43 0
      dashboard/src/components/studio/SelectLang.tsx
  88. 91 0
      dashboard/src/components/studio/SelectUser.txt
  89. 59 0
      dashboard/src/components/studio/anthology/AnthologyCreate.tsx
  90. 59 0
      dashboard/src/components/studio/article/ArticleCreate.tsx
  91. 59 0
      dashboard/src/components/studio/channel/ChannelCreate.tsx
  92. 107 0
      dashboard/src/components/studio/dict/DictCreate.tsx
  93. 41 0
      dashboard/src/components/studio/group/GroupCreate.tsx
  94. 78 84
      dashboard/src/components/studio/group/GroupFile.tsx
  95. 73 74
      dashboard/src/components/studio/group/GroupMember.tsx
  96. 95 0
      dashboard/src/components/studio/term/TermCreate.tsx
  97. 23 0
      dashboard/src/components/tag/TagArea.tsx
  98. 73 0
      dashboard/src/components/utilities/TimeShow.tsx
  99. 31 12
      dashboard/src/load.ts
  100. 6 0
      dashboard/src/locales/zh-Hans/blog/index.ts

+ 89 - 0
dashboard-fluent/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-fluent/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>

BIN
dashboard-fluent/src/assets/nut/code.png


+ 22 - 0
dashboard-fluent/src/assets/studio/images/wikipali_banner.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">studio</tspan></text>
+  </g>
+</svg>

+ 29 - 0
dashboard-fluent/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-fluent/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-fluent/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-fluent/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 };
+}

+ 23 - 0
dashboard-fluent/src/components/library/FooterBar.tsx

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

+ 145 - 0
dashboard-fluent/src/components/library/HeadBar.tsx

@@ -0,0 +1,145 @@
+import { Link } from "@fluentui/react/lib/Link";
+import { useIntl } from "react-intl";
+import { useNavigate } from "react-router-dom";
+import { Layer } from "@fluentui/react/lib/Layer";
+import { DefaultPalette, Stack, IStackStyles, ICommandBarItemProps, CommandBar, IButtonProps } from "@fluentui/react";
+import { IOverflowSetItemProps, OverflowSet } from "@fluentui/react/lib/OverflowSet";
+import { IconButton, IButtonStyles } from "@fluentui/react/lib/Button";
+
+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 = "community" }: IWidgetHeadBar) => {
+	//Library head bar
+	const intl = useIntl(); //i18n
+	const navigate = useNavigate();
+	// TODO
+	const items: ICommandBarItemProps[] = [
+		{
+			text: intl.formatMessage({ id: "columns.library.community.title" }),
+			key: "community",
+			onClick: () => navigate("/community/list"),
+		},
+		{
+			text: intl.formatMessage({ id: "columns.library.palicanon.title" }),
+			key: "palicanon",
+			onClick: () => navigate("/palicanon/list"),
+		},
+		{
+			text: intl.formatMessage({ id: "columns.library.course.title" }),
+			key: "course",
+			onClick: () => navigate("/course"),
+		},
+		{
+			text: intl.formatMessage({ id: "columns.library.dict.title" }),
+			key: "dict",
+			onClick: () => navigate("/dict/recent"),
+		},
+		{
+			text: intl.formatMessage({ id: "columns.library.anthology.title" }),
+			key: "anthology",
+			onClick: () => navigate("/anthology"),
+		},
+		{
+			text: intl.formatMessage({ id: "columns.library.help.title" }),
+			key: "help",
+			onClick: () => navigate("https://asset-hk.wikipali.org/help/zh-Hans"),
+		},
+	];
+
+	const overflowItems: ICommandBarItemProps[] = [
+		{
+			text: intl.formatMessage({ id: "columns.library.palihandbook.title" }),
+			key: "palihandbook",
+			onClick: () => navigate("https://asset-hk.wikipali.org/handbook/zh-Hans"),
+		},
+		{
+			text: intl.formatMessage({ id: "columns.library.calendar.title" }),
+			key: "calendar",
+			onClick: () => navigate("/calendar"),
+		},
+		{
+			text: intl.formatMessage({ id: "columns.library.convertor.title" }),
+			key: "convertor",
+			onClick: () => navigate("/convertor"),
+		},
+		{
+			text: intl.formatMessage({ id: "columns.library.statistics.title" }),
+			key: "statistics",
+			onClick: () => navigate("/statistics"),
+		},
+	];
+	// Styles definition
+	const stackStyles: IStackStyles = {
+		root: {
+			background: DefaultPalette.themeDark,
+		},
+	};
+	const itemStyles: React.CSSProperties = {
+		alignItems: "center",
+		color: DefaultPalette.white,
+		display: "flex",
+		height: 50,
+		justifyContent: "center",
+	};
+
+	const itemSelected: React.CSSProperties = {
+		color: DefaultPalette.white,
+		backgroundColor: "gray",
+		padding: 6,
+	};
+
+	const onRenderItem = (item: IOverflowSetItemProps): JSX.Element => {
+		const styles = selectedKeys === item.key ? itemSelected : { padding: 6 };
+		return (
+			<span style={styles} key={item.key} onClick={item.onClick}>
+				{item.text}
+			</span>
+		);
+	};
+
+	const onRenderOverflowButton = (overflowItems: any[] | undefined): JSX.Element => {
+		const buttonStyles: Partial<IButtonStyles> = {
+			root: {
+				minWidth: 0,
+				padding: "0 4px",
+				alignSelf: "stretch",
+				height: "auto",
+			},
+		};
+		return (
+			<IconButton
+				title="More options"
+				styles={buttonStyles}
+				menuIconProps={{ iconName: "More" }}
+				menuProps={{ items: overflowItems! }}
+			/>
+		);
+	};
+	return (
+		<Layer style={{ background: DefaultPalette.themeDarker }}>
+			<Stack horizontal horizontalAlign="space-between" styles={stackStyles}>
+				<span style={itemStyles}>
+					<Link to="/">
+						<img alt="code" style={{ height: "3em" }} src={img_banner} />
+					</Link>
+				</span>
+				<span style={itemStyles}>
+					<OverflowSet
+						items={items}
+						overflowItems={overflowItems}
+						onRenderOverflowButton={onRenderOverflowButton}
+						onRenderItem={onRenderItem}
+					/>
+				</span>
+				<span style={itemStyles}>译经楼</span>
+			</Stack>
+		</Layer>
+	);
+};
+
+export default Widget;

+ 3 - 0
dashboard-fluent/src/locales/README.md

@@ -0,0 +1,3 @@
+# i18n
+
+nut目录为练习用途。里面的内容可能会被删除。**线上不要使用**。

+ 3 - 0
dashboard-fluent/src/locales/en-US/buttons.ts

@@ -0,0 +1,3 @@
+const items = {};
+
+export default items;

+ 3 - 0
dashboard-fluent/src/locales/en-US/forms.ts

@@ -0,0 +1,3 @@
+const items = {};
+
+export default items;

+ 6 - 0
dashboard-fluent/src/locales/zh-Hans/article/index.ts

@@ -0,0 +1,6 @@
+const items = {
+	"article.lable": "文章",
+	"article.fields.article.count.label": "文章数量",
+};
+
+export default items;

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

@@ -0,0 +1,7 @@
+const items = {
+	"buttons.submit": "提交",
+	"buttons.create": "新建",
+	"buttons.edit": "编辑",
+};
+
+export default items;

+ 15 - 0
dashboard-fluent/src/locales/zh-Hans/channel/index.ts

@@ -0,0 +1,15 @@
+const items = {
+  "channel.title": "版本风格",
+  "channel.type": "类型",
+  "channel.name": "名称",
+  "channel.create.message.noname": "请输入版本名称",
+  "channel.type.nissaya.title": "逐词解析",
+  "channel.type.translation.title": "译文",
+  "channel.lang": "语言",
+  "channel.fields.lang.label": "语言",
+  "channel.fields.type.label": "类型",
+  "channel.fields.name.label": "名称",
+
+};
+
+export default items;

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

@@ -0,0 +1,33 @@
+const items = {
+	dict: "字典",
+	"dict.fields.sn.label": "序号",
+	"dict.fields.word.label": "单词",
+	"dict.fields.type.label": "类型",
+	"dict.fields.grammar.label": "语法信息",
+	"dict.fields.parent.label": "词干",
+	"dict.fields.meaning.label": "意思",
+	"dict.fields.factors.label": "组份",
+	"dict.fields.factormeaning.label": "组份意思",
+	"dict.fields.note.label": "注释",
+	"dict.fields.confidence.label": "信心指数",
+	"dict.fields.dictname.label": "字典名称",
+	"dict.fields.type.n.label": "名词",
+	"dict.fields.type.ti.label": "三性",
+	"dict.fields.type.v.label": "动词",
+	"dict.fields.type.ind.label": "不变",
+	"dict.fields.type.m.label": "阳",
+	"dict.fields.type.nt.label": "中",
+	"dict.fields.type.f.label": "阴",
+	"dict.fields.type.sg.label": "单数",
+	"dict.fields.type.pl.label": "复数",
+	"dict.fields.type.nom.label": "主格",
+	"dict.fields.type.acc.label": "宾格",
+	"dict.fields.type.gen.label": "属格",
+	"dict.fields.type.dat.label": "为格",
+	"dict.fields.type.inst.label": "工具格",
+	"dict.fields.type.voc.label": "呼格",
+	"dict.fields.type.abl.label": "来源格",
+	"dict.fields.type.base.label": "词干",
+};
+
+export default items;

+ 28 - 0
dashboard-fluent/src/locales/zh-Hans/forms.ts

@@ -0,0 +1,28 @@
+const items = {
+	"forms.fields.email.label": "电子邮箱",
+	"forms.fields.password.label": "密码",
+	"forms.fields.id.label": "ID",
+	"forms.fields.message.label": "消息",
+	"forms.fields.created-at.label": "创建时间",
+	"forms.fields.update-at.label": "创建时间",
+	"forms.fields.lang.label": "语言",
+	"forms.create.message.no.lang": "请选择一种语言",
+	"forms.fields.title.label": "标题",
+	"forms.create.message.no.title": "请输入标题",
+	"forms.fields.subtitle.label": "副标题",
+	"forms.fields.summary.label": "简介",
+	"forms.fields.content.label": "内容",
+	"forms.fields.tag.label": "标签",
+	"forms.fields.power.label": "权限",
+	"forms.fields.type.label": "类型",
+	"forms.fields.publicity.label": "公开性",
+	"forms.fields.note.label": "注解",
+	"forms.fields.confidence.label": "信心指数",
+	"forms.fields.confidence.0.label": "不靠谱",
+	"forms.fields.confidence.25.label": "没把握",
+	"forms.fields.confidence.50.label": "一般",
+	"forms.fields.confidence.75.label": "还可以",
+	"forms.fields.confidence.100.label": "有把握",
+};
+
+export default items;

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

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

+ 7 - 0
dashboard-fluent/src/locales/zh-Hans/nut/index.ts

@@ -0,0 +1,7 @@
+const items = {
+  "nut.users.sign-in.title": "欢迎登录",
+  "nut.users.sign-up.title": "新用户注册",
+  "nut.users.logs.title": "日志列表",
+};
+
+export default items;

+ 3 - 0
dashboard-fluent/src/locales/zh-Hans/tables.ts

@@ -0,0 +1,3 @@
+const items = {};
+
+export default items;

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

@@ -0,0 +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.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 - 0
dashboard-fluent/src/locales/zh-Hans/utilities.ts

@@ -0,0 +1,12 @@
+const items = {
+	"utilities.time.secs_ago": "秒之前",
+	"utilities.time.mins_ago": "分钟之前",
+	"utilities.time.hs_ago": "小时之前",
+	"utilities.time.days_ago": "天之前",
+	"utilities.time.weeks_ago": "周之前",
+	"utilities.time.months_ago": "月之前",
+	"utilities.time.year_ago": "年之前",
+	"utilities.time.years_ago": "年之前",
+};
+
+export default items;

+ 3 - 0
dashboard-fluent/src/locales/zh-Hant/buttons.ts

@@ -0,0 +1,3 @@
+const items = {};
+
+export default items;

+ 3 - 0
dashboard-fluent/src/locales/zh-Hant/forms.ts

@@ -0,0 +1,3 @@
+const items = {};
+
+export default items;

+ 3 - 0
dashboard-fluent/src/locales/zh-Hant/nut/index.ts

@@ -0,0 +1,3 @@
+const items = {};
+
+export default items;

+ 123 - 78
dashboard/src/Router.tsx

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

+ 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>

+ 22 - 0
dashboard/src/assets/studio/images/wikipali_banner.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">studio</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;
+};

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

@@ -0,0 +1,110 @@
+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 };
+}
+
+export interface IApiChapterTag {
+	id: string;
+	name: string;
+	count: number;
+}
+export interface IApiResponseChapterTagList {
+	ok: boolean;
+	message: string;
+	data: { rows: IApiChapterTag[]; count: number };
+}
+
+export interface IApiResponseChannelListData {
+	channel_id: string;
+	count: number;
+	channel: {
+		id: number;
+		type: string;
+		owner_uid: string;
+		editor_id: number;
+		name: string;
+		summary: string;
+		lang: string;
+		status: number;
+		setting: string;
+		created_at: string;
+		updated_at: string;
+		uid: string;
+	};
+}
+export interface IApiResponseChannelList {
+	ok: boolean;
+	message: string;
+	data: { rows: IApiResponseChannelListData[]; 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={`/anthology/${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;

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

@@ -0,0 +1,86 @@
+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(prop.aid);
+	}, [prop.aid]);
+
+	function fetchData(id: string) {
+		ApiFetch(`/anthology/${id}`)
+			.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);
+				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, useEffect } from "react";
+import { List } from "antd";
+import ChannelListItem from "./ChannelListItem";
+import type { ChannelInfoProps } from "../api/Channel";
+import { ApiFetch } from "../../utils";
+import { IApiResponseChannelList } from "../api/Corpus";
+
+export interface ChannelFilterProps {
+	chapterProgress: number;
+	lang: string;
+	channelType: string;
+}
+interface IWidgetChannelList {
+	filter?: ChannelFilterProps;
+}
+const defaultChannelFilterProps: ChannelFilterProps = {
+	chapterProgress: 0.9,
+	lang: "zh",
+	channelType: "translation",
+};
+
+const Widget = ({ filter = defaultChannelFilterProps }: IWidgetChannelList) => {
+	const defualt: ChannelInfoProps[] = [];
+	const [tableData, setTableData] = useState(defualt);
+
+	useEffect(() => {
+		console.log("palichapterlist useEffect");
+		let url = `/progress?view=channel&channel_type=${filter.channelType}&lang=${filter.lang}&progress=${filter.chapterProgress}`;
+		ApiFetch(url).then(function (myJson) {
+			console.log("ajex", myJson);
+			const data = myJson as unknown as IApiResponseChannelList;
+			const newData: ChannelInfoProps[] = data.data.rows.map((item) => {
+				return {
+					ChannelName: item.channel.name,
+					ChannelId: item.channel.uid,
+					ChannelType: item.channel.type,
+					StudioName: "V",
+					StudioId: "123",
+					StudioType: "p",
+				};
+			});
+			setTableData(newData);
+		});
+	}, [filter]);
+	return (
+		<>
+			<h3>Channel</h3>
+			<List
+				itemLayout="vertical"
+				size="large"
+				dataSource={tableData}
+				renderItem={(item) => (
+					<List.Item>
+						<ChannelListItem data={item} />
+					</List.Item>
+				)}
+			/>
+		</>
+	);
+};
+
+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;

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

@@ -0,0 +1,85 @@
+//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 { ApiFetch } from "../../utils";
+
+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>
+			<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}
+			/>
+		</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 { 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;

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

@@ -0,0 +1,92 @@
+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;
+	const aa = {
+		marginTop: "auto",
+		marginBottom: "auto",
+		display: "-webkit-box",
+		//WebkitBoxOrient: "vertical",
+		//WebkitLineClamp: 3,
+		overflow: "hidden",
+	};
+
+	return (
+		<>
+			<Row>
+				<Col span={3}>封面</Col>
+				<Col span={21}>
+					<Row>
+						<Col span={16}>
+							<Row>
+								<Col>
+									<Title level={5}>
+										<Link>{prop.data.Title}</Link>
+									</Title>
+								</Col>
+							</Row>
+							<Row>
+								<Col>{prop.data.PaliTitle}</Col>
+							</Row>
+							<Row>
+								<Col>
+									<TocPath data={path} />
+								</Col>
+							</Row>
+						</Col>
+						<Col span={8}>进度条</Col>
+					</Row>
+					<Row>
+						<Col>
+							<Paragraph>
+								<div style={aa}>{prop.data.Summary}</div>
+							</Paragraph>
+						</Col>
+					</Row>
+					<Row>
+						<Col span={16}>
+							<TagArea data={tags} />
+						</Col>
+						<Col span={5}>
+							<ChannelListItem data={prop.data.Channel} />
+						</Col>
+						<Col span={3}>
+							<TimeShow time={prop.data.UpdatedAt} title="UpdatedAt" />
+						</Col>
+					</Row>
+				</Col>
+			</Row>
+		</>
+	);
+};
+
+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;

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

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

+ 64 - 0
dashboard/src/components/corpus/ChapterTagList.tsx

@@ -0,0 +1,64 @@
+import { message, Tag, Button } from "antd";
+import { useState, useEffect } from "react";
+import { ApiFetch } from "../../utils";
+import { IApiChapterTag, IApiResponseChapterTagList } from "../api/Corpus";
+
+interface ITagData {
+	title: string;
+	key: string;
+}
+interface IWidgetChapterTagList {
+	max?: number;
+	onTagClick: Function;
+}
+const Widget = (prop: IWidgetChapterTagList) => {
+	const defaultData: ITagData[] = [];
+	const [tableData, setTableData] = useState(defaultData);
+
+	useEffect(() => {
+		console.log("useEffect");
+		fetchData();
+	}, []);
+
+	function fetchData() {
+		ApiFetch(`/progress?view=chapter-tag`)
+			.then((response) => {
+				const json = response as unknown as IApiResponseChapterTagList;
+				const tags: IApiChapterTag[] = json.data.rows;
+				let newTags: ITagData[] = tags.map((item) => {
+					return {
+						key: item.name,
+						title: `${item.name}(${item.count})`,
+					};
+				});
+				setTableData(newTags);
+			})
+			.catch((error) => {
+				message.error(error);
+			});
+	}
+	let iTag = prop.max ? prop.max : tableData.length;
+	if (iTag > tableData.length) {
+		iTag = tableData.length;
+	}
+	return (
+		<>
+			{tableData.map((item, id) => {
+				return (
+					<Tag
+						key={id}
+						onClick={() => {
+							if (typeof prop.onTagClick !== "undefined") {
+								prop.onTagClick(item.key);
+							}
+						}}
+					>
+						<Button type="link">{item.title}</Button>
+					</Tag>
+				);
+			})}
+		</>
+	);
+};
+
+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 { ApiGetText } from "../../utils";
+import MDEditor from "@uiw/react-md-editor";
+
+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 }}>
+				<MDEditor.Markdown source={guide} />
+			</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/list">{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/list">{intl.formatMessage({ id: "columns.library.anthology.title" })}</Link>,
+			key: "anthology",
+		},
 		{
-			label: (<a href = "https://asset-hk.wikipali.org/help/zh-Hans" target="_blank" rel="noreferrer">{intl.formatMessage({ id: "columns.library.help.title" })}</a>),
-			key: 'help',
+			label: (
+				<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;

+ 63 - 0
dashboard/src/components/library/blog/BlogNav.tsx

@@ -0,0 +1,63 @@
+import { Link } from "react-router-dom";
+import { useIntl } from "react-intl";
+import { MailOutlined } from "@ant-design/icons";
+import type { MenuProps } from "antd";
+import { Menu, Row, Col } from "antd";
+
+interface IWidgetBlogNav {
+	selectedKey: string;
+	studio: string;
+}
+const Widget = (prop: IWidgetBlogNav) => {
+	//Library head bar
+	const intl = useIntl(); //i18n
+	// TODO
+
+	const items: MenuProps["items"] = [
+		{
+			label: <Link to={`/blog/${prop.studio}/overview`}>{intl.formatMessage({ id: "blog.overview" })}</Link>,
+			key: "overview",
+			icon: <MailOutlined />,
+		},
+		{
+			label: <Link to={`/blog/${prop.studio}/palicanon`}>{intl.formatMessage({ id: "blog.palicanon" })}</Link>,
+			key: "palicanon",
+			icon: <MailOutlined />,
+		},
+		{
+			label: (
+				<Link to={`/blog/${prop.studio}/course`}>
+					{intl.formatMessage({ id: "columns.library.course.title" })}
+				</Link>
+			),
+			key: "course",
+			icon: <MailOutlined />,
+		},
+		{
+			label: (
+				<Link to={`/blog/${prop.studio}/anthology`}>
+					{intl.formatMessage({ id: "columns.library.anthology.title" })}
+				</Link>
+			),
+			key: "anthology",
+			icon: <MailOutlined />,
+		},
+		{
+			label: (
+				<Link to={`/blog/${prop.studio}/term`}>{intl.formatMessage({ id: "columns.library.term.title" })}</Link>
+			),
+			key: "term",
+			icon: <MailOutlined />,
+		},
+	];
+	return (
+		<Row>
+			<Col flex="300px"></Col>
+
+			<Col flex="auto">
+				<Menu selectedKeys={[prop.selectedKey]} mode="horizontal" items={items} />
+			</Col>
+		</Row>
+	);
+};
+export default Widget;

+ 0 - 33
dashboard/src/components/library/blog/HeadBar.tsx

@@ -1,33 +0,0 @@
-import { Link } from "react-router-dom";
-import { Space } from "antd";
-import { useIntl } from "react-intl";
-
-const Widget = () => {
-	//Library head bar
-	const intl = useIntl();//i18n
-	// TODO
-  return (
-	<Space>
-		<Link to="/community">
-			{intl.formatMessage({ id: "columns.library.community.title" })}
-		</Link>
-		<Link to="/palicanon">
-			{intl.formatMessage({ id: "columns.library.palicanon.title" })}
-		</Link>
-		<Link to="/course">
-			{intl.formatMessage({ id: "columns.library.course.title" })}
-		</Link>
-		<Link to="/term">
-			{intl.formatMessage({ id: "columns.library.term.title" })}
-		</Link>
-		<Link to="/dict">
-			{intl.formatMessage({ id: "columns.library.dict.title" })}
-		</Link>
-		<Link to="/anthology">
-			{intl.formatMessage({ id: "columns.library.anthology.title" })}
-		</Link>
-	</Space>
-  );
-};
-
-export default Widget;

+ 20 - 0
dashboard/src/components/library/blog/Profile.tsx

@@ -0,0 +1,20 @@
+import { Card } from "antd";
+
+const Widget = () => {
+	return (
+		<>
+			<Card title="简介" bordered={false} style={{ width: "100%" }}>
+				<p>Card content</p>
+				<p>Card content</p>
+				<p>Card content</p>
+			</Card>
+			<Card title="团队" bordered={false} style={{ width: "100%" }}>
+				<p>Card content</p>
+				<p>Card content</p>
+				<p>Card content</p>
+			</Card>
+		</>
+	);
+};
+
+export default Widget;

+ 47 - 0
dashboard/src/components/library/blog/TimeLine.tsx

@@ -0,0 +1,47 @@
+import { Timeline } from "antd";
+
+interface IAuthorTimeLine {
+	lable: string;
+	content: string;
+	type: string;
+}
+const Widget = () => {
+	const data: IAuthorTimeLine[] = [
+		{
+			lable: "2015-09-1",
+			content: "Technical testing",
+			type: "translation",
+		},
+		{
+			lable: "2015-09-1",
+			content: "Technical testing",
+			type: "translation",
+		},
+		{
+			lable: "2015-09-1",
+			content: "Technical testing",
+			type: "translation",
+		},
+		{
+			lable: "2015-09-1",
+			content: "Technical testing",
+			type: "translation",
+		},
+	];
+
+	return (
+		<>
+			<Timeline mode={"left"} style={{ width: "100%" }}>
+				{data.map((item, id) => {
+					return (
+						<Timeline.Item key={id} label={item.lable}>
+							{item.content}
+						</Timeline.Item>
+					);
+				})}
+			</Timeline>
+		</>
+	);
+};
+
+export default Widget;

+ 73 - 0
dashboard/src/components/library/blog/TopArticleCard.tsx

@@ -0,0 +1,73 @@
+import { Card } from "antd";
+import { AppstoreOutlined, LikeOutlined, FieldTimeOutlined } from "@ant-design/icons";
+
+import { Space } from "antd";
+import { Link } from "react-router-dom";
+
+interface IIconParamListData {
+	label: React.ReactNode;
+	key: string;
+	icon: React.ReactNode;
+}
+interface IWidgetIconParamList {
+	data: IIconParamListData[];
+}
+const IconParamList = (prop: IWidgetIconParamList) => {
+	return (
+		<><Space>
+			{prop.data.map((item, id) => {
+				return (
+					<Space>
+						{item.icon} {item.label}
+					</Space>
+				);
+			})}
+			</Space>
+		</>
+	);
+};
+
+export interface ITopArticleCardData {
+	title: string;
+	link: string;
+	like: number;
+	hit: number;
+	updatedAt: string;
+}
+interface IWidgetTopArticleCard {
+	data: ITopArticleCardData;
+}
+const Widget = (prop: IWidgetTopArticleCard) => {
+	const items: IIconParamListData[] = [
+		{
+			label: "经藏",
+			key: "sutta",
+			icon: <AppstoreOutlined />,
+		},
+		{
+			label: prop.data.like,
+			key: "like",
+			icon: <LikeOutlined />,
+		},
+		{
+			label: prop.data.updatedAt,
+			key: "updated",
+			icon: <FieldTimeOutlined />,
+		},
+	];
+
+	return (
+		<>
+			<Card>
+				<h4>
+					<Link to={prop.data.link}>{prop.data.title}</Link>
+				</h4>
+				<div>
+					<IconParamList data={items} />
+				</div>
+			</Card>
+		</>
+	);
+};
+
+export default Widget;

+ 30 - 0
dashboard/src/components/library/blog/TopArticles.tsx

@@ -0,0 +1,30 @@
+import { Row, Col } from "antd";
+import TopArticleCard, { ITopArticleCardData } from "./TopArticleCard";
+interface IWidgetTopArticles {
+	studio: string;
+}
+const Widget = (prop: IWidgetTopArticles) => {
+	const data: ITopArticleCardData[] = [
+		{
+			title: "法句心品",
+			link: "/my/article/12345",
+			like: 50,
+			hit: 100,
+			updatedAt: "2022-12-3",
+		},
+		{ title: "法句心品", link: "/my/article/12345", like: 50, hit: 100, updatedAt: "2022-12-3" },
+		{ title: "疑惑度脱-应学法", link: "/my/article/12345", like: 50, hit: 100, updatedAt: "2022-12-3" },
+		{ title: "法句心品", link: "/my/article/12345", like: 50, hit: 100, updatedAt: "2022-12-3" },
+	];
+
+	const list = data.map((item, id) => {
+		return (
+			<Col flex="400px">
+				<TopArticleCard data={item} key={id} />
+			</Col>
+		);
+	});
+	return <Row>{list}</Row>;
+};
+
+export default Widget;

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


+ 16 - 12
dashboard/src/components/nut/Home.tsx

@@ -1,20 +1,24 @@
+import ReactMarkdown from "react-markdown";
 import code_png from "../../assets/nut/code.png";
 import MarkdownForm from "./MarkdownForm";
 import MarkdownShow from "./MarkdownShow";
 
 const Widget = () => {
-  return (
-    <div>
-      <h1>Home</h1>
-      <br />
-      <MarkdownShow body="- Hello, **《mint》**!" />
-      <br />
-      <h3>Form</h3>
-      <MarkdownForm />
-      <br />
-      <img alt="code" src={code_png} />
-    </div>
-  );
+	return (
+		<div>
+			<h1>Home</h1>
+			<br />
+			<MarkdownShow body="- Hello, **《mint》**!" />
+			<br />
+			<h3>Form</h3>
+			<MarkdownForm />
+			<br />
+			<img alt="code" src={code_png} />
+			<div>
+				<ReactMarkdown>*This* is text with `quote`</ReactMarkdown>
+			</div>
+		</div>
+	);
 };
 
 export default Widget;

+ 23 - 0
dashboard/src/components/studio/Confidence.tsx

@@ -0,0 +1,23 @@
+import { Slider } from "antd";
+import type { SliderMarks } from "antd/es/slider";
+import { useIntl } from "react-intl";
+
+const onChange = (value: string) => {
+	console.log(`selected ${value}`);
+};
+type IWidgetConfidence = {
+	defaultValue?: number;
+};
+const Widget = ({ defaultValue = 75 }: IWidgetConfidence) => {
+	const intl = useIntl();
+	const marks: SliderMarks = {
+		0: intl.formatMessage({ id: "forms.fields.confidence.0.label" }),
+		25: intl.formatMessage({ id: "forms.fields.confidence.25.label" }),
+		50: intl.formatMessage({ id: "forms.fields.confidence.50.label" }),
+		75: intl.formatMessage({ id: "forms.fields.confidence.75.label" }),
+		100: intl.formatMessage({ id: "forms.fields.confidence.100.label" }),
+	};
+	return <Slider marks={marks} defaultValue={defaultValue} />;
+};
+
+export default Widget;

+ 163 - 0
dashboard/src/components/studio/EditableTree.tsx

@@ -0,0 +1,163 @@
+import React, { useState } from "react";
+import { Tree } from "antd";
+import type { DataNode, TreeProps } from "antd/es/tree";
+
+type TreeNodeData = {
+	key: string;
+	title: string;
+	children: TreeNodeData[];
+	level: number;
+};
+export type ListNodeData = {
+	key: string;
+	title: string;
+	level: number;
+};
+
+var tocActivePath: TreeNodeData[] = [];
+function tocGetTreeData(articles: ListNodeData[], active = "") {
+	let treeData = [];
+
+	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 < articles.length; index++) {
+		const element = articles[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 IWidgetEditableTree = {
+	treeData: ListNodeData[];
+};
+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 onDragEnter: TreeProps["onDragEnter"] = (info) => {
+		console.log(info);
+		// expandedKeys 需要受控时设置
+		// setExpandedKeys(info.expandedKeys)
+	};
+
+	const onDrop: TreeProps["onDrop"] = (info) => {
+		console.log(info);
+		const dropKey = info.node.key;
+		const dragKey = info.dragNode.key;
+		const dropPos = info.node.pos.split("-");
+		const dropPosition = info.dropPosition - Number(dropPos[dropPos.length - 1]);
+
+		const loop = (
+			data: DataNode[],
+			key: React.Key,
+			callback: (node: DataNode, i: number, data: DataNode[]) => void
+		) => {
+			for (let i = 0; i < data.length; i++) {
+				if (data[i].key === key) {
+					return callback(data[i], i, data);
+				}
+				if (data[i].children) {
+					loop(data[i].children!, key, callback);
+				}
+			}
+		};
+		const data = [...gData];
+
+		// Find dragObject
+		let dragObj: DataNode;
+		loop(data, dragKey, (item, index, arr) => {
+			arr.splice(index, 1);
+			dragObj = item;
+		});
+
+		if (!info.dropToGap) {
+			// Drop on the content
+			loop(data, dropKey, (item) => {
+				item.children = item.children || [];
+				// where to insert 示例添加到头部,可以是随意位置
+				item.children.unshift(dragObj);
+			});
+		} else if (
+			((info.node as any).props.children || []).length > 0 && // Has children
+			(info.node as any).props.expanded && // Is expanded
+			dropPosition === 1 // On the bottom gap
+		) {
+			loop(data, dropKey, (item) => {
+				item.children = item.children || [];
+				// where to insert 示例添加到头部,可以是随意位置
+				item.children.unshift(dragObj);
+				// in previous version, we use item.children.push(dragObj) to insert the
+				// item to the tail of the children
+			});
+		} else {
+			let ar: DataNode[] = [];
+			let i: number;
+			loop(data, dropKey, (_item, index, arr) => {
+				ar = arr;
+				i = index;
+			});
+			if (dropPosition === -1) {
+				ar.splice(i!, 0, dragObj!);
+			} else {
+				ar.splice(i! + 1, 0, dragObj!);
+			}
+		}
+		setGData(data);
+	};
+
+	return (
+		<>
+			<Tree
+				rootClassName="draggable-tree"
+				//defaultExpandedKeys={expandedKeys}
+				draggable
+				blockNode
+				onDragEnter={onDragEnter}
+				onDrop={onDrop}
+				treeData={gData}
+			/>
+		</>
+	);
+};
+
+export default Widget;

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

@@ -1,24 +1,37 @@
 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;
+
 const onSearch = (value: string) => console.log(value);
 
 const Widget = () => {
-  return (
-	<Header className="header">
-		<Row>
-			<Col flex="100px">
-				<Link to="/">studio</Link>
-			</Col>
-			<Col flex="auto">
-				<Search placeholder="input search text" onSearch={onSearch} style={{ width: 200 }} />
-			</Col>
-			<Col flex="200px">登录信息</Col>
-		</Row>		
-	</Header>
-
-  );
+	return (
+		<Header className="header">
+			<Row justify="space-between">
+				<Col flex="80px">
+					<Link to="/">
+						<img alt="code" style={{ height: "3em" }} src={img_banner} />
+					</Link>
+				</Col>
+				<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>
+			</Row>
+		</Header>
+	);
 };
 
 export default Widget;

+ 158 - 105
dashboard/src/components/studio/LeftSider.tsx

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

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

@@ -0,0 +1,96 @@
+import { Cascader } from "antd";
+import { useIntl } from "react-intl";
+
+interface CascaderOption {
+	value: string | number;
+	label: string;
+	children?: CascaderOption[];
+}
+
+const Widget = () => {
+	const intl = useIntl();
+	const case8 = [
+		{
+			value: "nom",
+			label: intl.formatMessage({ id: "dict.fields.type.nom.label" }),
+		},
+		{
+			value: "acc",
+			label: intl.formatMessage({ id: "dict.fields.type.acc.label" }),
+		},
+		{
+			value: "gen",
+			label: intl.formatMessage({ id: "dict.fields.type.gen.label" }),
+		},
+		{
+			value: "dat",
+			label: intl.formatMessage({ id: "dict.fields.type.dat.label" }),
+		},
+		{
+			value: "inst",
+			label: intl.formatMessage({ id: "dict.fields.type.inst.label" }),
+		},
+		{
+			value: "abl",
+			label: intl.formatMessage({ id: "dict.fields.type.abl.label" }),
+		},
+		{
+			value: "voc",
+			label: intl.formatMessage({ id: "dict.fields.type.voc.label" }),
+		},
+	];
+	const case2 = [
+		{
+			value: "sg",
+			label: intl.formatMessage({ id: "dict.fields.type.sg.label" }),
+			children: case8,
+		},
+		{
+			value: "pl",
+			label: intl.formatMessage({ id: "dict.fields.type.pl.label" }),
+			children: case8,
+		},
+		{
+			value: "base",
+			label: intl.formatMessage({ id: "dict.fields.type.base.label" }),
+		},
+	];
+	const case3 = [
+		{
+			value: "m",
+			label: intl.formatMessage({ id: "dict.fields.type.m.label" }),
+			children: case2,
+		},
+		{
+			value: "nt",
+			label: intl.formatMessage({ id: "dict.fields.type.nt.label" }),
+			children: case2,
+		},
+		{
+			value: "f",
+			label: intl.formatMessage({ id: "dict.fields.type.f.label" }),
+			children: case2,
+		},
+	];
+	const options: CascaderOption[] = [
+		{
+			value: ".n.",
+			label: intl.formatMessage({ id: "dict.fields.type.n.label" }),
+			children: case3,
+		},
+		{
+			value: ".ti.",
+			label: intl.formatMessage({ id: "dict.fields.type.ti.label" }),
+			children: case3,
+		},
+		{
+			value: ".v.",
+			label: intl.formatMessage({ id: "dict.fields.type.v.label" }),
+			children: case3,
+		},
+	];
+
+	return <Cascader options={options} placeholder="Please select case" />;
+};
+
+export default Widget;

+ 43 - 0
dashboard/src/components/studio/SelectLang.tsx

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

+ 91 - 0
dashboard/src/components/studio/SelectUser.txt

@@ -0,0 +1,91 @@
+/*
+import { Select, Spin } from "antd";
+import type { SelectProps } from "antd/es/select";
+import debounce from "lodash/debounce";
+import React, { useMemo, useRef, useState } from "react";
+
+export interface DebounceSelectProps<ValueType = any>
+	extends Omit<SelectProps<ValueType | ValueType[]>, "options" | "children"> {
+	fetchOptions: (search: string) => Promise<ValueType[]>;
+	debounceTimeout?: number;
+}
+
+function DebounceSelect<ValueType extends { key?: string; label: React.ReactNode; value: string | number } = any>({
+	fetchOptions,
+	debounceTimeout = 800,
+	...props
+}: DebounceSelectProps<ValueType>) {
+	const [fetching, setFetching] = useState(false);
+	const [options, setOptions] = useState<ValueType[]>([]);
+	const fetchRef = useRef(0);
+
+	const debounceFetcher = useMemo(() => {
+		const loadOptions = (value: string) => {
+			fetchRef.current += 1;
+			const fetchId = fetchRef.current;
+			setOptions([]);
+			setFetching(true);
+
+			fetchOptions(value).then((newOptions) => {
+				if (fetchId !== fetchRef.current) {
+					// for fetch callback order
+					return;
+				}
+
+				setOptions(newOptions);
+				setFetching(false);
+			});
+		};
+
+		return debounce(loadOptions, debounceTimeout);
+	}, [fetchOptions, debounceTimeout]);
+
+	return (
+		<Select
+			labelInValue
+			filterOption={false}
+			onSearch={debounceFetcher}
+			notFoundContent={fetching ? <Spin size="small" /> : null}
+			{...props}
+			options={options}
+		/>
+	);
+}
+
+// Usage of DebounceSelect
+interface UserValue {
+	label: string;
+	value: string;
+}
+
+async function fetchUserList(username: string): Promise<UserValue[]> {
+	console.log("fetching user", username);
+
+	return fetch("https://randomuser.me/api/?results=5")
+		.then((response) => response.json())
+		.then((body) =>
+			body.results.map((user: { name: { first: string; last: string }; login: { username: string } }) => ({
+				label: `${user.name.first} ${user.name.last}`,
+				value: user.login.username,
+			}))
+		);
+}
+
+const Widget = () => {
+	const [value, setValue] = useState<UserValue[]>([]);
+
+	return (
+		<DebounceSelect
+			mode="multiple"
+			value={value}
+			placeholder="Select users"
+			fetchOptions={fetchUserList}
+			onChange={(newValue) => {
+				setValue(newValue as UserValue[]);
+			}}
+			style={{ width: "100%" }}
+		/>
+	);
+};
+
+export default Widget;

+ 59 - 0
dashboard/src/components/studio/anthology/AnthologyCreate.tsx

@@ -0,0 +1,59 @@
+import { ProForm, ProFormText, ProFormSelect } from "@ant-design/pro-components";
+import { useIntl } from "react-intl";
+import { message } from "antd";
+
+interface IFormData {
+	title: string;
+	lang: string;
+}
+
+type IWidgetAnthologyCreate = {
+	studio: string | undefined;
+};
+const Widget = (param: IWidgetAnthologyCreate) => {
+	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="title"
+					required
+					label={intl.formatMessage({ id: "channel.name" })}
+					rules={[
+						{
+							required: true,
+							message: intl.formatMessage({ id: "channel.create.message.noname" }),
+						},
+					]}
+				/>
+			</ProForm.Group>
+			<ProForm.Group>
+				<ProFormSelect
+					options={[
+						{
+							value: "zh-Hans",
+							label: intl.formatMessage({ id: "languages.zh-Hans" }),
+						},
+						{
+							value: "en-US",
+							label: intl.formatMessage({ id: "English" }),
+						},
+					]}
+					width="md"
+					name="lang"
+					label={intl.formatMessage({ id: "forms.fields.lang.label" })}
+				/>
+			</ProForm.Group>
+		</ProForm>
+	);
+};
+
+export default Widget;

+ 59 - 0
dashboard/src/components/studio/article/ArticleCreate.tsx

@@ -0,0 +1,59 @@
+import { ProForm, ProFormText, ProFormSelect } from "@ant-design/pro-components";
+import { useIntl } from "react-intl";
+import { message } from "antd";
+
+interface IFormData {
+	title: string;
+	lang: string;
+}
+
+type IWidgetArticleCreate = {
+	studio: string | undefined;
+};
+const Widget = (param: IWidgetArticleCreate) => {
+	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="title"
+					required
+					label={intl.formatMessage({ id: "channel.name" })}
+					rules={[
+						{
+							required: true,
+							message: intl.formatMessage({ id: "channel.create.message.noname" }),
+						},
+					]}
+				/>
+			</ProForm.Group>
+			<ProForm.Group>
+				<ProFormSelect
+					options={[
+						{
+							value: "zh-Hans",
+							label: intl.formatMessage({ id: "languages.zh-Hans" }),
+						},
+						{
+							value: "en-US",
+							label: intl.formatMessage({ id: "English" }),
+						},
+					]}
+					width="md"
+					name="lang"
+					label={intl.formatMessage({ id: "forms.fields.lang.label" })}
+				/>
+			</ProForm.Group>
+		</ProForm>
+	);
+};
+
+export default Widget;

+ 59 - 0
dashboard/src/components/studio/channel/ChannelCreate.tsx

@@ -0,0 +1,59 @@
+import { ProForm, ProFormText, ProFormSelect } from "@ant-design/pro-components";
+import { useIntl } from "react-intl";
+import { message } from "antd";
+
+interface IFormData {
+	name: string;
+	type: string;
+}
+
+type IWidgetChannelCreate = {
+	studio: string | undefined;
+};
+const Widget = (param: IWidgetChannelCreate) => {
+	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.Group>
+				<ProFormSelect
+					options={[
+						{
+							value: "translation",
+							label: intl.formatMessage({ id: "channel.type.translation.title" }),
+						},
+						{
+							value: "nissaya",
+							label: intl.formatMessage({ id: "channel.type.nissaya.title" }),
+						},
+					]}
+					width="md"
+					name="type"
+					label={intl.formatMessage({ id: "channel.type" })}
+				/>
+			</ProForm.Group>
+		</ProForm>
+	);
+};
+
+export default Widget;

+ 107 - 0
dashboard/src/components/studio/dict/DictCreate.tsx

@@ -0,0 +1,107 @@
+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";
+import SelectCase from "../SelectCase";
+import Confidene from "../Confidence";
+
+interface IFormData {
+	word: string;
+	type: string;
+	grammar: string;
+	parent: string;
+	meaning: string;
+	note: string;
+	factors: string;
+	factormeaning: string;
+	lang: string;
+}
+
+type IWidgetDictCreate = {
+	studio: string | undefined;
+};
+const Widget = (param: IWidgetDictCreate) => {
+	const intl = useIntl();
+	/*
+	const onLangChange = (value: string) => {
+		console.log(`selected ${value}`);
+	};
+
+	const onLangSearch = (value: string) => {
+		console.log("search:", 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>
+					<div>语法信息</div>
+					<SelectCase />
+				</ProForm.Group>
+				<ProForm.Group>
+					<ProFormText
+						width="md"
+						name="parent"
+						label={intl.formatMessage({ id: "dict.fields.parent.label" })}
+					/>
+				</ProForm.Group>
+				<ProForm.Group>
+					<div>语言</div>
+					<SelectLang />
+				</ProForm.Group>
+				<ProForm.Group>
+					<ProFormText
+						width="md"
+						name="meaning"
+						label={intl.formatMessage({ id: "dict.fields.meaning.label" })}
+					/>
+				</ProForm.Group>
+				<ProForm.Group>
+					<ProFormText
+						width="md"
+						name="factors"
+						label={intl.formatMessage({ id: "dict.fields.factors.label" })}
+					/>
+				</ProForm.Group>
+				<ProForm.Group>
+					<ProFormText
+						width="md"
+						name="factormeaning"
+						label={intl.formatMessage({ id: "dict.fields.factormeaning.label" })}
+					/>
+				</ProForm.Group>
+				<ProForm.Group>
+					<ProFormTextArea name="note" label={intl.formatMessage({ id: "forms.fields.note.label" })} />
+				</ProForm.Group>
+				<Layout>
+					<div>信心指数</div>
+					<Confidene />
+				</Layout>
+			</ProForm>
+		</Layout>
+	);
+};
+
+export default Widget;

+ 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;

+ 31 - 12
dashboard/src/load.ts

@@ -1,19 +1,38 @@
-import { Empty } from "google-protobuf/google/protobuf/empty_pb";
-import { Duration } from "google-protobuf/google/protobuf/duration_pb";
+//import { Empty } from "google-protobuf/google/protobuf/empty_pb";
+//import { Duration } from "google-protobuf/google/protobuf/duration_pb";
 
-import { DURATION, get as getToken, signIn } from "./reducers/current-user";
-import { refresh as refreshLayout } from "./reducers/layout";
-import { GRPC_HOST, get as httpGet, grpc_metadata } from "./request";
+import { get as getToken, IUser, signIn } from "./reducers/current-user";
+//import { DURATION } from "./reducers/current-user";
+import { ISite, refresh as refreshLayout } from "./reducers/layout";
+import { get } from "./request";
+//import { GRPC_HOST,  grpc_metadata } from "./request";
 import store from "./store";
 
+interface IUserResponse {
+	ok: boolean;
+	message: string;
+	data: IUser;
+}
 const init = () => {
-  // TODO ajax get site information, SEE reducers/layout/ISite
-  // store.dispatch(refreshLayout(it));
-
-  if (getToken()) {
-    // TODO get current user profile & new token, SEE reducers/current-user/IUser
-    // store.dispatch(signIn([user, token]));
-  }
+	console.log("onload");
+	// ajax get site information, SEE reducers/layout/ISite
+	get("/v2/siteinfo/en").then((json) => {
+		store.dispatch(refreshLayout(json as ISite));
+	});
+	const token = getToken();
+	if (token) {
+		// get current user profile & new token, SEE reducers/current-user/IUser
+		get("/v2/auth/current").then((json) => {
+			const user = json as IUserResponse;
+			if (user.ok) {
+				store.dispatch(signIn([user.data, token ? token : ""]));
+			} else {
+				console.error(user.message);
+			}
+		});
+	} else {
+		console.log("no token");
+	}
 };
 
 export default init;

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

@@ -0,0 +1,6 @@
+const items = {
+	"blog.overview": "概览",
+	"blog.palicanon": "译文",
+};
+
+export default items;

Niektóre pliki nie zostały wyświetlone z powodu dużej ilości zmienionych plików