@weibaohui/dsh-git-server 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (248) hide show
  1. package/README.md +43 -0
  2. package/client/bundle.js +2356 -0
  3. package/client/index.js +2342 -0
  4. package/cordis.patch.yml +11 -0
  5. package/package.json +70 -0
  6. package/server/dist/api/v1.js +2029 -0
  7. package/server/dist/authx/password.js +43 -0
  8. package/server/dist/authx/um.js +157 -0
  9. package/server/dist/conf.js +323 -0
  10. package/server/dist/context.js +701 -0
  11. package/server/dist/db/actions.js +202 -0
  12. package/server/dist/db/db.js +838 -0
  13. package/server/dist/dshapi.js +1416 -0
  14. package/server/dist/editorconfig.js +104 -0
  15. package/server/dist/gitx/git.js +580 -0
  16. package/server/dist/gitx/http.js +232 -0
  17. package/server/dist/gitx/paths.js +28 -0
  18. package/server/dist/gitx/service.js +251 -0
  19. package/server/dist/gosprintf.js +167 -0
  20. package/server/dist/hook.js +53 -0
  21. package/server/dist/index.js +112 -0
  22. package/server/dist/lfsx.js +265 -0
  23. package/server/dist/mailer.js +127 -0
  24. package/server/dist/markup.js +317 -0
  25. package/server/dist/metrics.js +18 -0
  26. package/server/dist/mirror.js +203 -0
  27. package/server/dist/multipart.js +33 -0
  28. package/server/dist/repox.js +99 -0
  29. package/server/dist/router.js +185 -0
  30. package/server/dist/serv.js +43 -0
  31. package/server/dist/server.js +382 -0
  32. package/server/dist/sshx/dispatch.js +160 -0
  33. package/server/dist/sshx/lfs-transfer.js +289 -0
  34. package/server/dist/sshx/server.js +107 -0
  35. package/server/dist/toolx.js +108 -0
  36. package/server/dist/twofactor.js +160 -0
  37. package/server/dist/webapi.js +377 -0
  38. package/server/dist/webhook.js +128 -0
  39. package/server/package.json +34 -0
  40. package/server/public/img/avatar_default.png +0 -0
  41. package/server/tsconfig.json +19 -0
  42. package/server/vendored-conf/app.ini +538 -0
  43. package/server/vendored-conf/gitignore/Actionscript +11 -0
  44. package/server/vendored-conf/gitignore/Ada +5 -0
  45. package/server/vendored-conf/gitignore/Agda +1 -0
  46. package/server/vendored-conf/gitignore/Android +32 -0
  47. package/server/vendored-conf/gitignore/Anjuta +3 -0
  48. package/server/vendored-conf/gitignore/AppEngine +2 -0
  49. package/server/vendored-conf/gitignore/AppceleratorTitanium +3 -0
  50. package/server/vendored-conf/gitignore/ArchLinuxPackages +13 -0
  51. package/server/vendored-conf/gitignore/Archives +27 -0
  52. package/server/vendored-conf/gitignore/Autotools +14 -0
  53. package/server/vendored-conf/gitignore/BricxCC +4 -0
  54. package/server/vendored-conf/gitignore/C +32 -0
  55. package/server/vendored-conf/gitignore/C Sharp +108 -0
  56. package/server/vendored-conf/gitignore/C++ +28 -0
  57. package/server/vendored-conf/gitignore/CFWheels +12 -0
  58. package/server/vendored-conf/gitignore/CMake +6 -0
  59. package/server/vendored-conf/gitignore/CUDA +6 -0
  60. package/server/vendored-conf/gitignore/CakePHP +13 -0
  61. package/server/vendored-conf/gitignore/ChefCookbook +9 -0
  62. package/server/vendored-conf/gitignore/Cloud9 +3 -0
  63. package/server/vendored-conf/gitignore/CodeIgniter +6 -0
  64. package/server/vendored-conf/gitignore/CodeKit +3 -0
  65. package/server/vendored-conf/gitignore/CommonLisp +3 -0
  66. package/server/vendored-conf/gitignore/Composer +6 -0
  67. package/server/vendored-conf/gitignore/Concrete5 +3 -0
  68. package/server/vendored-conf/gitignore/Coq +3 -0
  69. package/server/vendored-conf/gitignore/CraftCMS +3 -0
  70. package/server/vendored-conf/gitignore/DM +5 -0
  71. package/server/vendored-conf/gitignore/Dart +15 -0
  72. package/server/vendored-conf/gitignore/DartEditor +2 -0
  73. package/server/vendored-conf/gitignore/Delphi +57 -0
  74. package/server/vendored-conf/gitignore/Dreamweaver +3 -0
  75. package/server/vendored-conf/gitignore/Drupal +36 -0
  76. package/server/vendored-conf/gitignore/EPiServer +4 -0
  77. package/server/vendored-conf/gitignore/Eagle +36 -0
  78. package/server/vendored-conf/gitignore/Eclipse +39 -0
  79. package/server/vendored-conf/gitignore/EiffelStudio +2 -0
  80. package/server/vendored-conf/gitignore/Elisp +5 -0
  81. package/server/vendored-conf/gitignore/Elixir +4 -0
  82. package/server/vendored-conf/gitignore/Emacs +32 -0
  83. package/server/vendored-conf/gitignore/Ensime +4 -0
  84. package/server/vendored-conf/gitignore/Erlang +10 -0
  85. package/server/vendored-conf/gitignore/Espresso +1 -0
  86. package/server/vendored-conf/gitignore/ExpressionEngine +19 -0
  87. package/server/vendored-conf/gitignore/ExtJs +4 -0
  88. package/server/vendored-conf/gitignore/Fancy +2 -0
  89. package/server/vendored-conf/gitignore/Finale +13 -0
  90. package/server/vendored-conf/gitignore/FlexBuilder +3 -0
  91. package/server/vendored-conf/gitignore/ForceDotCom +4 -0
  92. package/server/vendored-conf/gitignore/FuelPHP +2 -0
  93. package/server/vendored-conf/gitignore/GWT +28 -0
  94. package/server/vendored-conf/gitignore/Gcov +5 -0
  95. package/server/vendored-conf/gitignore/GitBook +16 -0
  96. package/server/vendored-conf/gitignore/Go +24 -0
  97. package/server/vendored-conf/gitignore/Gradle +8 -0
  98. package/server/vendored-conf/gitignore/Grails +33 -0
  99. package/server/vendored-conf/gitignore/Haskell +16 -0
  100. package/server/vendored-conf/gitignore/IGORPro +5 -0
  101. package/server/vendored-conf/gitignore/IPythonNotebook +2 -0
  102. package/server/vendored-conf/gitignore/Idris +2 -0
  103. package/server/vendored-conf/gitignore/JDeveloper +13 -0
  104. package/server/vendored-conf/gitignore/Java +12 -0
  105. package/server/vendored-conf/gitignore/Jboss +19 -0
  106. package/server/vendored-conf/gitignore/Jekyll +3 -0
  107. package/server/vendored-conf/gitignore/JetBrains +46 -0
  108. package/server/vendored-conf/gitignore/Joomla +525 -0
  109. package/server/vendored-conf/gitignore/KDevelop4 +2 -0
  110. package/server/vendored-conf/gitignore/Kate +3 -0
  111. package/server/vendored-conf/gitignore/KiCAD +13 -0
  112. package/server/vendored-conf/gitignore/Kohana +2 -0
  113. package/server/vendored-conf/gitignore/LabVIEW +16 -0
  114. package/server/vendored-conf/gitignore/Laravel +4 -0
  115. package/server/vendored-conf/gitignore/Lazarus +30 -0
  116. package/server/vendored-conf/gitignore/Leiningen +12 -0
  117. package/server/vendored-conf/gitignore/LemonStand +21 -0
  118. package/server/vendored-conf/gitignore/LibreOffice +2 -0
  119. package/server/vendored-conf/gitignore/Lilypond +6 -0
  120. package/server/vendored-conf/gitignore/Linux +7 -0
  121. package/server/vendored-conf/gitignore/Lithium +2 -0
  122. package/server/vendored-conf/gitignore/Lua +41 -0
  123. package/server/vendored-conf/gitignore/LyX +4 -0
  124. package/server/vendored-conf/gitignore/Magento +126 -0
  125. package/server/vendored-conf/gitignore/Matlab +16 -0
  126. package/server/vendored-conf/gitignore/Maven +9 -0
  127. package/server/vendored-conf/gitignore/Mercurial +6 -0
  128. package/server/vendored-conf/gitignore/Mercury +13 -0
  129. package/server/vendored-conf/gitignore/MetaProgrammingSystem +16 -0
  130. package/server/vendored-conf/gitignore/MicrosoftOffice +10 -0
  131. package/server/vendored-conf/gitignore/ModelSim +23 -0
  132. package/server/vendored-conf/gitignore/Momentics +8 -0
  133. package/server/vendored-conf/gitignore/MonoDevelop +8 -0
  134. package/server/vendored-conf/gitignore/Nanoc +10 -0
  135. package/server/vendored-conf/gitignore/NetBeans +8 -0
  136. package/server/vendored-conf/gitignore/Nim +1 -0
  137. package/server/vendored-conf/gitignore/Ninja +2 -0
  138. package/server/vendored-conf/gitignore/Node +28 -0
  139. package/server/vendored-conf/gitignore/NotepadPP +2 -0
  140. package/server/vendored-conf/gitignore/OCaml +20 -0
  141. package/server/vendored-conf/gitignore/Objective-C +43 -0
  142. package/server/vendored-conf/gitignore/Opa +13 -0
  143. package/server/vendored-conf/gitignore/OpenCart +11 -0
  144. package/server/vendored-conf/gitignore/OracleForms +8 -0
  145. package/server/vendored-conf/gitignore/Packer +5 -0
  146. package/server/vendored-conf/gitignore/Perl +20 -0
  147. package/server/vendored-conf/gitignore/Phalcon +2 -0
  148. package/server/vendored-conf/gitignore/PhpStorm +63 -0
  149. package/server/vendored-conf/gitignore/PlayFramework +17 -0
  150. package/server/vendored-conf/gitignore/Plone +18 -0
  151. package/server/vendored-conf/gitignore/Prestashop +25 -0
  152. package/server/vendored-conf/gitignore/Processing +7 -0
  153. package/server/vendored-conf/gitignore/Python +58 -0
  154. package/server/vendored-conf/gitignore/Qooxdoo +5 -0
  155. package/server/vendored-conf/gitignore/Qt +34 -0
  156. package/server/vendored-conf/gitignore/R +16 -0
  157. package/server/vendored-conf/gitignore/ROS +47 -0
  158. package/server/vendored-conf/gitignore/Rails +35 -0
  159. package/server/vendored-conf/gitignore/Redcar +1 -0
  160. package/server/vendored-conf/gitignore/Redis +3 -0
  161. package/server/vendored-conf/gitignore/RhodesRhomobile +9 -0
  162. package/server/vendored-conf/gitignore/Ruby +36 -0
  163. package/server/vendored-conf/gitignore/Rust +11 -0
  164. package/server/vendored-conf/gitignore/SBT +9 -0
  165. package/server/vendored-conf/gitignore/SCons +2 -0
  166. package/server/vendored-conf/gitignore/SVN +1 -0
  167. package/server/vendored-conf/gitignore/Sass +2 -0
  168. package/server/vendored-conf/gitignore/Scala +17 -0
  169. package/server/vendored-conf/gitignore/Scrivener +7 -0
  170. package/server/vendored-conf/gitignore/Sdcc +8 -0
  171. package/server/vendored-conf/gitignore/SeamGen +26 -0
  172. package/server/vendored-conf/gitignore/SketchUp +1 -0
  173. package/server/vendored-conf/gitignore/SlickEdit +11 -0
  174. package/server/vendored-conf/gitignore/Stella +12 -0
  175. package/server/vendored-conf/gitignore/SublimeText +14 -0
  176. package/server/vendored-conf/gitignore/SugarCRM +25 -0
  177. package/server/vendored-conf/gitignore/Swift +43 -0
  178. package/server/vendored-conf/gitignore/Symfony +37 -0
  179. package/server/vendored-conf/gitignore/SymphonyCMS +6 -0
  180. package/server/vendored-conf/gitignore/SynopsysVCS +36 -0
  181. package/server/vendored-conf/gitignore/Tags +14 -0
  182. package/server/vendored-conf/gitignore/TeX +136 -0
  183. package/server/vendored-conf/gitignore/TextMate +3 -0
  184. package/server/vendored-conf/gitignore/Textpattern +11 -0
  185. package/server/vendored-conf/gitignore/TortoiseGit +2 -0
  186. package/server/vendored-conf/gitignore/TurboGears2 +20 -0
  187. package/server/vendored-conf/gitignore/Typo3 +20 -0
  188. package/server/vendored-conf/gitignore/Umbraco +17 -0
  189. package/server/vendored-conf/gitignore/Unity +21 -0
  190. package/server/vendored-conf/gitignore/UnrealEngine +76 -0
  191. package/server/vendored-conf/gitignore/VVVV +6 -0
  192. package/server/vendored-conf/gitignore/Vagrant +1 -0
  193. package/server/vendored-conf/gitignore/Vim +6 -0
  194. package/server/vendored-conf/gitignore/VirtualEnv +10 -0
  195. package/server/vendored-conf/gitignore/VisualStudio +217 -0
  196. package/server/vendored-conf/gitignore/VisualStudioCode +2 -0
  197. package/server/vendored-conf/gitignore/Waf +3 -0
  198. package/server/vendored-conf/gitignore/WebMethods +14 -0
  199. package/server/vendored-conf/gitignore/WebStorm +63 -0
  200. package/server/vendored-conf/gitignore/Windows +18 -0
  201. package/server/vendored-conf/gitignore/WordPress +18 -0
  202. package/server/vendored-conf/gitignore/Xcode +23 -0
  203. package/server/vendored-conf/gitignore/XilinxISE +57 -0
  204. package/server/vendored-conf/gitignore/Xojo +11 -0
  205. package/server/vendored-conf/gitignore/Yeoman +6 -0
  206. package/server/vendored-conf/gitignore/Yii +6 -0
  207. package/server/vendored-conf/gitignore/ZendFramework +20 -0
  208. package/server/vendored-conf/gitignore/Zephir +26 -0
  209. package/server/vendored-conf/gitignore/macOS +25 -0
  210. package/server/vendored-conf/label/Default +7 -0
  211. package/server/vendored-conf/license/Abstyles License +10 -0
  212. package/server/vendored-conf/license/Academic Free License v1.1 +22 -0
  213. package/server/vendored-conf/license/Academic Free License v1.2 +27 -0
  214. package/server/vendored-conf/license/Academic Free License v2.0 +43 -0
  215. package/server/vendored-conf/license/Academic Free License v2.1 +44 -0
  216. package/server/vendored-conf/license/Academic Free License v3.0 +42 -0
  217. package/server/vendored-conf/license/Affero General Public License v1.0 +84 -0
  218. package/server/vendored-conf/license/Apache License 1.0 +19 -0
  219. package/server/vendored-conf/license/Apache License 1.1 +20 -0
  220. package/server/vendored-conf/license/Apache License 2.0 +72 -0
  221. package/server/vendored-conf/license/Artistic License 1.0 +48 -0
  222. package/server/vendored-conf/license/Artistic License 2.0 +84 -0
  223. package/server/vendored-conf/license/BSD 2-clause License +8 -0
  224. package/server/vendored-conf/license/BSD 3-clause License +10 -0
  225. package/server/vendored-conf/license/BSD 4-clause License +13 -0
  226. package/server/vendored-conf/license/Creative Commons CC0 1.0 Universal +39 -0
  227. package/server/vendored-conf/license/Eclipse Public License 1.0 +72 -0
  228. package/server/vendored-conf/license/Educational Community License v1.0 +22 -0
  229. package/server/vendored-conf/license/Educational Community License v2.0 +97 -0
  230. package/server/vendored-conf/license/GNU Affero General Public License v3.0 +241 -0
  231. package/server/vendored-conf/license/GNU Free Documentation License v1.1 +118 -0
  232. package/server/vendored-conf/license/GNU Free Documentation License v1.2 +129 -0
  233. package/server/vendored-conf/license/GNU Free Documentation License v1.3 +148 -0
  234. package/server/vendored-conf/license/GNU General Public License v1.0 +99 -0
  235. package/server/vendored-conf/license/GNU General Public License v2.0 +116 -0
  236. package/server/vendored-conf/license/GNU General Public License v3.0 +231 -0
  237. package/server/vendored-conf/license/GNU Lesser General Public License v2.1 +174 -0
  238. package/server/vendored-conf/license/GNU Lesser General Public License v3.0 +70 -0
  239. package/server/vendored-conf/license/GNU Library General Public License v2.0 +173 -0
  240. package/server/vendored-conf/license/ISC license +6 -0
  241. package/server/vendored-conf/license/MIT License +8 -0
  242. package/server/vendored-conf/license/Mozilla Public License 1.0 +122 -0
  243. package/server/vendored-conf/license/Mozilla Public License 1.1 +142 -0
  244. package/server/vendored-conf/license/Mozilla Public License 2.0 +143 -0
  245. package/server/vendored-conf/readme/Default +3 -0
  246. package/src/dsh-host.js +168 -0
  247. package/src/index.js +682 -0
  248. package/src/runner.js +364 -0
@@ -0,0 +1,2342 @@
1
+ /**
2
+ * @weibaohui/dsh-git-server — Browser half(原生 dsh 插件 UI,无 iframe/桥接)。
3
+ *
4
+ * 两个面:
5
+ * 1. sidebar.footer.action「Git」→ 全屏管理页(仓库列表/建仓/删除 +
6
+ * 仓库浏览:文件树/文件内容/提交/分支/工单[列表/新建/评论/开关])。
7
+ * 2. settings.section「Git 服务器」→ 服务配置(启停/端口/数据目录/兜底密码)。
8
+ *
9
+ * 数据通道:宿主同源路由 /dsh-git-server/api/*(dsh 登录门禁保护,
10
+ * 宿主按 dsh 会话解析操作者并以其身份代理 kernel API)。
11
+ * 主题:直接用 --dsw-alias-* token(dsh 原生,亮暗自动跟随)。
12
+ */
13
+
14
+ let __React = null
15
+ try { __React = require('react') } catch {}
16
+ if (!__React || typeof __React.createElement !== 'function') {
17
+ __React = {
18
+ createElement(type, props, ...kids) {
19
+ return { type, props: props || {}, kids: kids.flat(9).filter(k => k !== null && k !== undefined && k !== false && k !== true || true) }
20
+ },
21
+ useState(init) { const v = [typeof init === 'function' ? init() : init]; return [v[0], x => { v[0] = typeof x === 'function' ? x(v[0]) : x }] },
22
+ useEffect() {}, useMemo(fn) { return fn() }, useCallback(fn) { return fn }, useRef(v = null) { return { current: v } },
23
+ }
24
+ }
25
+ const { createElement: h, useState, useEffect, useCallback } = __React
26
+
27
+ // ── locale ────────────────────────────────────────────────────────────────
28
+
29
+ const NS = 'dshGitServer'
30
+ const ZH = {
31
+ nav: 'Git', title: '代码仓库',
32
+ myRepos: '我的仓库', newRepo: '新建仓库', repoName: '仓库名', private: '私有',
33
+ create: '创建', delete: '删除', deleteConfirm: '确定删除仓库', open: '打开',
34
+ clone: '克隆地址', copy: '复制', copied: '已复制',
35
+ files: '文件', commits: '提交', branches: '分支', issues: '工单',
36
+ emptyRepo: '还没有仓库,先建一个', back: '‹ 返回',
37
+ issueNew: '新工单', issueTitle: '标题', issueBody: '描述', submit: '提交',
38
+ comment: '评论', commentPlaceholder: '说点什么…', close: '关闭工单', reopen: '重新打开',
39
+ noIssues: '暂无工单', openState: '开启', closedState: '已关闭',
40
+ loading: '加载中…', loadFailed: '加载失败', retry: '重试',
41
+ emptyDir: '空目录', emptyFile: '(二进制或空文件)',
42
+ tags: '标签', pulls: 'PR', wiki: 'Wiki', releases: '发版',
43
+ readme: 'README', noPulls: '暂无 PR', merged: '已合并', merge: '合并',
44
+ mergeConfirm: '确认合并此 PR?', headToBase: '→', newWiki: '新页面',
45
+ edit: '编辑', save: '保存', wikiTitle: '页面名',
46
+ newRelease: '发新版本', tag: 'Tag', relTitle: '标题', relNote: '说明', target: '目标分支/提交',
47
+ noReleases: '暂无发版', noWiki: '暂无页面——创建第一个', deletePage: '删除页面',
48
+ prMerged: '已合并', prOpen: '开启中', prClosed: '已关闭',
49
+ labels: '标签管理', milestones: '里程碑', settings: '设置',
50
+ }
51
+ const EN = {
52
+ nav: 'Git', title: 'Repositories',
53
+ myRepos: 'My repositories', newRepo: 'New repository', repoName: 'Repository name', private: 'Private',
54
+ create: 'Create', delete: 'Delete', deleteConfirm: 'Delete repository', open: 'Open',
55
+ clone: 'Clone URL', copy: 'Copy', copied: 'Copied',
56
+ files: 'Files', commits: 'Commits', branches: 'Branches', issues: 'Issues',
57
+ emptyRepo: 'No repositories yet — create one', back: '‹ Back',
58
+ issueNew: 'New issue', issueTitle: 'Title', issueBody: 'Description', submit: 'Submit',
59
+ comment: 'Comment', commentPlaceholder: 'Say something…', close: 'Close issue', reopen: 'Reopen',
60
+ noIssues: 'No issues', openState: 'Open', closedState: 'Closed',
61
+ loading: 'Loading…', loadFailed: 'Failed to load', retry: 'Retry',
62
+ emptyDir: 'Empty directory', emptyFile: '(binary or empty)',
63
+ tags: 'Tags', pulls: 'PRs', wiki: 'Wiki', releases: 'Releases',
64
+ readme: 'README', noPulls: 'No pull requests', merged: 'Merged', merge: 'Merge',
65
+ mergeConfirm: 'Merge this PR?', headToBase: '→', newWiki: 'New page',
66
+ edit: 'Edit', save: 'Save', wikiTitle: 'Page name',
67
+ newRelease: 'New release', tag: 'Tag', relTitle: 'Title', relNote: 'Notes', target: 'Target',
68
+ noReleases: 'No releases', noWiki: 'No pages yet — create one', deletePage: 'Delete page',
69
+ prMerged: 'Merged', prOpen: 'Open', prClosed: 'Closed',
70
+ labels: 'Labels', milestones: 'Milestones', settings: 'Settings',
71
+ }
72
+
73
+ // ── styles(dsw token 原生) ───────────────────────────────────────────────
74
+
75
+ function ensureStyles() {
76
+ if (typeof document === 'undefined' || document.getElementById('dgs-styles')) return
77
+ const holder = document.createElement('div')
78
+ holder.id = 'dgs-styles'
79
+ holder.style.display = 'none'
80
+ holder.innerHTML = `<style>
81
+ .dgs-page{position:fixed;inset:0;z-index:2147483000;display:flex;flex-direction:column;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);font-family:var(--dsw-font-family);font-size:14px}
82
+ .dgs-head{display:flex;align-items:center;gap:12px;padding:12px 28px;border-bottom:1px solid var(--dsw-alias-border-l2);flex:none}
83
+ .dgs-h1{font-size:16px;font-weight:700;margin:0}
84
+ .dgs-close{margin-left:auto;cursor:pointer;border:none;background:transparent;color:var(--dsw-alias-label-secondary);font-size:18px;padding:4px 8px;border-radius:8px}
85
+ .dgs-close:hover{background:var(--dsw-alias-interactive-bg-hover)}
86
+ .dgs-body{box-sizing:border-box;flex:1;overflow-x:hidden;overflow-y:auto;padding:20px 28px 32px;max-width:1156px;width:100%;margin:0 auto}
87
+ .dgs-card{background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l2);border-radius:12px;padding:14px 16px;margin-bottom:10px}
88
+ .dgs-row{display:flex;align-items:center;gap:10px;flex-wrap:wrap}
89
+ .dgs-repo{cursor:pointer;transition:border-color .12s}
90
+ .dgs-repo:hover{border-color:var(--dsw-alias-state-business-primary)}
91
+ .dgs-name{font-weight:600;font-size:15px;color:var(--dsw-alias-label-primary);text-decoration:none}
92
+ .dgs-name:hover{text-decoration:underline}
93
+ .dgs-badge{font-size:11px;padding:1px 8px;border-radius:999px;border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary)}
94
+ .dgs-badge.pri{color:#f59e0b;border-color:#f59e0b55}
95
+ .dgs-badge.ok{color:var(--dsw-alias-state-success-primary);border-color:var(--dsw-alias-state-success-primary)44}
96
+ .dgs-badge.closed{color:var(--dsw-alias-label-tertiary)}
97
+ .dgs-labeled{display:inline-flex;align-items:stretch;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;overflow:hidden}
98
+ .dgs-labeled .dgs-btn{border:none;border-radius:0}
99
+ .dgs-labeled-count{display:inline-flex;align-items:center;padding:0 10px;font-size:12px;font-weight:600;color:var(--dsw-alias-label-secondary);border-left:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2)}
100
+ .dgs-sub{font-size:12px;color:var(--dsw-alias-label-tertiary)}
101
+ .dgs-btn{cursor:pointer;border:none;border-radius:8px;padding:6px 14px;font-size:13px;font-weight:500;background:var(--dsw-alias-state-business-primary);color:#fff}
102
+ .dgs-btn:hover{filter:brightness(1.1)}
103
+ .dgs-btn:disabled{opacity:.5;cursor:default}
104
+ .dgs-btn.ghost{background:transparent;color:var(--dsw-alias-label-secondary);border:1px solid var(--dsw-alias-border-l2)}
105
+ .dgs-btn.ghost:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary);filter:none}
106
+ .dgs-btn.danger{background:transparent;color:var(--dsw-alias-state-error-primary);border:1px solid var(--dsw-alias-state-error-primary)55}
107
+ .dgs-input{flex:1;min-width:160px;padding:7px 11px;font-size:13px;color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l2);border-radius:8px;outline:none}
108
+ .dgs-input:focus{border-color:var(--dsw-alias-state-business-primary)}
109
+ textarea.dgs-input{min-height:72px;resize:vertical}
110
+ /* 卡片直属的输入控件不在 flex 行内,flex:1 失效——显式撑满 */
111
+ textarea.dgs-input,.dgs-card>.dgs-input{width:100%;box-sizing:border-box;flex:none}
112
+ .dgs-subtabs{display:flex;gap:6px;margin-bottom:14px}
113
+ .dgs-subtab{cursor:pointer;border:none;border-radius:999px;padding:4px 14px;font-size:12.5px;font-weight:500;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-secondary)}
114
+ .dgs-subtab.active{background:var(--dsw-alias-state-business-primary);color:#fff}
115
+ .dgs-stats{display:flex;border:1px solid var(--dsw-alias-border-l2);border-radius:10px;overflow:hidden;margin-bottom:8px}
116
+ .dgs-stats-item{flex:1;display:flex;align-items:center;justify-content:center;gap:6px;padding:10px 0;cursor:pointer;color:var(--dsw-alias-label-secondary);border-right:1px solid var(--dsw-alias-border-l2)}
117
+ .dgs-stats-item:last-child{border-right:none}
118
+ .dgs-stats-item:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}
119
+ .dgs-stats-num{font-weight:700;color:var(--dsw-alias-label-primary)}
120
+ .dgs-pill{cursor:pointer;border:1px solid var(--dsw-alias-state-success-primary)55;background:transparent;color:var(--dsw-alias-state-success-primary);border-radius:6px;padding:4px 12px;font-size:12px;font-weight:500}
121
+ .dgs-pill.active{background:color-mix(in srgb,var(--dsw-alias-state-success-primary) 10%,transparent)}
122
+ .dgs-pill.closed{border-color:var(--dsw-alias-state-error-primary)55;color:var(--dsw-alias-state-error-primary)}
123
+ .dgs-pill.closed.active{background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 10%,transparent)}
124
+ .dgs-issue-num{flex:none;width:34px;height:34px;border-radius:6px;background:var(--dsw-alias-state-success-primary);color:#fff;display:inline-flex;align-items:center;justify-content:center;font-size:11px;font-weight:700}
125
+ .dgs-issue-num.closed{background:var(--dsw-alias-label-tertiary)}
126
+ .dgs-issue-title{font-weight:500;color:var(--dsw-alias-state-business-primary)}
127
+ .dgs-tabs{display:flex;flex-wrap:wrap;gap:4px;border-bottom:1px solid var(--dsw-alias-border-l2);margin-bottom:14px}
128
+ .dgs-tab{cursor:pointer;border:none;background:transparent;color:var(--dsw-alias-label-secondary);font-size:13px;font-weight:500;padding:8px 14px;border-radius:8px 8px 0 0;border-bottom:2px solid transparent}
129
+ .dgs-tab.active{color:var(--dsw-alias-state-business-primary);border-bottom-color:var(--dsw-alias-state-business-primary)}
130
+ .dgs-table{width:100%;border-collapse:collapse}
131
+ .dgs-table td{padding:7px 10px;border-bottom:1px solid var(--dsw-alias-border-l1);font-size:13px}
132
+ .dgs-table tr:hover td{background:var(--dsw-alias-interactive-bg-hover)}
133
+ .dgs-file{white-space:pre-wrap;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12.5px;line-height:1.7;background:var(--dsw-alias-markdown-code-block);border:1px solid var(--dsw-alias-border-l2);border-radius:10px;padding:14px;overflow:auto;max-height:60vh}
134
+ .dgs-diff-line{display:flex;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px;line-height:20px;padding:0 8px}
135
+ .dgs-diff-line.hunk{color:var(--dsw-alias-label-tertiary);background:var(--dsw-alias-bg-layer-2);margin:2px 0}
136
+ .dgs-diff-line.add{background:color-mix(in srgb,var(--dsw-alias-state-success-primary) 12%,transparent)}
137
+ .dgs-diff-line.del{background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 12%,transparent)}
138
+ .dgs-diff-no{flex:none;width:44px;text-align:right;padding-right:10px;color:var(--dsw-alias-label-tertiary);user-select:none}
139
+ .dgs-diff-stat{font-size:12px;font-weight:600;color:var(--dsw-alias-state-success-primary)}
140
+ .dgs-diff-stat.del{color:var(--dsw-alias-state-error-primary);margin-left:8px}
141
+ .dgs-crumbs{display:flex;gap:6px;align-items:center;font-size:13px;margin-bottom:10px;flex-wrap:wrap}
142
+ .dgs-crumb{color:var(--dsw-alias-label-secondary);cursor:pointer}
143
+ .dgs-crumb:hover{color:var(--dsw-alias-state-business-primary)}
144
+ .dgs-crumb.cur{color:var(--dsw-alias-label-primary);cursor:default;font-weight:500}
145
+ .dgs-issue{border-bottom:1px solid var(--dsw-alias-border-l1);padding:10px 4px}
146
+ .dgs-issue-layout{display:flex;gap:18px;align-items:flex-start}
147
+ .dgs-issue-main{flex:1;min-width:0}
148
+ .dgs-issue-side{width:230px;flex:none}
149
+ .dgs-side-block{border-bottom:1px solid var(--dsw-alias-border-l2);padding:10px 0}
150
+ .dgs-side-title{font-weight:700;font-size:12.5px;margin-bottom:4px}
151
+ .dgs-settings-layout{display:flex;gap:20px;align-items:flex-start}
152
+ .dgs-settings-nav{width:180px;flex:none;border-right:1px solid var(--dsw-alias-border-l2);padding-right:12px}
153
+ .dgs-settings-nav .item{display:block;padding:7px 10px;border-radius:8px;cursor:pointer;font-size:13px;color:var(--dsw-alias-label-secondary)}
154
+ .dgs-settings-nav .item.active{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary);font-weight:600}
155
+ .dgs-settings-nav .item:hover{background:var(--dsw-alias-interactive-bg-hover)}
156
+ .dgs-comment{background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l2);border-radius:10px;padding:10px 12px;margin:8px 0}
157
+ .dgs-empty{color:var(--dsw-alias-label-tertiary);text-align:center;padding:40px 0;font-size:13px}
158
+ .dgs-err{color:var(--dsw-alias-state-error-primary);font-size:13px}
159
+ .dgs-ico{margin-right:8px;opacity:.75}
160
+ .dgs-toast{position:fixed;left:50%;bottom:28px;transform:translateX(-50%);background:var(--dsw-alias-toast-bg);color:#fff;border-radius:8px;padding:8px 16px;font-size:12px;z-index:2147483600}
161
+ .dgs-settings{max-width:720px;display:flex;flex-direction:column;gap:12px;padding:12px 16px 24px;color:var(--dsw-alias-label-primary)}
162
+ .dgs-settings .dgs-card{margin-bottom:0}
163
+ .dgs-settings label{font-size:13px;color:var(--dsw-alias-label-secondary);min-width:120px}
164
+ .dgs-md{font-size:13.5px;line-height:1.75}
165
+ .dgs-md h1,.dgs-md h2,.dgs-md h3{margin:14px 0 8px;color:var(--dsw-alias-label-primary)}
166
+ .dgs-md p{margin:8px 0}
167
+ .dgs-md pre{background:var(--dsw-alias-markdown-code-block);border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:10px;overflow:auto;font-size:12.5px}
168
+ .dgs-md code{background:var(--dsw-alias-markdown-inline-code);border-radius:4px;padding:1px 5px;font-size:12.5px}
169
+ .dgs-md pre code{background:transparent;padding:0}
170
+ .dgs-md a{color:var(--dsw-alias-state-business-primary)}
171
+ .dgs-md img{max-width:100%}
172
+ .dgs-md blockquote{border-left:3px solid var(--dsw-alias-border-l2);margin:8px 0;padding:2px 12px;color:var(--dsw-alias-label-secondary)}
173
+ .dgs-md table{border-collapse:collapse}
174
+ .dgs-md td,.dgs-md th{border:1px solid var(--dsw-alias-border-l2);padding:4px 10px}
175
+ </style>`
176
+ document.head.appendChild(holder)
177
+ }
178
+
179
+ // ── data helpers ──────────────────────────────────────────────────────────
180
+
181
+ const API = '/dsh-git-server/api'
182
+ async function api(method, path, body) {
183
+ const res = await fetch(API + path, {
184
+ method,
185
+ headers: body !== undefined ? { 'Content-Type': 'application/json' } : {},
186
+ body: body !== undefined ? JSON.stringify(body) : undefined,
187
+ })
188
+ try { return await res.json() } catch { return { ok: false, error: 'http ' + res.status } }
189
+ }
190
+
191
+ // ── hash 路由 ──────────────────────────────────────────────────────────────
192
+
193
+ function useHashRoute() {
194
+ const [route, setRoute] = useState(() => location.hash.slice(1) || '/')
195
+ useEffect(() => {
196
+ const on = () => setRoute(location.hash.slice(1) || '/')
197
+ window.addEventListener('hashchange', on)
198
+ return () => window.removeEventListener('hashchange', on)
199
+ }, [])
200
+ return route
201
+ }
202
+ const nav = (hash) => { location.hash = hash }
203
+
204
+ // ── 全屏管理页 ────────────────────────────────────────────────────────────
205
+
206
+ function RepoBrowser({ repo, t, onBack }) {
207
+ const [tab, setTab] = useState('files')
208
+ const [prPreset, setPrPreset] = useState(null)
209
+ const [issuesSub, setIssuesSub] = useState(null)
210
+ const [ov, setOv] = useState(null)
211
+ const [rev, setRev] = useState('')
212
+ const [cloneUrl, setCloneUrl] = useState('')
213
+ const [copied, setCopied] = useState(false)
214
+ const [star, setStar] = useState(null)
215
+ const [watch, setWatch] = useState(null)
216
+ const [forkBusy, setForkBusy] = useState(false)
217
+ const [msg, setMsg] = useState('')
218
+ useEffect(() => {
219
+ api('GET', `/dsh/repos/${repo.owner}/${repo.name}/overview`).then((d) => {
220
+ if (!d.defaultBranch) return
221
+ setOv(d)
222
+ setRev(d.defaultBranch)
223
+ })
224
+ fetch(API + '/status').then((r) => r.json()).then((st) => {
225
+ if (st && st.running) setCloneUrl('git clone http://' + location.hostname + ':' + (st.port || 3400) + '/' + repo.owner + '/' + repo.name + '.git')
226
+ }).catch(() => {})
227
+ api('GET', `/dsh/repos/${repo.owner}/${repo.name}/star`).then((d) => d.count !== undefined && setStar(d))
228
+ api('GET', `/dsh/repos/${repo.owner}/${repo.name}/watch`).then((d) => d.count !== undefined && setWatch(d))
229
+ }, [repo.owner, repo.name])
230
+ const toggle = async (kind) => {
231
+ const d = await api('POST', `/dsh/repos/${repo.owner}/${repo.name}/${kind}`)
232
+ if (d.on !== undefined) (kind === 'star' ? setStar : setWatch)({ on: d.on, count: d.count })
233
+ }
234
+ const fork = async () => {
235
+ setForkBusy(true); setMsg('')
236
+ const d = await api('POST', `/dsh/repos/${repo.owner}/${repo.name}/fork`)
237
+ setForkBusy(false)
238
+ if (d.ok) nav('/r/' + d.owner + '/' + d.name)
239
+ else setMsg(d.error || 'fork failed')
240
+ }
241
+ const branchSel = ov && ov.branches && ov.branches.length
242
+ ? h('select', {
243
+ className: 'dgs-input', style: { flex: 'none', width: 'auto', padding: '4px 8px' },
244
+ value: rev, onChange: (e) => setRev(e.target.value),
245
+ }, ov.branches.map((b) => h('option', { key: b, value: b }, b)))
246
+ : null
247
+ return h('div', null,
248
+ h('div', { className: 'dgs-row', style: { marginBottom: 8 } },
249
+ h('button', { className: 'dgs-btn ghost', onClick: onBack }, t('back')),
250
+ h('span', { className: 'dgs-h1' }, repo.owner + '/' + repo.name),
251
+ repo.private || (ov && ov.private) ? h('span', { className: 'dgs-badge pri' }, t('private')) : null,
252
+ h('span', { style: { flex: 1 } }),
253
+ star ? h('span', { className: 'dgs-labeled' },
254
+ h('button', { className: 'dgs-btn ghost', onClick: () => toggle('star') }, star.on ? '★ Unstar' : '☆ Star'),
255
+ h('span', { className: 'dgs-labeled-count' }, star.count)) : null,
256
+ watch ? h('span', { className: 'dgs-labeled' },
257
+ h('button', { className: 'dgs-btn ghost', onClick: () => toggle('watch') }, watch.on ? '👁 Unwatch' : '👁 Watch'),
258
+ h('span', { className: 'dgs-labeled-count' }, watch.count)) : null,
259
+ h('button', { className: 'dgs-btn ghost', disabled: forkBusy, onClick: fork, style: { display: 'inline-flex', alignItems: 'center', gap: 6 } },
260
+ h('span', { style: { display: 'inline-flex', lineHeight: 0 }, dangerouslySetInnerHTML: { __html: '<svg width="13" height="13" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"><circle cx="4" cy="3" r="1.7" fill="currentColor" stroke="none"/><circle cx="12" cy="3" r="1.7" fill="currentColor" stroke="none"/><circle cx="8" cy="13" r="1.7" fill="currentColor" stroke="none"/><path d="M4 4.7v2.8c0 .6.4 1 1 1h2.2M12 4.7v2.8c0 .6-.4 1-1 1H8.8M8 8.5v2.8"/></svg>' } }),
261
+ 'Fork'),
262
+ ),
263
+ msg ? h('div', { className: 'dgs-err' }, msg) : null,
264
+ h('div', { className: 'dgs-tabs' },
265
+ ['files', 'issues', 'pulls', 'wiki', 'releases', 'settings'].map((k) =>
266
+ h('button', { key: k, className: 'dgs-tab' + (tab === k ? ' active' : ''), onClick: () => setTab(k) }, t(k)))),
267
+ (tab === 'commits' || tab === 'branches' || tab === 'tags') ? h('div', { className: 'dgs-tabs' },
268
+ h('button', { className: 'dgs-tab', onClick: () => setTab('files') }, t('files')),
269
+ h('button', { className: 'dgs-tab active' }, tab === 'commits' ? t('commits') : tab === 'branches' ? t('branches') : '标签'))
270
+ : null,
271
+ ov && tab === 'files' ? h('div', { style: { margin: '0 0 10px' } },
272
+ ov.description ? h('div', { className: 'dgs-sub', style: { marginBottom: 8 } }, ov.description) : h('div', { className: 'dgs-sub', style: { marginBottom: 8, fontStyle: 'italic' } }, '暂无描述'),
273
+ h('div', { className: 'dgs-stats' },
274
+ h('a', { className: 'dgs-stats-item', onClick: () => setTab('commits') },
275
+ h('span', { className: 'dgs-stats-num' }, '⎇ ' + (ov.numCommits || 0)), ' 提交历史'),
276
+ h('a', { className: 'dgs-stats-item', onClick: () => setTab('branches') },
277
+ h('span', { className: 'dgs-stats-num' }, '⑂ ' + (ov.numBranches || 0)), ' 代码分支'),
278
+ h('a', { className: 'dgs-stats-item', onClick: () => setTab('releases') },
279
+ h('span', { className: 'dgs-stats-num' }, '◎ ' + (ov.numReleases || 0)), ' 版本发布')),
280
+ h('div', { className: 'dgs-row', style: { margin: '8px 0 0' } },
281
+ branchSel,
282
+ h('span', { style: { flex: 1 } }),
283
+ cloneUrl ? h('span', { className: 'dgs-labeled' },
284
+ h('span', { className: 'dgs-labeled-count', style: { borderLeft: 'none', borderRight: '1px solid var(--dsw-alias-border-l2)' } }, 'HTTP'),
285
+ h('button', { className: 'dgs-btn ghost', style: { borderRadius: 0, padding: '4px 8px', fontFamily: 'ui-monospace,monospace', fontSize: 12, maxWidth: 340, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }, onClick: () => {
286
+ try { navigator.clipboard.writeText(cloneUrl) } catch {}
287
+ setCopied(true); setTimeout(() => setCopied(false), 1600)
288
+ }, title: cloneUrl }, copied ? t('copied') : cloneUrl.replace('git clone ', '')),
289
+ h('a', { className: 'dgs-btn ghost', style: { borderRadius: '0 8px 8px 0', borderLeft: 'none' }, href: `/dsh-git-server/api/dsh/repos/${repo.owner}/${repo.name}/archive/${encodeURIComponent(rev || 'master')}.zip`, download: `${repo.name}-${rev}.zip`, title: '下载' }, '⭳'))
290
+ : null))
291
+ : null,
292
+ tab === 'files' && h(FileTree, { repo, rev: rev || 'master', t, overview: ov, onOverview: setOv }),
293
+ tab === 'commits' && h(Commits, { repo, rev: rev || 'master', t }),
294
+ tab === 'branches' && h(Branches, { repo, t, onNewPR: (head, base) => { setPrPreset({ head, base }); setTab('pulls') } }),
295
+ tab === 'issues' && h(IssuesArea, { repo, t, initialSub: issuesSub || undefined }),
296
+ tab === 'pulls' && h(Pulls, { repo, t, preset: prPreset, onPresetDone: () => setPrPreset(null), onGoIssuesSub: (k) => { setIssuesSub(k); setTab('issues') } }),
297
+ tab === 'wiki' && h(WikiView, { repo, t }),
298
+ tab === 'releases' && h(Releases, { repo, t }),
299
+ tab === 'settings' && h(RepoSettings, { repo, t }),
300
+ )
301
+ }
302
+
303
+ function FileTree({ repo, rev, t, overview, onOverview }) {
304
+ const [path, setPath] = useState('')
305
+ const [entries, setEntries] = useState(null)
306
+ const [file, setFile] = useState(null)
307
+ const [err, setErr] = useState('')
308
+ const [readme, setReadme] = useState(null)
309
+ const [blameOn, setBlameOn] = useState(false)
310
+ const [newFile, setNewFile] = useState(null) // {mode:'create'|'upload'}
311
+ const [nf, setNf] = useState({ path: '', content: '', message: '' })
312
+ const [busy, setBusy] = useState(false)
313
+ const [msg, setMsg] = useState('')
314
+ const [editing, setEditing] = useState(false)
315
+ const [editText, setEditText] = useState('')
316
+ const [editMsg, setEditMsg] = useState('')
317
+ const [histMode, setHistMode] = useState(null)
318
+ const load = useCallback((p) => {
319
+ setFile(null); setEntries(null); setErr(''); setBlameOn(false); setEditing(false); setHistMode(null)
320
+ api('GET', `/dsh/repos/${repo.owner}/${repo.name}/tree?ref=${encodeURIComponent(rev)}&path=${encodeURIComponent(p)}`)
321
+ .then((d) => {
322
+ const entries = (d.data && d.data.entries) || d.entries
323
+ if (entries) setEntries(entries)
324
+ else setErr((d && d.error) || t('loadFailed'))
325
+ })
326
+ .catch(() => setErr(t('loadFailed')))
327
+ if (p === '' && overview === undefined) {
328
+ api('GET', `/dsh/repos/${repo.owner}/${repo.name}/overview`).then((d) => d.defaultBranch && onOverview(d))
329
+ }
330
+ if (p !== '') setReadme(null)
331
+ }, [repo.owner, repo.name, rev])
332
+ useEffect(() => { load(path) }, [load, path])
333
+ const openEntry = (e) => {
334
+ const p = e.path
335
+ if (e.type === 'dir' || e.type === 'tree') { setPath(p); return }
336
+ api('GET', `/repos/${repo.owner}/${repo.name}/raw?ref=${encodeURIComponent(rev)}&path=${encodeURIComponent(p)}`)
337
+ .then(async (d) => {
338
+ const isMd = /\.md$/i.test(e.name)
339
+ let html = ''
340
+ if (isMd && d.ok) {
341
+ const r = await api('POST', '/dsh/markdown', { text: d.text })
342
+ html = r.html || ''
343
+ }
344
+ setFile({ name: e.name, text: d.ok ? d.text : null, size: e.size, isMd, html })
345
+ })
346
+ }
347
+ const startCreate = (mode) => {
348
+ setNewFile(mode); setMsg('')
349
+ setNf({ path: '', content: '', message: '' })
350
+ }
351
+ const submitFile = async () => {
352
+ setBusy(true); setMsg('')
353
+ const full = (path ? path + '/' : '') + nf.path.trim()
354
+ const body = { path: full, branch: rev, message: nf.message }
355
+ if (newFile === 'upload') {
356
+ if (!nf.content) { setBusy(false); setMsg('请选择文件'); return }
357
+ body.contentBase64 = btoa(unescape(encodeURIComponent(nf.content)))
358
+ body.overwrite = true
359
+ } else {
360
+ if (!nf.path.trim()) { setBusy(false); setMsg('文件名必填'); return }
361
+ body.content = nf.content
362
+ }
363
+ const d = await api('POST', `/dsh/repos/${repo.owner}/${repo.name}/files`, body)
364
+ setBusy(false)
365
+ if (d && d.ok) { setNewFile(null); load(path) }
366
+ else setMsg((d && d.error) || 'failed')
367
+ }
368
+ return h('div', null,
369
+ h('div', { className: 'dgs-row', style: { marginBottom: 8 } },
370
+ h('div', { className: 'dgs-crumbs', style: { margin: 0 } },
371
+ h('span', { className: 'dgs-crumb' + (path ? '' : ' cur'), style: { cursor: path ? 'pointer' : 'default' }, onClick: () => setPath('') }, rev),
372
+ path.split('/').filter(Boolean).map((seg, i, arr) =>
373
+ h('span', { key: i },
374
+ h('span', { className: 'dgs-crumb' + (i === arr.length - 1 ? ' cur' : ''), onClick: () => setPath(arr.slice(0, i + 1).join('/')) }, seg),
375
+ i < arr.length - 1 ? h('span', { className: 'dgs-sub' }, '/') : null))),
376
+ h('span', { style: { flex: 1 } }),
377
+ h('button', { className: 'dgs-btn', onClick: () => startCreate('create') }, '新的文件'),
378
+ h('button', { className: 'dgs-btn ghost', onClick: () => startCreate('upload') }, '上传文件')),
379
+ msg ? h('div', { className: 'dgs-err' }, msg) : null,
380
+ newFile ? h('div', { className: 'dgs-card', style: { margin: '8px 0' } },
381
+ h('div', { className: 'dgs-row', style: { marginBottom: 8 } },
382
+ h('span', { style: { fontWeight: 700 } }, newFile === 'create' ? '新建文件' : '上传文件'),
383
+ h('span', { style: { flex: 1 } }),
384
+ h('button', { className: 'dgs-btn ghost', onClick: () => setNewFile(null) }, t('back'))),
385
+ h('div', { className: 'dgs-row' },
386
+ h('span', { className: 'dgs-sub' }, (path || '') + '/'),
387
+ h('input', { className: 'dgs-input', placeholder: newFile === 'create' ? '文件名(如 docs/guide.md)' : '目标路径', value: nf.path, onChange: (e) => setNf({ ...nf, path: e.target.value }) })),
388
+ newFile === 'create' ? h('textarea', { className: 'dgs-input', style: { marginTop: 8, minHeight: 160 }, placeholder: '文件内容', value: nf.content, onChange: (e) => setNf({ ...nf, content: e.target.value }) })
389
+ : h('input', { type: 'file', style: { marginTop: 8 }, onChange: async (e) => {
390
+ const f = e.target.files && e.target.files[0]
391
+ if (!f) return
392
+ if (!nf.path) setNf((x) => ({ ...x, path: f.name }))
393
+ setNf((x) => ({ ...x, content: 'BIN:' + f.name }))
394
+ const buf = await f.arrayBuffer()
395
+ let bin = ''
396
+ new Uint8Array(buf).forEach((b) => { bin += String.fromCharCode(b) })
397
+ setNf((x) => ({ ...x, content: btoa(bin) }))
398
+ } }),
399
+ h('div', { className: 'dgs-row', style: { marginTop: 8 } },
400
+ h('input', { className: 'dgs-input', placeholder: '提交信息(可选)', value: nf.message, onChange: (e) => setNf({ ...nf, message: e.target.value }) })),
401
+ h('div', { className: 'dgs-row', style: { marginTop: 8, justifyContent: 'flex-end' } },
402
+ h('button', { className: 'dgs-btn', disabled: busy || !nf.path.trim(), onClick: submitFile }, '提交修改')))
403
+ : null,
404
+ err ? h('div', { className: 'dgs-err' }, err) : null,
405
+ file ? h('div', null,
406
+ h('div', { className: 'dgs-row', style: { margin: '8px 0 10px' } },
407
+ h('button', { className: 'dgs-btn ghost', onClick: () => setFile(null) }, '⑂ ' + rev),
408
+ h('span', { className: 'dgs-sub' }, repo.name + ' /'),
409
+ h('span', { style: { fontWeight: 600 } }, file.name)),
410
+ h('div', { className: 'dgs-card', style: { padding: 0, overflow: 'hidden' } },
411
+ h('div', { className: 'dgs-row', style: { padding: '8px 12px', borderBottom: '1px solid var(--dsw-alias-border-l2)', background: 'var(--dsw-alias-bg-layer-2)' } },
412
+ h('span', { style: { fontWeight: 600, fontSize: 12.5 } }, '📄 ' + file.name + ' ' + (file.size ? fmtSize(file.size) : '')),
413
+ h('span', { style: { flex: 1 } }),
414
+ file.text !== null ? h('a', { className: 'dgs-sub', style: { cursor: 'pointer', marginRight: 12 }, onClick: () => {
415
+ try { navigator.clipboard.writeText(location.origin + '/r/' + repo.owner + '/' + repo.name + '/src/' + encodeURIComponent(rev) + '/' + encodeURIComponent(path)) } catch {}
416
+ setMsg('✓ 已复制永久链接'); setTimeout(() => setMsg(''), 1500)
417
+ } }, '永久链接') : null,
418
+ file.text !== null ? h('a', { className: 'dgs-sub', style: { cursor: 'pointer', marginRight: 12 }, onClick: () => setHistMode(histMode === 'history' ? null : 'history') }, '文件历史') : null,
419
+ file.text !== null ? h('a', { className: 'dgs-sub', style: { marginRight: 12 }, href: `/dsh-git-server/api/repos/${repo.owner}/${repo.name}/raw?ref=${encodeURIComponent(rev)}&path=${encodeURIComponent(path)}`, target: '_blank', rel: 'noreferrer' }, '原始文件') : null,
420
+ file.text !== null ? h('button', { className: 'dgs-btn ghost', style: { padding: '2px 8px' }, onClick: () => { setEditing(true); setEditText(file.text || '') } }, '✏️') : null,
421
+ file.text !== null ? h('button', { className: 'dgs-btn danger', style: { padding: '2px 8px' }, onClick: async () => {
422
+ if (!confirm('删除文件 ' + file.name + ' ?')) return
423
+ const d = await api('DELETE', `/dsh/repos/${repo.owner}/${repo.name}/files`, { path, branch: rev })
424
+ if (d && d.ok) { setFile(null); load(path) } else setMsg((d && d.error) || '删除失败')
425
+ } }, '🗑️') : null,
426
+ file.text !== null ? h('button', { className: 'dgs-btn ghost', style: { padding: '2px 8px' }, onClick: () => setBlameOn(!blameOn) }, 'Blame') : null),
427
+ editing ? h('div', { style: { padding: 12 } },
428
+ h('textarea', { className: 'dgs-input', style: { minHeight: '40vh', fontFamily: 'ui-monospace,monospace' }, value: editText, onChange: (e) => setEditText(e.target.value) }),
429
+ h('div', { className: 'dgs-row', style: { marginTop: 8 } },
430
+ h('input', { className: 'dgs-input', placeholder: '提交信息(可选)', value: editMsg, onChange: (e) => setEditMsg(e.target.value) }),
431
+ h('span', { style: { flex: 1 } }),
432
+ h('button', { className: 'dgs-btn ghost', onClick: () => setEditing(false) }, '取消'),
433
+ h('button', { className: 'dgs-btn', disabled: busy, onClick: async () => {
434
+ setBusy(true); setMsg('')
435
+ const d = await api('POST', `/dsh/repos/${repo.owner}/${repo.name}/files`, { path, branch: rev, content: editText, message: editMsg, overwrite: true })
436
+ setBusy(false)
437
+ if (d && d.ok) { setEditing(false); load(path) } else setMsg((d && d.error) || '保存失败')
438
+ } }, '提交修改')))
439
+ : histMode === 'history' ? h(FileHistory, { repo, rev, path })
440
+ : blameOn && file.text !== null
441
+ ? h(BlameView, { repo, rev, path, text: file.text })
442
+ : file.isMd
443
+ ? h('div', { className: 'dgs-md', style: { padding: '14px 16px' }, dangerouslySetInnerHTML: { __html: file.html || '' } })
444
+ : h('pre', { className: 'dgs-file', style: { border: 'none', borderRadius: 0 } }, file.text === null ? t('emptyFile') : file.text)))
445
+ : entries === null ? h('div', { className: 'dgs-empty' }, t('loading'))
446
+ : renderDir(),
447
+ )
448
+
449
+ function renderDir() {
450
+ const head = entries.find((e) => e.last)
451
+ const rows = entries.map((e) =>
452
+ h('tr', { key: e.path, style: { cursor: 'pointer' }, onClick: () => openEntry(e) },
453
+ h('td', { style: { width: 24 } }, h('span', { className: 'dgs-ico' }, e.type === 'dir' || e.type === 'tree' ? '📁' : '📄')),
454
+ h('td', null, h('div', null, e.name),
455
+ e.last ? h('div', { className: 'dgs-sub' }, (e.last.msg || '')) : null,
456
+ e.last ? h('div', { className: 'dgs-sub', style: { marginTop: 1 } }, timeAgo(e.last.date)) : null),
457
+ h('td', { className: 'dgs-sub', style: { textAlign: 'right', width: 70 } }, e.type === 'blob' ? fmtSize(e.size) : '')))
458
+ const headRow = head ? h('div', { className: 'dgs-row', style: { padding: '8px 0 10px', borderBottom: '1px solid var(--dsw-alias-border-l2)', marginBottom: 4 } },
459
+ h('span', { style: { width: 26, height: 26, borderRadius: 6, background: 'var(--dsw-alias-bg-layer-2)', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', fontSize: 12, fontWeight: 700, color: 'var(--dsw-alias-label-secondary)' } }, (head.last.author || '?').slice(0, 1).toUpperCase()),
460
+ h('span', { className: 'dgs-badge' }, head.last.sha),
461
+ h('span', { style: { fontWeight: 500 } }, head.last.msg),
462
+ h('span', { style: { flex: 1 } }),
463
+ h('span', { className: 'dgs-sub' }, timeAgo(head.last.date))) : null
464
+ const table = entries.length === 0
465
+ ? h('div', { className: 'dgs-empty' }, t('emptyDir'))
466
+ : h('div', null, headRow, h('table', { className: 'dgs-table' }, h('tbody', null, rows)))
467
+ const readmeEl = (path === '' && overview && overview.readmeHtml)
468
+ ? h('div', { className: 'dgs-card', style: { marginTop: 12 } },
469
+ h('div', { style: { fontWeight: 700, marginBottom: 8 } }, t('readme')),
470
+ h('div', { className: 'dgs-md', dangerouslySetInnerHTML: { __html: overview.readmeHtml } }))
471
+ : null
472
+ return h('div', null, table, readmeEl)
473
+ }
474
+ }
475
+
476
+ function IssuesArea({ repo, t, initialSub }) {
477
+ const [sub, setSub] = useState(initialSub || 'list')
478
+ const [labelJump, setLabelJump] = useState(null)
479
+ const [msJump, setMsJump] = useState(null)
480
+ const subLink = (k, label) => h('a', { key: k, style: { cursor: 'pointer', marginRight: 18, fontSize: 13,
481
+ color: sub === k ? 'var(--dsw-alias-state-business-primary)' : 'var(--dsw-alias-label-secondary)',
482
+ fontWeight: sub === k ? 600 : 400 },
483
+ onClick: () => setSub(k) }, label)
484
+ const [createSignal, setCreateSignal] = useState(0)
485
+ return h('div', null,
486
+ h('div', { className: 'dgs-row', style: { margin: '0 0 12px' } },
487
+ subLink('labels', '标签管理'),
488
+ subLink('milestones', '里程碑'),
489
+ h('span', { style: { flex: 1 } }),
490
+ sub === 'list' ? h('button', { className: 'dgs-btn', onClick: () => setCreateSignal((n) => n + 1) }, t('issueNew')) : null),
491
+ sub === 'list' && h(Issues, { repo, t, presetLabel: labelJump, presetMilestone: msJump,
492
+ onPresetDone: () => { setLabelJump(null); setMsJump(null) }, createSignal }),
493
+ sub === 'labels' && h(LabelsManage, { repo, t, onFilterLabel: (lid) => { setLabelJump(lid); setSub('list') } }),
494
+ sub === 'milestones' && h(MilestonesManage, { repo, t, onOpenMilestone: (m) => { setMsJump(m.id); setSub('list') } }))
495
+ }
496
+
497
+ function Issues({ repo, t, presetLabel, presetMilestone, onPresetDone, createSignal }) {
498
+ const [state, setState] = useState('open')
499
+ const [flt, setFlt] = useState({ label: '', milestone: '', assignee: '' })
500
+ useEffect(() => {
501
+ if (presetLabel || presetMilestone) {
502
+ setFlt((f) => ({ ...f, label: presetLabel ? String(presetLabel) : f.label, milestone: presetMilestone ? String(presetMilestone) : f.milestone }))
503
+ onPresetDone && onPresetDone()
504
+ }
505
+ }, [presetLabel, presetMilestone])
506
+ const [sort, setSort] = useState('latest')
507
+ const [search, setSearch] = useState('')
508
+ const [list, setList] = useState(null)
509
+ const [meta, setMeta] = useState({ labels: [], milestones: [], collabs: [] })
510
+ const [creating, setCreating] = useState(false)
511
+ useEffect(() => { if (createSignal) setCreating(true) }, [createSignal])
512
+ const [openIdx, setOpenIdx] = useState(null)
513
+ const reload = useCallback(() => {
514
+ setList(null)
515
+ const q = new URLSearchParams({ state, sort })
516
+ if (flt.label) q.set('label', flt.label)
517
+ if (flt.milestone) q.set('milestone', flt.milestone)
518
+ if (flt.assignee) q.set('assignee', flt.assignee)
519
+ if (search.trim()) q.set('search', search.trim())
520
+ api('GET', `/dsh/repos/${repo.owner}/${repo.name}/issues?` + q).then((d) => setList(Array.isArray(d) ? d : (d.issues || [])))
521
+ }, [repo.owner, repo.name, state, flt, sort, search])
522
+ useEffect(reload, [reload])
523
+ useEffect(() => {
524
+ api('GET', `/dsh/repos/${repo.owner}/${repo.name}/labels`).then((d) => setMeta((m) => ({ ...m, labels: Array.isArray(d) ? d : [] })))
525
+ api('GET', `/dsh/repos/${repo.owner}/${repo.name}/milestones`).then((d) => setMeta((m) => ({ ...m, milestones: Array.isArray(d) ? d : [] })))
526
+ api('GET', `/dsh/repos/${repo.owner}/${repo.name}/collaborators`).then((d) => setMeta((m) => ({ ...m, collabs: Array.isArray(d) ? d : [] })))
527
+ }, [repo.owner, repo.name])
528
+ if (openIdx !== null) return h(IssueDetail, { repo, idx: openIdx, t, onBack: () => { setOpenIdx(null); reload() } })
529
+ const fltSel = (key, items, blank) => h('select', {
530
+ className: 'dgs-input', style: { maxWidth: 150, flex: 'none', width: 'auto', padding: '5px 8px' },
531
+ value: flt[key], onChange: (e) => setFlt({ ...flt, [key]: e.target.value }),
532
+ }, h('option', { value: '' }, blank),
533
+ items.map((x) => h('option', { key: x.id || x.name, value: String(x.id || x.name) }, x.title || x.name)))
534
+ return h('div', null,
535
+ h('div', { className: 'dgs-row', style: { margin: '8px 0 12px' } },
536
+ h('button', { className: 'dgs-pill' + (state === 'open' ? ' active' : ''), onClick: () => setState('open') }, '⊘ ' + t('openState')),
537
+ h('button', { className: 'dgs-pill' + (state === 'closed' ? ' active closed' : ''), onClick: () => setState('closed') }, '✓ ' + t('closedState'))),
538
+ creating ? h(NewIssue, { repo, t, onDone: () => { setCreating(false); reload() } }) : null,
539
+ h('div', { className: 'dgs-row', style: { margin: '0 0 12px' } },
540
+ fltSel('label', meta.labels, '标签'),
541
+ fltSel('milestone', meta.milestones, '里程碑'),
542
+ fltSel('assignee', meta.collabs, '负责人'),
543
+ h('select', { className: 'dgs-input', style: { maxWidth: 150, flex: 'none', width: 'auto', padding: '5px 8px' }, value: sort, onChange: (e) => setSort(e.target.value) },
544
+ [['latest', '最新'], ['oldest', '最旧'], ['recentupdate', '最近更新'], ['leastupdate', '最久未更新'], ['mostcomment', '最多评论'], ['leastcomment', '最少评论']].map(([v, label]) =>
545
+ h('option', { key: v, value: v }, '排序: ' + label))),
546
+ h('input', { className: 'dgs-input', style: { maxWidth: 180, flex: 'none' }, placeholder: '搜索标题/内容', value: search, onChange: (e) => setSearch(e.target.value) }),
547
+ (flt.label || flt.milestone || flt.assignee || search) ? h('button', { className: 'dgs-btn ghost', onClick: () => { setFlt({ label: '', milestone: '', assignee: '' }); setSearch('') } }, '清除') : null),
548
+ list === null ? h('div', { className: 'dgs-empty' }, t('loading'))
549
+ : list.length === 0 ? h('div', { className: 'dgs-empty' }, t('noIssues'))
550
+ : h('div', null, list.map((i) => issueRow(i)))
551
+ )
552
+
553
+ function issueRow(i) {
554
+ const badge = h('span', { className: 'dgs-issue-num' + (i.state === 'open' ? '' : ' closed') }, '#' + i.number)
555
+ const title = h('span', { className: 'dgs-issue-title' }, i.title)
556
+ const labels = (i.labels || []).map((l) =>
557
+ h('span', { key: l.id, className: 'dgs-badge', style: { background: (l.color || '#70c24a') + '33', borderColor: l.color || '#70c24a' } }, l.name))
558
+ const meta = h('div', { className: 'dgs-row', style: { marginTop: 4, gap: 6 } },
559
+ h('span', { className: 'dgs-sub' }, '由 ' + (i.user || '') + ' 于 ' + timeAgo(i.updatedAt) + ' 创建'),
560
+ i.milestone ? h('span', { className: 'dgs-badge' }, '◆ ' + i.milestone.title) : null,
561
+ i.assignee ? h('span', { className: 'dgs-badge' }, '@' + i.assignee) : null)
562
+ return h('div', {
563
+ key: i.number, className: 'dgs-issue', style: { cursor: 'pointer', display: 'flex', alignItems: 'center', gap: 10 },
564
+ onClick: () => setOpenIdx(i.number),
565
+ }, badge,
566
+ h('div', { style: { flex: 1, minWidth: 0 } },
567
+ h('div', { className: 'dgs-row', style: { gap: 6 } }, title, labels),
568
+ meta),
569
+ h('span', { className: 'dgs-sub', style: { flex: 'none' } }, '💬 ' + (i.comments || 0)))
570
+ }
571
+ }
572
+
573
+ function NewIssue({ repo, t, onDone }) {
574
+ const [title, setTitle] = useState('')
575
+ const [body, setBody] = useState('')
576
+ const [busy, setBusy] = useState(false)
577
+ const [err, setErr] = useState('')
578
+ const [preview, setPreview] = useState(false)
579
+ const [previewHtml, setPreviewHtml] = useState('')
580
+ const [labels, setLabels] = useState([])
581
+ const [ms, setMs] = useState([])
582
+ const [collabs, setCollabs] = useState([])
583
+ const [sel, setSel] = useState({ labels: new Set(), milestone: '', assignee: '' })
584
+ useEffect(() => {
585
+ api('GET', `/dsh/repos/${repo.owner}/${repo.name}/labels`).then((d) => setLabels(Array.isArray(d) ? d : []))
586
+ api('GET', `/dsh/repos/${repo.owner}/${repo.name}/milestones`).then((d) => setMs(Array.isArray(d) ? d : []))
587
+ api('GET', `/dsh/repos/${repo.owner}/${repo.name}/collaborators`).then((d) => setCollabs(Array.isArray(d) ? d : []))
588
+ }, [repo.owner, repo.name])
589
+ const submit = async () => {
590
+ setBusy(true); setErr('')
591
+ const d = await api('POST', `/repos/${repo.owner}/${repo.name}/issues`, {
592
+ title, body,
593
+ labels: [...sel.labels].map(Number),
594
+ milestone: sel.milestone ? Number(sel.milestone) : undefined,
595
+ assignee: sel.assignee || undefined,
596
+ })
597
+ setBusy(false)
598
+ d.ok ? onDone() : setErr(d.error || 'failed')
599
+ }
600
+ const toggleLabel = (id) => setSel((x) => {
601
+ const next = new Set(x.labels)
602
+ next.has(id) ? next.delete(id) : next.add(id)
603
+ return { ...x, labels: next }
604
+ })
605
+ const side = h('div', null,
606
+ h('div', { className: 'dgs-side-block' },
607
+ h('div', { className: 'dgs-side-title' }, '标签 ⚙'),
608
+ labels.length === 0 ? h('div', { className: 'dgs-sub' }, '未选择标签') : null,
609
+ labels.map((l) => h('label', { key: l.id, className: 'dgs-sub', style: { display: 'flex', gap: 6, alignItems: 'center', margin: '4px 0', cursor: 'pointer' } },
610
+ h('input', { type: 'checkbox', checked: sel.labels.has(l.id), onChange: () => toggleLabel(l.id) }),
611
+ h('span', { className: 'dgs-badge', style: { background: (l.color || '#70c24a') + '33' } }, l.name)))),
612
+ h('div', { className: 'dgs-side-block' },
613
+ h('div', { className: 'dgs-side-title' }, '里程碑 ⚙'),
614
+ h('select', { className: 'dgs-input', style: { width: '100%' }, value: sel.milestone, onChange: (e) => setSel({ ...sel, milestone: e.target.value }) },
615
+ h('option', { value: '' }, '未选择里程碑'),
616
+ ms.map((m) => h('option', { key: m.id, value: m.id }, m.title + (m.closed ? ' ✓' : ''))))),
617
+ h('div', { className: 'dgs-side-block' },
618
+ h('div', { className: 'dgs-side-title' }, '指派成员 ⚙'),
619
+ h('select', { className: 'dgs-input', style: { width: '100%' }, value: sel.assignee, onChange: (e) => setSel({ ...sel, assignee: e.target.value }) },
620
+ h('option', { value: '' }, '未指派成员'),
621
+ collabs.map((u) => h('option', { key: u.name, value: u.name }, u.name)))))
622
+ return h('div', { className: 'dgs-card', style: { margin: '10px 0' } },
623
+ h('div', { className: 'dgs-issue-layout' },
624
+ h('div', { className: 'dgs-issue-main' },
625
+ h('input', { className: 'dgs-input', placeholder: '标题', value: title, onChange: (e) => setTitle(e.target.value) }),
626
+ h('div', { className: 'dgs-row', style: { margin: '8px 0 4px' } },
627
+ h('button', { className: 'dgs-subtab' + (!preview ? ' active' : ''), onClick: () => setPreview(false) }, '内容编辑'),
628
+ h('button', { className: 'dgs-subtab' + (preview ? ' active' : ''), onClick: () => {
629
+ setPreview(true)
630
+ api('POST', '/dsh/markdown', { text: body }).then((d) => setPreviewHtml(d.html || ''))
631
+ } }, '效果预览')),
632
+ preview ? h('div', { className: 'dgs-md', style: { minHeight: 160, border: '1px solid var(--dsw-alias-border-l2)', borderRadius: 8, padding: 12 } },
633
+ body ? h('div', { dangerouslySetInnerHTML: { __html: previewHtml } }) : h('span', { className: 'dgs-sub' }, '暂无内容'))
634
+ : h('textarea', { className: 'dgs-input', style: { minHeight: 160 }, placeholder: '内容', value: body, onChange: (e) => setBody(e.target.value) }),
635
+ err ? h('div', { className: 'dgs-err' }, err) : null,
636
+ h('div', { className: 'dgs-row', style: { marginTop: 10, justifyContent: 'flex-end' } },
637
+ h('button', { className: 'dgs-btn', disabled: busy || !title.trim(), onClick: submit }, '创建工单'))),
638
+ h('div', { className: 'dgs-issue-side' }, side)))
639
+ }
640
+
641
+ function IssueDetail({ repo, idx, t, onBack }) {
642
+ const [issue, setIssue] = useState(null)
643
+ const [comments, setComments] = useState(null)
644
+ const [text, setText] = useState('')
645
+ const [busy, setBusy] = useState(false)
646
+ const [editC, setEditC] = useState(null)
647
+ const [editing, setEditing] = useState(false)
648
+ const [draft, setDraft] = useState({ title: '', body: '' })
649
+ const [allLabels, setAllLabels] = useState([])
650
+ const [allMs, setAllMs] = useState([])
651
+ const [collabs, setCollabs] = useState([])
652
+ const [msg, setMsg] = useState('')
653
+ const reload = useCallback(() => {
654
+ setComments(null)
655
+ api('GET', `/dsh/repos/${repo.owner}/${repo.name}/issues/${idx}`).then((d) => setIssue(d && d.number ? d : null))
656
+ api('GET', `/repos/${repo.owner}/${repo.name}/issues/${idx}/comments`).then((d) => setComments(d.comments || []))
657
+ }, [repo.owner, repo.name, idx])
658
+ useEffect(reload, [reload])
659
+ useEffect(() => {
660
+ api('GET', `/dsh/repos/${repo.owner}/${repo.name}/labels`).then((d) => setAllLabels(Array.isArray(d) ? d : []))
661
+ api('GET', `/dsh/repos/${repo.owner}/${repo.name}/milestones`).then((d) => setAllMs(Array.isArray(d) ? d : []))
662
+ api('GET', `/dsh/repos/${repo.owner}/${repo.name}/collaborators`).then((d) => setCollabs(Array.isArray(d) ? d : []))
663
+ }, [repo.owner, repo.name])
664
+ if (!issue) return h('div', { className: 'dgs-empty' }, t('loading'))
665
+ const patch = async (fields) => {
666
+ setBusy(true); setMsg('')
667
+ const d = await api('PATCH', `/dsh/repos/${repo.owner}/${repo.name}/issues/${idx}`, fields)
668
+ setBusy(false)
669
+ if (d && d.ok) { setEditing(false); reload() } else setMsg((d && d.error) || 'failed')
670
+ }
671
+ const main = h('div', null,
672
+ editing ? h('div', { className: 'dgs-card', style: { margin: '10px 0' } },
673
+ h('input', { className: 'dgs-input', value: draft.title, onChange: (e) => setDraft({ ...draft, title: e.target.value }) }),
674
+ h('textarea', { className: 'dgs-input', style: { marginTop: 8 }, value: draft.body, onChange: (e) => setDraft({ ...draft, body: e.target.value }) }),
675
+ h('div', { className: 'dgs-row', style: { marginTop: 8 } },
676
+ h('button', { className: 'dgs-btn', disabled: busy || !draft.title.trim(), onClick: () => patch({ title: draft.title, body: draft.body }) }, t('save'))))
677
+ : h('div', { className: 'dgs-card', style: { margin: '10px 0' } },
678
+ issue.body ? h('div', { style: { whiteSpace: 'pre-wrap' } }, issue.body) : h('div', { className: 'dgs-sub' }, '暂无内容'),
679
+ h('div', { className: 'dgs-row', style: { marginTop: 10, flexWrap: 'wrap' } },
680
+ (issue.labels || []).map((l) => h('span', { key: l.id, className: 'dgs-badge', style: { background: (l.color || '#70c24a') + '33' } }, l.name)),
681
+ issue.milestone ? h('span', { className: 'dgs-badge' }, '◆ ' + issue.milestone.title) : null,
682
+ issue.assignee ? h('span', { className: 'dgs-badge' }, '@' + issue.assignee) : null)),
683
+ comments === null ? h('div', { className: 'dgs-empty' }, t('loading'))
684
+ : comments.length === 0 ? h('div', { className: 'dgs-empty' }, t('noIssues'))
685
+ : comments.map((c) =>
686
+ h('div', { key: c.id, className: 'dgs-comment' },
687
+ h('div', { className: 'dgs-row', style: { marginBottom: 4 } },
688
+ h('span', { className: 'dgs-sub' }, `${c.user || ''} · ${fmtDate(c.created)}`),
689
+ h('span', { style: { flex: 1 } }),
690
+ c.id ? h('button', { className: 'dgs-btn ghost', style: { padding: '1px 8px' }, onClick: () => { setEditC({ id: c.id, text: c.body }) } }, '编辑') : null,
691
+ c.id ? h('button', { className: 'dgs-btn danger', style: { padding: '1px 8px' }, onClick: async () => {
692
+ if (!confirm('删除此评论?')) return
693
+ await api('DELETE', `/repos/${repo.owner}/${repo.name}/issues/comments/${c.id}`)
694
+ reload()
695
+ } }, '删') : null),
696
+ editC && editC.id === c.id ? h('div', null,
697
+ h('textarea', { className: 'dgs-input', value: editC.text, onChange: (e) => setEditC({ ...editC, text: e.target.value }) }),
698
+ h('div', { className: 'dgs-row', style: { marginTop: 4 } },
699
+ h('button', { className: 'dgs-btn', onClick: async () => {
700
+ await api('PATCH', `/repos/${repo.owner}/${repo.name}/issues/comments/${c.id}`, { body: editC.text })
701
+ setEditC(null); reload()
702
+ } }, t('save'))))
703
+ : h('div', { style: { whiteSpace: 'pre-wrap' } }, c.body))),
704
+ h('div', { className: 'dgs-card', style: { margin: '12px 0' } },
705
+ h('textarea', { className: 'dgs-input', placeholder: t('commentPlaceholder'), value: text, onChange: (e) => setText(e.target.value) }),
706
+ h('div', { className: 'dgs-row', style: { marginTop: 8 } },
707
+ h('button', { className: 'dgs-btn', disabled: busy || !text.trim(),
708
+ onClick: async () => {
709
+ setBusy(true)
710
+ await api('POST', `/repos/${repo.owner}/${repo.name}/issues/${idx}/comments`, { body: text })
711
+ setText(''); setBusy(false); reload()
712
+ } }, t('comment')))))
713
+ const side = h('div', null,
714
+ h('div', { className: 'dgs-side-block' },
715
+ h('div', { className: 'dgs-side-title' }, '标签 ⚙'),
716
+ allLabels.length === 0 ? h('div', { className: 'dgs-sub' }, '未选择标签') : null,
717
+ allLabels.map((l) => h('label', { key: l.id, className: 'dgs-sub', style: { display: 'flex', gap: 6, alignItems: 'center', margin: '4px 0', cursor: 'pointer' } },
718
+ h('input', { type: 'checkbox', checked: (issue.labels || []).some((x) => x.id === l.id),
719
+ onChange: (e) => {
720
+ const cur = new Set((issue.labels || []).map((x) => x.id))
721
+ if (e.target.checked) cur.add(l.id); else cur.delete(l.id)
722
+ patch({ labels: [...cur] })
723
+ } }),
724
+ h('span', { className: 'dgs-badge', style: { background: (l.color || '#70c24a') + '33' } }, l.name)))),
725
+ h('div', { className: 'dgs-side-block' },
726
+ h('div', { className: 'dgs-side-title' }, '里程碑 ⚙'),
727
+ h('select', { className: 'dgs-input', style: { width: '100%' }, value: issue.milestone ? issue.milestone.id : '',
728
+ onChange: (e) => patch({ milestone: Number(e.target.value) || 0 }) },
729
+ h('option', { value: '' }, '未选择里程碑'),
730
+ allMs.map((m) => h('option', { key: m.id, value: m.id }, m.title + (m.closed ? ' ✓' : ''))))),
731
+ h('div', { className: 'dgs-side-block' },
732
+ h('div', { className: 'dgs-side-title' }, '指派成员 ⚙'),
733
+ h('select', { className: 'dgs-input', style: { width: '100%' }, value: issue.assignee || '',
734
+ onChange: (e) => patch({ assignee: e.target.value }) },
735
+ h('option', { value: '' }, '未指派成员'),
736
+ collabs.map((u) => h('option', { key: u.name, value: u.name }, u.name)))),
737
+ h('div', { className: 'dgs-side-block' },
738
+ h('div', { className: 'dgs-side-title' }, (issue.participants || 1) + ' 名参与者'),
739
+ h('div', { className: 'dgs-sub' }, issue.author || '')))
740
+ return h('div', null,
741
+ h('div', { className: 'dgs-row', style: { marginBottom: 10 } },
742
+ h('button', { className: 'dgs-btn ghost', onClick: onBack }, t('back')),
743
+ h('span', { style: { fontWeight: 600 } }, '#' + idx + ' ' + issue.title),
744
+ h('span', { className: 'dgs-badge' + (issue.state === 'open' ? ' ok' : ' closed') }, issue.state === 'open' ? t('openState') : t('closedState')),
745
+ h('span', { style: { flex: 1 } }),
746
+ h('button', { className: 'dgs-btn ghost', onClick: () => { setDraft({ title: issue.title, body: issue.body }); setEditing(!editing) } }, t('edit')),
747
+ h('button', { className: 'dgs-btn danger', disabled: busy,
748
+ onClick: async () => {
749
+ setBusy(true)
750
+ await api('PATCH', `/repos/${repo.owner}/${repo.name}/issues/${idx}`, { state: issue.state === 'open' ? 'closed' : 'open' })
751
+ setBusy(false); reload()
752
+ } }, issue.state === 'open' ? t('close') : t('reopen'))),
753
+ msg ? h('div', { className: 'dgs-err' }, msg) : null,
754
+ h('div', { className: 'dgs-issue-layout' },
755
+ h('div', { className: 'dgs-issue-main' }, main),
756
+ h('div', { className: 'dgs-issue-side' }, side)))
757
+ }
758
+
759
+ function Commits({ repo, rev, t }) {
760
+ const [list, setList] = useState(null)
761
+ const [sel, setSel] = useState(null)
762
+ const [page, setPage] = useState(1)
763
+ const [full, setFull] = useState(false)
764
+ const [q, setQ] = useState('')
765
+ useEffect(() => { setPage(1); setQ('') }, [rev])
766
+ useEffect(() => {
767
+ setList(null)
768
+ api('GET', `/repos/${repo.owner}/${repo.name}/commits?ref=${encodeURIComponent(rev)}&page=${page}&pageSize=30`)
769
+ .then((d) => {
770
+ const l = d.commits || []
771
+ setList(l)
772
+ setFull(l.length < 30)
773
+ })
774
+ }, [repo.owner, repo.name, rev, page])
775
+ if (sel) return h(CommitDetail, { repo, sha: sel, t, onBack: () => setSel(null) })
776
+ if (list === null) return h('div', { className: 'dgs-empty' }, t('loading'))
777
+ if (list.length === 0) return h('div', { className: 'dgs-empty' }, t('emptyDir'))
778
+ const shown = q.trim() ? list.filter((c) => (c.message || '').toLowerCase().includes(q.trim().toLowerCase()) || (c.author || '').toLowerCase().includes(q.trim().toLowerCase())) : list
779
+ return h('div', null,
780
+ h('div', { className: 'dgs-row', style: { margin: '0 0 10px' } },
781
+ h('input', { className: 'dgs-input', style: { maxWidth: 260, flex: 'none' }, placeholder: '搜索提交历史', value: q, onChange: (e) => setQ(e.target.value) })),
782
+ shown.length === 0 ? h('div', { className: 'dgs-empty' }, '—') : null,
783
+ h('table', { className: 'dgs-table' },
784
+ h('tbody', null, shown.map((c, i) =>
785
+ h('tr', { key: i, style: { cursor: 'pointer' }, onClick: () => setSel(c.sha) },
786
+ h('td', null, h('div', { style: { fontWeight: 500 } }, (c.message || '').split('\n')[0]),
787
+ h('div', { className: 'dgs-sub' }, c.author)),
788
+ h('td', { className: 'dgs-sub', style: { textAlign: 'right', whiteSpace: 'nowrap' } }, (c.sha || '').slice(0, 10)),
789
+ h('td', { className: 'dgs-sub', style: { textAlign: 'right', whiteSpace: 'nowrap', width: 100 } }, timeAgo(c.date)))))),
790
+ h('div', { className: 'dgs-row', style: { marginTop: 10, justifyContent: 'center' } },
791
+ h('button', { className: 'dgs-btn ghost', disabled: page <= 1, onClick: () => setPage(page - 1) }, '‹ 较新'),
792
+ h('span', { className: 'dgs-sub' }, '第 ' + page + ' 页'),
793
+ h('button', { className: 'dgs-btn ghost', disabled: full, onClick: () => setPage(page + 1) }, '较旧 ›')))
794
+ }
795
+
796
+ function CommitDetail({ repo, sha, t, onBack }) {
797
+ const [d, setD] = useState(null)
798
+ useEffect(() => {
799
+ api('GET', `/dsh/repos/${repo.owner}/${repo.name}/commits/${sha}`).then((x) => x && x.sha ? setD(x) : setD(x || {}))
800
+ }, [repo.owner, repo.name, sha])
801
+ if (!d || !d.sha) return h('div', { className: 'dgs-empty' }, t('loading'))
802
+ const when = d.committer && d.committer.when
803
+ return h('div', null,
804
+ h('div', { className: 'dgs-row', style: { marginBottom: 10 } },
805
+ h('button', { className: 'dgs-btn ghost', onClick: onBack }, t('back')),
806
+ h('span', { style: { fontWeight: 600 } }, (d.message || '').split('\n')[0]),
807
+ h('span', { className: 'dgs-sub' }, (d.sha || '').slice(0, 10))),
808
+ h('div', { className: 'dgs-sub', style: { marginBottom: 8 } },
809
+ `${(d.author && d.author.name) || ''} · ${fmtDate(when)}`),
810
+ h(DiffView, { patch: d.files || '' }))
811
+ }
812
+
813
+ // unified diff → 结构化渲染(文件分块 / 行号 / +绿 -红 / hunk 灰 / 每文件增删统计)
814
+ function DiffView({ patch }) {
815
+ const files = []
816
+ let cur = null
817
+ let oldNo = 0; let newNo = 0
818
+ for (const line of patch.split('\n')) {
819
+ if (line.startsWith('diff --git ')) {
820
+ cur = { name: line.replace(/^diff --git a\/(\S+) b\/.*/, '$1'), header: [], stats: { add: 0, del: 0 }, lines: [] }
821
+ files.push(cur); oldNo = 0; newNo = 0
822
+ continue
823
+ }
824
+ if (!cur) continue
825
+ if (line.startsWith('@@')) {
826
+ const m = /@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@/.exec(line)
827
+ if (m) { oldNo = Number(m[1]); newNo = Number(m[2]) }
828
+ cur.lines.push({ kind: 'hunk', text: line }); continue
829
+ }
830
+ if (line.startsWith('index ') || line.startsWith('--- ') || line.startsWith('+++ ') ||
831
+ line.startsWith('new file mode') || line.startsWith('deleted file mode') ||
832
+ line.startsWith('old mode') || line.startsWith('new mode') || line.startsWith('similarity ') ||
833
+ line.startsWith('rename from') || line.startsWith('rename to')) { cur.header.push(line); continue }
834
+ if (line.startsWith('+')) { cur.stats.add++; cur.lines.push({ kind: 'add', old: '', new: newNo++, text: line.slice(1) }); continue }
835
+ if (line.startsWith('-')) { cur.stats.del++; cur.lines.push({ kind: 'del', old: oldNo++, new: '', text: line.slice(1) }); continue }
836
+ if (line.startsWith(' ')) { cur.lines.push({ kind: 'ctx', old: oldNo++, new: newNo++, text: line.slice(1) }); continue }
837
+ if (line === '') { cur.lines.push({ kind: 'ctx', old: oldNo++, new: newNo++, text: '' }); continue }
838
+ cur.lines.push({ kind: 'ctx', text: line })
839
+ }
840
+ if (files.length === 0) return h('div', { className: 'dgs-empty' }, '—')
841
+ return h('div', null, files.map((f, i) =>
842
+ h('div', { key: i, className: 'dgs-card', style: { padding: 0, overflow: 'hidden', marginBottom: 12 } },
843
+ h('div', { className: 'dgs-row', style: { padding: '8px 12px', borderBottom: '1px solid var(--dsw-alias-border-l2)', background: 'var(--dsw-alias-bg-layer-2)' } },
844
+ h('span', { style: { fontWeight: 600, fontSize: 12.5 } }, f.name),
845
+ h('span', { style: { flex: 1 } }),
846
+ h('span', { className: 'dgs-diff-stat' }, '+' + f.stats.add),
847
+ h('span', { className: 'dgs-diff-stat del' }, '-' + f.stats.del)),
848
+ h('div', { style: { overflowX: 'auto' } }, f.lines.map((l, j) => {
849
+ if (l.kind === 'hunk') return h('div', { key: j, className: 'dgs-diff-line hunk' }, l.text)
850
+ return h('div', { key: j, className: 'dgs-diff-line ' + l.kind },
851
+ h('span', { className: 'dgs-diff-no' }, l.old || ''),
852
+ h('span', { className: 'dgs-diff-no' }, l.new || ''),
853
+ h('span', { className: 'dgs-diff-code', style: { whiteSpace: 'pre' } }, l.text))
854
+ })))))
855
+ }
856
+ function Branches({ repo, t, onNewPR }) {
857
+ const [view, setView] = useState('overview')
858
+ const [list, setList] = useState(null)
859
+ const [tags, setTags] = useState(null)
860
+ const [rels, setRels] = useState([])
861
+ const [def, setDef] = useState('')
862
+ const reload = () => api('GET', `/repos/${repo.owner}/${repo.name}/branches`).then((d) => setList(d.branches || []))
863
+ useEffect(() => {
864
+ reload()
865
+ api('GET', `/dsh/repos/${repo.owner}/${repo.name}/overview`).then((d) => setDef(d.defaultBranch || ''))
866
+ api('GET', `/dsh/repos/${repo.owner}/${repo.name}/tags`).then((d) => setTags(Array.isArray(d) ? d : []))
867
+ api('GET', `/dsh/repos/${repo.owner}/${repo.name}/releases`).then((d) => setRels(Array.isArray(d) ? d : (d.data || [])))
868
+ }, [repo.owner, repo.name])
869
+ if (list === null) return h('div', { className: 'dgs-empty' }, t('loading'))
870
+ const tabBtn = (k, label) => h('button', { className: 'dgs-subtab' + (view === k ? ' active' : ''), onClick: () => setView(k) }, label)
871
+ const active = list.filter((b) => b.name !== def)
872
+ return h('div', null,
873
+ h('div', { className: 'dgs-subtabs' }, tabBtn('overview', '概况'), tabBtn('all', '所有分支')),
874
+ view === 'overview' ? h('div', null,
875
+ h('div', { className: 'dgs-card', style: { marginBottom: 12 } },
876
+ h('div', { style: { fontWeight: 700, marginBottom: 8 } }, '默认分支'),
877
+ h('div', { className: 'dgs-row' },
878
+ h('span', { className: 'dgs-badge' }, def || 'master'),
879
+ h('span', { className: 'dgs-sub', style: { flex: 1 } }, ''),
880
+ h('button', { className: 'dgs-btn ghost', onClick: () => setView('all') }, '更改默认分支'))),
881
+ h('div', { className: 'dgs-card' },
882
+ h('div', { style: { fontWeight: 700, marginBottom: 8 } }, '活跃分支'),
883
+ active.length === 0 ? h('div', { className: 'dgs-sub' }, '—')
884
+ : active.map((b) => h('div', { key: b.name, className: 'dgs-row', style: { padding: '4px 0' } },
885
+ h('a', { className: 'dgs-name', style: { cursor: 'pointer' }, onClick: () => onNewPR && onNewPR(b.name, def) }, b.name),
886
+ h('span', { className: 'dgs-sub', style: { flex: 1 } }, ''),
887
+ h('span', { className: 'dgs-sub' }, (b.sha || '').slice(0, 10))))))
888
+ : h('div', null,
889
+ h('table', { className: 'dgs-table' },
890
+ h('tbody', null, list.map((b) =>
891
+ h('tr', { key: b.name },
892
+ h('td', { style: { fontWeight: 500 } },
893
+ b.name,
894
+ b.name === def ? h('span', { className: 'dgs-badge ok', style: { marginLeft: 8 } }, '默认') : null),
895
+ h('td', { className: 'dgs-sub', style: { textAlign: 'right' } },
896
+ (b.sha || '').slice(0, 10),
897
+ b.name !== def ? h('button', { className: 'dgs-btn ghost', style: { marginLeft: 8, padding: '2px 8px' }, onClick: () => onNewPR && onNewPR(b.name, def) }, '+ PR') : null,
898
+ b.name !== def ? h('button', { className: 'dgs-btn danger', style: { marginLeft: 4, padding: '2px 8px' }, onClick: async () => {
899
+ if (!confirm('删除分支 ' + b.name + ' ?')) return
900
+ await api('DELETE', `/repos/${repo.owner}/${repo.name}/branches/${encodeURIComponent(b.name)}`)
901
+ reload()
902
+ } }, '删') : null))))),
903
+ h('div', { style: { fontWeight: 700, margin: '18px 0 8px' } }, '标签'),
904
+ tags === null ? h('div', { className: 'dgs-empty' }, t('loading'))
905
+ : tags.length === 0 ? h('div', { className: 'dgs-empty' }, '—')
906
+ : h('table', { className: 'dgs-table' },
907
+ h('tbody', null, tags.map((tg) => {
908
+ const released = rels.some((r) => r.tag === tg.name)
909
+ return h('tr', { key: tg.name },
910
+ h('td', { style: { fontWeight: 500 } },
911
+ '◎ ' + tg.name,
912
+ released ? h('span', { className: 'dgs-badge ok', style: { marginLeft: 8 } }, '已发版') : null),
913
+ h('td', { className: 'dgs-sub' }, tg.msg || ''),
914
+ h('td', { className: 'dgs-sub', style: { textAlign: 'right', whiteSpace: 'nowrap' } },
915
+ (tg.sha || '') + ' · ' + timeAgo(tg.date)))
916
+ }))))
917
+ )
918
+ }
919
+
920
+ // ── 标签 ───────────────────────────────────────────────────────────────────
921
+
922
+ function Tags({ repo, ov, t }) {
923
+ if (!ov) return h('div', { className: 'dgs-empty' }, t('loading'))
924
+ return ov.tags.length === 0 ? h('div', { className: 'dgs-empty' }, '—')
925
+ : h('table', { className: 'dgs-table' },
926
+ h('tbody', null, ov.tags.map((name) =>
927
+ h('tr', { key: name },
928
+ h('td', { style: { fontWeight: 500 } }, name),
929
+ h('td', { className: 'dgs-sub', style: { textAlign: 'right' } }, t('tags'))))))
930
+ }
931
+
932
+ // ── Blame ──────────────────────────────────────────────────────────────────
933
+
934
+ function BlameView({ repo, rev, path, text }) {
935
+ const [lines, setLines] = useState(null)
936
+ useEffect(() => {
937
+ api('GET', `/dsh/repos/${repo.owner}/${repo.name}/blame?ref=${encodeURIComponent(rev)}&path=${encodeURIComponent(path)}`)
938
+ .then((d) => setLines(d.lines || []))
939
+ }, [repo.owner, repo.name, rev, path])
940
+ if (lines === null) return h('div', { className: 'dgs-empty' }, '…')
941
+ const codeLines = (text || '').split('\n')
942
+ return h('table', { className: 'dgs-table' },
943
+ h('tbody', null, codeLines.map((code, i) => {
944
+ const bl = lines.find((l) => l.line === i + 1) || {}
945
+ return h('tr', { key: i },
946
+ h('td', { className: 'dgs-sub', style: { textAlign: 'right', width: 34 } }, String(i + 1)),
947
+ h('td', { className: 'dgs-sub', style: { width: 170, whiteSpace: 'nowrap' } },
948
+ bl.author ? `${bl.author} · ${(bl.sha || '').slice(0, 7)}` : ''),
949
+ h('td', null, h('span', { style: { whiteSpace: 'pre-wrap', fontSize: 12.5 } }, code)))
950
+ })))
951
+ }
952
+
953
+ // ── 文件历史 ───────────────────────────────────────────────────────────────
954
+
955
+ function FileHistory({ repo, rev, path }) {
956
+ const [list, setList] = useState(null)
957
+ useEffect(() => {
958
+ api('GET', `/dsh/repos/${repo.owner}/${repo.name}/file-history?ref=${encodeURIComponent(rev)}&path=${encodeURIComponent(path)}`)
959
+ .then((d) => setList(d.commits || []))
960
+ }, [repo.owner, repo.name, rev, path])
961
+ if (list === null) return h('div', { className: 'dgs-empty' }, '…')
962
+ return h('table', { className: 'dgs-table' },
963
+ h('tbody', null, list.length === 0
964
+ ? h('tr', null, h('td', { className: 'dgs-sub' }, '—'))
965
+ : list.map((cm) =>
966
+ h('tr', { key: cm.sha },
967
+ h('td', { className: 'dgs-sub', style: { width: 70 } }, cm.sha),
968
+ h('td', null, cm.msg || ''),
969
+ h('td', { className: 'dgs-sub', style: { textAlign: 'right', whiteSpace: 'nowrap' } },
970
+ (cm.author || '') + ' · ' + timeAgo(cm.date))))))
971
+ }
972
+
973
+ // ── 标签管理 ───────────────────────────────────────────────────────────────
974
+
975
+ function LabelsManage({ repo, t, onFilterLabel }) {
976
+ const [list, setList] = useState(null)
977
+ const [creating, setCreating] = useState(false)
978
+ const [editId, setEditId] = useState(null)
979
+ const [form, setForm] = useState({ name: '', color: '#70c24a' })
980
+ const [msg, setMsg] = useState('')
981
+ const reload = () => api('GET', `/dsh/repos/${repo.owner}/${repo.name}/labels`).then((d) => setList(Array.isArray(d) ? d : []))
982
+ useEffect(() => { reload() }, [repo.owner, repo.name])
983
+ const save = async () => {
984
+ const d = editId
985
+ ? await api('PATCH', `/dsh/repos/${repo.owner}/${repo.name}/labels/${editId}`, form)
986
+ : await api('POST', `/dsh/repos/${repo.owner}/${repo.name}/labels`, form)
987
+ if (d && d.ok !== false && !d.error) {
988
+ setForm({ name: '', color: '#70c24a' }); setCreating(false); setEditId(null); reload()
989
+ } else setMsg((d && d.error) || 'failed')
990
+ }
991
+ const startCreate = () => { setCreating(!creating); setEditId(null); setForm({ name: '', color: '#70c24a' }) }
992
+ const startEdit = (l) => { setEditId(l.id); setCreating(false); setForm({ name: l.name, color: l.color || '#70c24a' }) }
993
+ const header = h('div', { className: 'dgs-row', style: { margin: '8px 0 12px' } },
994
+ h('span', { style: { fontWeight: 700 } }, list ? list.length + ' 个标签' : ''),
995
+ h('span', { style: { flex: 1 } }),
996
+ h('button', { className: 'dgs-btn', onClick: startCreate }, '创建标签'))
997
+ const formCard = (creating || editId !== null)
998
+ ? h('div', { className: 'dgs-card', style: { marginBottom: 12 } },
999
+ h('div', { className: 'dgs-row' },
1000
+ h('input', { className: 'dgs-input', style: { maxWidth: 160 }, placeholder: '标签名称', value: form.name, onChange: (e) => setForm({ ...form, name: e.target.value }) }),
1001
+ h('input', { type: 'color', value: form.color, onChange: (e) => setForm({ ...form, color: e.target.value }), style: { width: 36, height: 30, padding: 0, border: 'none', background: 'none' } }),
1002
+ h('span', { style: { flex: 1 } }),
1003
+ h('button', { className: 'dgs-btn', disabled: !form.name.trim(), onClick: save }, editId !== null ? '保存' : '创建标签')))
1004
+ : null
1005
+ const rows = (list || []).map((l) =>
1006
+ h('div', { key: l.id, className: 'dgs-issue' },
1007
+ h('div', { className: 'dgs-row' },
1008
+ h('span', { className: 'dgs-badge', style: { background: (l.color || '#70c24a') + '33', borderColor: l.color || '#70c24a' } }, l.name),
1009
+ h('span', { style: { flex: 1 } }),
1010
+ h('a', { className: 'dgs-sub', style: { cursor: 'pointer' }, onClick: () => onFilterLabel && onFilterLabel(l.id) }, (l.openIssues || 0) + ' 个开启的工单'),
1011
+ h('button', { className: 'dgs-btn ghost', onClick: () => startEdit(l) }, '编辑'),
1012
+ h('button', { className: 'dgs-btn danger', onClick: async () => {
1013
+ await api('DELETE', `/dsh/repos/${repo.owner}/${repo.name}/labels/${l.id}`); reload()
1014
+ } }, t('delete')))))
1015
+ return h('div', null,
1016
+ header,
1017
+ msg ? h('div', { className: 'dgs-err' }, msg) : null,
1018
+ formCard,
1019
+ list === null ? h('div', { className: 'dgs-empty' }, t('loading'))
1020
+ : list.length === 0 ? h('div', { className: 'dgs-card', style: { maxWidth: 520, margin: '30px auto', textAlign: 'center' } },
1021
+ h('div', { style: { fontWeight: 700, marginBottom: 8 } }, '加载预定义的标签模板'),
1022
+ h('div', { className: 'dgs-sub', style: { marginBottom: 14 } }, '此仓库还未创建任何标签。可以通过上方的「创建标签」创建一个新的标签或加载一组预定义的标签。'),
1023
+ h('div', { className: 'dgs-row', style: { justifyContent: 'center' } },
1024
+ h('select', { className: 'dgs-input', style: { maxWidth: 240, flex: 'none' } },
1025
+ h('option', { value: 'default' }, 'Default'),
1026
+ h('option', { value: 'enterprise' }, 'Enterprise')),
1027
+ h('button', { className: 'dgs-btn', onClick: async (e) => {
1028
+ const existing = new Set((list || []).map((l) => l.name.toLowerCase()))
1029
+ const defaults = [
1030
+ { name: 'bug', color: '#e25444' }, { name: 'duplicate', color: '#b7b7b7' },
1031
+ { name: 'enhancement', color: '#70c24a' }, { name: 'help wanted', color: '#c8c8ff' },
1032
+ { name: 'invalid', color: '#fef2c0' }, { name: 'question', color: '#d876e3' },
1033
+ { name: 'wontfix', color: '#ffffff' },
1034
+ ]
1035
+ for (const tpl of defaults) {
1036
+ if (existing.has(tpl.name.toLowerCase())) continue
1037
+ await api('POST', `/dsh/repos/${repo.owner}/${repo.name}/labels`, tpl)
1038
+ }
1039
+ reload()
1040
+ } }, '加载标签模板')))
1041
+ : rows)
1042
+ }
1043
+
1044
+ // ── 里程碑管理 ─────────────────────────────────────────────────────────────
1045
+
1046
+ function MilestonesManage({ repo, t, onOpenMilestone }) {
1047
+ const [list, setList] = useState(null)
1048
+ const [showClosed, setShowClosed] = useState(false)
1049
+ const [creating, setCreating] = useState(false)
1050
+ const [editId, setEditId] = useState(null)
1051
+ const [form, setForm] = useState({ title: '', due: '', description: '' })
1052
+ const [busy, setBusy] = useState(false)
1053
+ const [msg, setMsg] = useState('')
1054
+ const [openMs, setOpenMs] = useState(null)
1055
+ const [msIssues, setMsIssues] = useState(null)
1056
+ const reload = () => api('GET', `/dsh/repos/${repo.owner}/${repo.name}/milestones`).then((d) => setList(Array.isArray(d) ? d : []))
1057
+ useEffect(() => { reload() }, [repo.owner, repo.name])
1058
+ const toggleMs = (m) => {
1059
+ if (onOpenMilestone) onOpenMilestone(m)
1060
+ else if (openMs === m.id) { setOpenMs(null); setMsIssues(null) }
1061
+ else {
1062
+ setOpenMs(m.id); setMsIssues(null)
1063
+ api('GET', `/dsh/repos/${repo.owner}/${repo.name}/issues?milestone=${m.id}&state=all`).then((d) => setMsIssues(Array.isArray(d) ? d : []))
1064
+ }
1065
+ }
1066
+ const startCreate = () => {
1067
+ setCreating(!creating); setEditId(null)
1068
+ setForm({ title: '', due: '', description: '' })
1069
+ }
1070
+ const startEdit = (m) => {
1071
+ setEditId(m.id); setCreating(false)
1072
+ setForm({ title: m.title, due: m.due ? new Date(m.due * 1000).toISOString().slice(0, 10) : '', description: m.description || '' })
1073
+ }
1074
+ const saveForm = async () => {
1075
+ setBusy(true); setMsg('')
1076
+ const body = { title: form.title.trim(), description: form.description }
1077
+ if (form.due) body.due = form.due
1078
+ const d = editId
1079
+ ? await api('PATCH', `/dsh/repos/${repo.owner}/${repo.name}/milestones/${editId}`, body)
1080
+ : await api('POST', `/dsh/repos/${repo.owner}/${repo.name}/milestones`, body)
1081
+ setBusy(false)
1082
+ if (d && d.ok !== false && !d.error) {
1083
+ setCreating(false); setEditId(null); setForm({ title: '', due: '', description: '' }); reload()
1084
+ } else setMsg((d && d.error) || 'failed')
1085
+ }
1086
+ const shown = (list || []).filter((m) => showClosed ? m.closed : !m.closed)
1087
+ const openCount = (list || []).filter((m) => !m.closed).length
1088
+ const closedCount = (list || []).filter((m) => m.closed).length
1089
+ return h('div', null,
1090
+ h('div', { className: 'dgs-row', style: { margin: '8px 0 12px' } },
1091
+ h('button', { className: 'dgs-btn ghost', onClick: () => setShowClosed(false) }, '开放里程碑 (' + openCount + ')'),
1092
+ h('button', { className: 'dgs-btn ghost', onClick: () => setShowClosed(true) }, '已关闭里程碑 (' + closedCount + ')'),
1093
+ h('span', { style: { flex: 1 } }),
1094
+ h('button', { className: 'dgs-btn', onClick: startCreate }, '+ 创建里程碑')),
1095
+ msg ? h('div', { className: 'dgs-err' }, msg) : null,
1096
+ (creating || editId !== null) ? h('div', { className: 'dgs-card', style: { margin: '10px 0' } },
1097
+ h('div', { className: 'dgs-row' },
1098
+ h('span', { style: { fontWeight: 700 } }, editId !== null ? '编辑里程碑' : '创建里程碑'),
1099
+ h('span', { style: { flex: 1 } }),
1100
+ h('button', { className: 'dgs-btn ghost', onClick: () => { setCreating(false); setEditId(null) } }, t('back'))),
1101
+ h('div', { style: { margin: '8px 0 0' } },
1102
+ h('input', { className: 'dgs-input', placeholder: '里程碑标题', value: form.title, onChange: (e) => setForm({ ...form, title: e.target.value }) }),
1103
+ h('div', { className: 'dgs-row', style: { marginTop: 8 } },
1104
+ h('span', { className: 'dgs-sub', style: { minWidth: 60 } }, '截止日期'),
1105
+ h('input', { type: 'date', className: 'dgs-input', style: { maxWidth: 180, flex: 'none' }, value: form.due, onChange: (e) => setForm({ ...form, due: e.target.value }) }),
1106
+ form.due ? h('a', { className: 'dgs-sub', style: { cursor: 'pointer' }, onClick: () => setForm({ ...form, due: '' }) }, '清除') : null),
1107
+ h('textarea', { className: 'dgs-input', style: { marginTop: 8, minHeight: 60 }, placeholder: '描述', value: form.description, onChange: (e) => setForm({ ...form, description: e.target.value }) }),
1108
+ h('div', { className: 'dgs-row', style: { marginTop: 8 } },
1109
+ h('button', { className: 'dgs-btn', disabled: busy || !form.title.trim(), onClick: saveForm }, editId !== null ? t('save') : t('submit')))))
1110
+ : null,
1111
+ list === null ? h('div', { className: 'dgs-empty' }, t('loading'))
1112
+ : shown.length === 0 ? h('div', { className: 'dgs-empty' }, '—')
1113
+ : shown.map((m) => {
1114
+ const total = (m.open || 0) + (m.closedIssues || 0)
1115
+ const pct = total ? Math.round((m.closedIssues || 0) * 100 / total) : 0
1116
+ const overdue = m.due && !m.closed && m.due * 1000 < Date.now()
1117
+ return h('div', { key: m.id, className: 'dgs-issue' },
1118
+ h('div', { className: 'dgs-row', style: { alignItems: 'flex-start' } },
1119
+ h('div', { style: { flex: 2, minWidth: 0 } },
1120
+ h('span', { style: { fontWeight: 600, cursor: 'pointer' }, onClick: () => toggleMs(m) }, '◆ ' + m.title),
1121
+ m.description ? h('div', { className: 'dgs-sub', style: { marginTop: 2 } }, m.description) : null,
1122
+ m.due ? h('div', { className: 'dgs-sub', style: { marginTop: 2, color: overdue ? 'var(--dsw-alias-state-error-primary)' : undefined } },
1123
+ '截止 ' + fmtDate(m.due * 1000) + (overdue ? ' · 已逾期' : '')) : null),
1124
+ h('div', { style: { flex: 1, minWidth: 170 } },
1125
+ h('div', { style: { height: 8, background: 'var(--dsw-alias-bg-layer-2)', borderRadius: 8, overflow: 'hidden', marginBottom: 4 } },
1126
+ h('div', { style: { width: pct + '%', height: '100%', background: 'var(--dsw-alias-state-business-primary)' } })),
1127
+ h('div', { className: 'dgs-sub' }, pct + '% 完成 · ' + (m.closedIssues || 0) + ' 已关闭 · ' + (m.open || 0) + ' 开放')),
1128
+ h('div', { className: 'dgs-row', style: { flex: 'none' } },
1129
+ h('button', { className: 'dgs-btn ghost', onClick: () => startEdit(m) }, t('edit')),
1130
+ h('button', { className: 'dgs-btn ghost', onClick: async () => {
1131
+ await api('PATCH', `/dsh/repos/${repo.owner}/${repo.name}/milestones/${m.id}`, { closed: !m.closed }); reload()
1132
+ } }, m.closed ? '重新打开' : '关闭里程碑'),
1133
+ h('button', { className: 'dgs-btn danger', onClick: async () => {
1134
+ if (!confirm('删除里程碑 ' + m.title + ' ?')) return
1135
+ await api('DELETE', `/dsh/repos/${repo.owner}/${repo.name}/milestones/${m.id}`)
1136
+ if (openMs === m.id) { setOpenMs(null); setMsIssues(null) }
1137
+ reload()
1138
+ } }, t('delete')))),
1139
+ openMs === m.id ? h('div', { style: { margin: '6px 0 2px 24px' } },
1140
+ msIssues === null ? h('span', { className: 'dgs-sub' }, t('loading'))
1141
+ : msIssues.length === 0 ? h('span', { className: 'dgs-sub' }, t('noIssues'))
1142
+ : msIssues.map((i) => h('div', { key: i.number, className: 'dgs-row', style: { padding: '2px 0' } },
1143
+ h('span', { className: 'dgs-badge' + (i.state === 'open' ? ' ok' : ' closed') }, '#' + i.number),
1144
+ h('span', { style: { cursor: 'pointer' }, onClick: () => nav('/r/' + repo.owner + '/' + repo.name) }, i.title)))) : null)
1145
+ }))
1146
+ }
1147
+
1148
+ // ── 仓库设置(基础信息 + 协作者) ──────────────────────────────────────────
1149
+
1150
+ function RepoSettings({ repo, t }) {
1151
+ const [sub, setSub] = useState('basic')
1152
+ const [info, setInfo] = useState(null)
1153
+ const [form, setForm] = useState({ description: '', private: false, website: '' })
1154
+ const [collabs, setCollabs] = useState([])
1155
+ const [addName, setAddName] = useState('')
1156
+ const [hooks, setHooks] = useState(null)
1157
+ const [hookUrl, setHookUrl] = useState('')
1158
+ const [msg, setMsg] = useState('')
1159
+ const [busy, setBusy] = useState(false)
1160
+ const loadCollabs = () => api('GET', `/dsh/repos/${repo.owner}/${repo.name}/collaborators`).then((d) => setCollabs(Array.isArray(d) ? d : []))
1161
+ const loadHooks = () => api('GET', `/dsh/repos/${repo.owner}/${repo.name}/hooks`).then((d) => setHooks(Array.isArray(d) ? d : []))
1162
+ useEffect(() => {
1163
+ api('GET', `/dsh/repos/${repo.owner}/${repo.name}`).then((d) => {
1164
+ if (d && d.name) { setInfo(d); setForm({ description: d.description || '', private: !!d.private, website: d.website || '' }) }
1165
+ else setMsg((d && d.error) || t('loadFailed'))
1166
+ })
1167
+ loadCollabs()
1168
+ loadHooks()
1169
+ }, [repo.owner, repo.name])
1170
+ if (info === null && !msg) return h('div', { className: 'dgs-empty' }, t('loading'))
1171
+ const navItem = (k, label) => h('span', { key: k, className: 'item' + (sub === k ? ' active' : ''), onClick: () => setSub(k) }, label)
1172
+ const basic = h('div', { className: 'dgs-card', style: { marginBottom: 12 } },
1173
+ h('div', { style: { fontWeight: 700, marginBottom: 8 } }, '基本设置'),
1174
+ h('div', { className: 'dgs-row' },
1175
+ h('span', { className: 'dgs-sub', style: { minWidth: 70 } }, '仓库描述'),
1176
+ h('input', { className: 'dgs-input', value: form.description, onChange: (e) => setForm({ ...form, description: e.target.value }) })),
1177
+ h('div', { className: 'dgs-sub', style: { margin: '2px 0 0 78px', fontSize: 11 } }, '最多 512 个字符,剩余 ' + (512 - (form.description || '').length)),
1178
+ h('div', { className: 'dgs-row' },
1179
+ h('span', { className: 'dgs-sub', style: { minWidth: 70 } }, '官方网站'),
1180
+ h('input', { className: 'dgs-input', placeholder: 'https://…', value: form.website, onChange: (e) => setForm({ ...form, website: e.target.value }) })),
1181
+ h('div', { className: 'dgs-row' },
1182
+ h('span', { className: 'dgs-sub', style: { minWidth: 70 } }, '可见性'),
1183
+ h('label', { className: 'dgs-sub', style: { display: 'flex', gap: 4, alignItems: 'center' } },
1184
+ h('input', { type: 'checkbox', checked: form.private, onChange: (e) => setForm({ ...form, private: e.target.checked }) }), '该仓库为私有的')),
1185
+ h('div', { className: 'dgs-row', style: { marginTop: 8 } },
1186
+ h('button', { className: 'dgs-btn', disabled: busy,
1187
+ onClick: async () => {
1188
+ setBusy(true); setMsg('')
1189
+ const d = await api('PATCH', `/dsh/repos/${repo.owner}/${repo.name}`, form)
1190
+ setBusy(false)
1191
+ d && d.ok ? setMsg('✓ 已保存') : setMsg((d && d.error) || 'failed')
1192
+ } }, '更新设置')))
1193
+ const collab = h('div', { className: 'dgs-card' },
1194
+ h('div', { style: { fontWeight: 700, marginBottom: 8 } }, '协作者'),
1195
+ h('div', { className: 'dgs-row' },
1196
+ h('input', { className: 'dgs-input', style: { maxWidth: 200 }, placeholder: '用户名', value: addName, onChange: (e) => setAddName(e.target.value) }),
1197
+ h('button', { className: 'dgs-btn', disabled: !addName.trim(),
1198
+ onClick: async () => {
1199
+ const d = await api('POST', `/dsh/repos/${repo.owner}/${repo.name}/collaborators/${encodeURIComponent(addName.trim())}`)
1200
+ if (d && d.ok !== false && !d.error) { setAddName(''); setMsg(''); loadCollabs() } else setMsg((d && d.error) || 'failed')
1201
+ } }, '+ 添加')),
1202
+ collabs.length === 0 ? h('div', { className: 'dgs-sub', style: { marginTop: 8 } }, '—') : null,
1203
+ collabs.map((u) => h('div', { key: u.name, className: 'dgs-row', style: { marginTop: 6 } },
1204
+ h('a', { className: 'dgs-name', href: '#/u/' + u.name }, u.name),
1205
+ h('span', { style: { flex: 1 } }),
1206
+ h('button', { className: 'dgs-btn danger', onClick: async () => {
1207
+ await api('DELETE', `/dsh/repos/${repo.owner}/${repo.name}/collaborators/${encodeURIComponent(u.name)}`)
1208
+ loadCollabs()
1209
+ } }, t('delete')))))
1210
+ const hooksCard = h('div', { className: 'dgs-card' },
1211
+ h('div', { style: { fontWeight: 700, marginBottom: 8 } }, 'Webhook'),
1212
+ h('div', { className: 'dgs-row' },
1213
+ h('input', { className: 'dgs-input', placeholder: 'https://…', value: hookUrl, onChange: (e) => setHookUrl(e.target.value) }),
1214
+ h('button', { className: 'dgs-btn', disabled: !hookUrl.trim(), onClick: async () => {
1215
+ const d = await api('POST', `/dsh/repos/${repo.owner}/${repo.name}/hooks`, { url: hookUrl.trim() })
1216
+ if (d && !d.error) { setHookUrl(''); loadHooks() } else setMsg((d && d.error) || 'failed')
1217
+ } }, '+ 添加')),
1218
+ (hooks || []).map((hk) => h('div', { key: hk.id, className: 'dgs-row', style: { marginTop: 6 } },
1219
+ h('span', { className: 'dgs-badge' + (hk.is_active ? ' ok' : ' closed') }, hk.is_active ? '启用' : '停用'),
1220
+ h('span', { className: 'dgs-sub', style: { flex: 1, overflow: 'hidden', textOverflow: 'ellipsis' } }, hk.url),
1221
+ h('button', { className: 'dgs-btn ghost', onClick: async () => {
1222
+ await api('PATCH', `/dsh/repos/${repo.owner}/${repo.name}/hooks/${hk.id}`, { active: !hk.is_active }); loadHooks()
1223
+ } }, hk.is_active ? '停用' : '启用'),
1224
+ h('button', { className: 'dgs-btn danger', onClick: async () => {
1225
+ await api('DELETE', `/dsh/repos/${repo.owner}/${repo.name}/hooks/${hk.id}`); loadHooks()
1226
+ } }, t('delete')))),
1227
+ hooks !== null && hooks.length === 0 ? h('div', { className: 'dgs-sub', style: { marginTop: 6 } }, '—') : null)
1228
+ const danger = h('div', { className: 'dgs-card', style: { marginTop: 12, borderColor: 'var(--dsw-alias-state-error-primary)55' } },
1229
+ h('div', { style: { fontWeight: 700, marginBottom: 8, color: 'var(--dsw-alias-state-error-primary)' } }, '危险区域'),
1230
+ h('div', { className: 'dgs-row' },
1231
+ h('span', { className: 'dgs-sub', style: { flex: 1 } }, '转移仓库所有权(组织/用户)'),
1232
+ h('button', { className: 'dgs-btn ghost', onClick: async () => {
1233
+ const org = prompt('转移到组织名(留空 = 当前用户)', '')
1234
+ if (org === null) return
1235
+ const d = await api('POST', `/dsh/repos/${repo.owner}/${repo.name}/transfer`, { org: org || undefined })
1236
+ setMsg(d && d.ok ? '已转移' : (d && d.error) || '转移失败')
1237
+ } }, '转移仓库')),
1238
+ h('div', { className: 'dgs-row' },
1239
+ h('span', { className: 'dgs-sub', style: { flex: 1 } }, '清除 Wiki 数据(删除所有 Wiki 页面)'),
1240
+ h('button', { className: 'dgs-btn danger', onClick: async () => {
1241
+ if (!confirm('确定清除该仓库的全部 Wiki 页面?')) return
1242
+ const d = await api('POST', `/dsh/repos/${repo.owner}/${repo.name}/wiki-wipe`)
1243
+ setMsg(d && d.ok ? '已清除' : (d && d.error) || '清除失败')
1244
+ } }, '清除 Wiki 数据')),
1245
+ h('div', { className: 'dgs-row' },
1246
+ h('span', { className: 'dgs-sub', style: { flex: 1 } }, '删除此仓库(含所有工单/PR/Wiki)'),
1247
+ h('button', { className: 'dgs-btn danger', onClick: async () => {
1248
+ if (!confirm('确定删除仓库 ' + repo.name + ' ?此操作不可恢复')) return
1249
+ const d = await api('DELETE', '/repos/' + repo.owner + '/' + repo.name)
1250
+ if (d && d.ok) nav('/repos')
1251
+ else setMsg((d && d.error) || '删除失败')
1252
+ } }, t('delete') + ' ' + repo.name)))
1253
+ return h('div', null,
1254
+ msg ? h('div', { className: 'dgs-err' }, msg) : null,
1255
+ h('div', { className: 'dgs-settings-layout' },
1256
+ h('div', { className: 'dgs-settings-nav' },
1257
+ navItem('basic', '基本设置'),
1258
+ navItem('collab', '管理协作者'),
1259
+ navItem('hooks', '管理 Web 钩子'),
1260
+ navItem('danger', '危险区域')),
1261
+ h('div', { style: { flex: 1, minWidth: 0 } },
1262
+ sub === 'basic' ? basic : sub === 'collab' ? collab : sub === 'hooks' ? hooksCard : danger)))
1263
+ }
1264
+
1265
+ // ── PR(列表 + 详情 + 合并 + 评论复用 issue 通道) ────────────────────────
1266
+
1267
+ function Pulls({ repo, t, preset, onPresetDone, onGoIssuesSub }) {
1268
+ const [list, setList] = useState(null)
1269
+ const [openIdx, setOpenIdx] = useState(null)
1270
+ const [creating, setCreating] = useState(false)
1271
+ const [state, setState] = useState('open')
1272
+ const [flt, setFlt] = useState({ label: '', milestone: '', assignee: '' })
1273
+ const [sort, setSort] = useState('latest')
1274
+ const [meta, setMeta] = useState({ labels: [], milestones: [], collabs: [] })
1275
+ const [branches, setBranches] = useState([])
1276
+ const [form, setForm] = useState({ title: '', head: '', base: '', body: '' })
1277
+ const [sel, setSel] = useState({ labels: new Set(), milestone: '', assignee: '' })
1278
+ const [msg, setMsg] = useState('')
1279
+ const [busy, setBusy] = useState(false)
1280
+ const [preview, setPreview] = useState(null)
1281
+ const [previewMode, setPreviewMode] = useState(false)
1282
+ const [previewHtml, setPreviewHtml] = useState('')
1283
+ const reload = useCallback(() => {
1284
+ setList(null)
1285
+ api('GET', `/dsh/repos/${repo.owner}/${repo.name}/pulls`).then((d) => setList(Array.isArray(d) ? d : (d.data || d.pulls || [])))
1286
+ }, [repo.owner, repo.name])
1287
+ useEffect(reload, [reload])
1288
+ useEffect(() => {
1289
+ api('GET', `/dsh/repos/${repo.owner}/${repo.name}/overview`).then((d) => {
1290
+ setBranches(d.branches || [])
1291
+ setForm((f) => ({ ...f, base: d.defaultBranch || 'master' }))
1292
+ })
1293
+ api('GET', `/dsh/repos/${repo.owner}/${repo.name}/labels`).then((d) => setMeta((m) => ({ ...m, labels: Array.isArray(d) ? d : [] })))
1294
+ api('GET', `/dsh/repos/${repo.owner}/${repo.name}/milestones`).then((d) => setMeta((m) => ({ ...m, milestones: Array.isArray(d) ? d : [] })))
1295
+ api('GET', `/dsh/repos/${repo.owner}/${repo.name}/collaborators`).then((d) => setMeta((m) => ({ ...m, collabs: Array.isArray(d) ? d : [] })))
1296
+ }, [repo.owner, repo.name])
1297
+ useEffect(() => {
1298
+ if (!creating || !form.head || !form.base || form.head === form.base) { setPreview(null); return }
1299
+ api('GET', `/dsh/repos/${repo.owner}/${repo.name}/compare?base=${encodeURIComponent(form.base)}&head=${encodeURIComponent(form.head)}`)
1300
+ .then((d) => setPreview(d && d.commits ? d : null))
1301
+ }, [creating, form.head, form.base, repo.owner, repo.name])
1302
+ useEffect(() => {
1303
+ if (preset) {
1304
+ setCreating(true)
1305
+ setForm((f) => ({ ...f, head: preset.head, base: preset.base || f.base }))
1306
+ onPresetDone && onPresetDone()
1307
+ }
1308
+ }, [preset])
1309
+ if (openIdx !== null) return h(PullDetail, { repo, idx: openIdx, t, onBack: () => { setOpenIdx(null); reload() } })
1310
+ if (list === null) return h('div', { className: 'dgs-empty' }, t('loading'))
1311
+ const openCount = list.filter((x) => x.state === 'open').length
1312
+ const closedCount = list.filter((x) => x.state !== 'open').length
1313
+ let shown = list.filter((x) => state === 'open' ? x.state === 'open' : x.state !== 'open')
1314
+ if (flt.label) shown = shown.filter((x) => (x.labels || []).some((l) => String(l.id) === String(flt.label)))
1315
+ if (flt.milestone) shown = shown.filter((x) => x.milestone && String(x.milestone.id) === String(flt.milestone))
1316
+ if (flt.assignee) shown = shown.filter((x) => x.assignee === flt.assignee)
1317
+ shown = [...shown].sort((a, b) => {
1318
+ const at = (a.updatedAt || 0) * 1000; const bt = (b.updatedAt || 0) * 1000
1319
+ return sort === 'latest' ? bt - at : sort === 'oldest' ? at - bt : (b.comments || 0) - (a.comments || 0)
1320
+ })
1321
+ const fltSel = (key, items, blank) => h('select', {
1322
+ key: key, className: 'dgs-input', style: { maxWidth: 130, flex: 'none', width: 'auto', padding: '5px 8px', marginLeft: 8 },
1323
+ value: flt[key], onChange: (e) => setFlt({ ...flt, [key]: e.target.value }),
1324
+ }, h('option', { value: '' }, blank),
1325
+ items.map((x) => h('option', { key: x.id || x.name, value: String(x.id || x.name) }, x.title || x.name)))
1326
+ const row = (x) => h('div', { key: x.index, className: 'dgs-issue', style: { cursor: 'pointer', display: 'flex', alignItems: 'center', gap: 10 }, onClick: () => setOpenIdx(x.index) },
1327
+ h('span', { className: 'dgs-issue-num' + (x.state !== 'open' ? ' closed' : '') }, '#' + x.index),
1328
+ h('div', { style: { flex: 1, minWidth: 0 } },
1329
+ h('div', { className: 'dgs-row', style: { gap: 6 } },
1330
+ h('span', { className: 'dgs-issue-title' }, x.title),
1331
+ (x.labels || []).map((l) => h('span', { key: l.id, className: 'dgs-badge', style: { background: (l.color || '#70c24a') + '33', borderColor: l.color || '#70c24a' } }, l.name)),
1332
+ x.milestone ? h('span', { className: 'dgs-badge' }, '◆ ' + x.milestone.title) : null,
1333
+ x.state === 'merged' ? h('span', { className: 'dgs-badge' }, '已合并') : null),
1334
+ h('div', { className: 'dgs-sub', style: { marginTop: 2 } },
1335
+ '由 ' + (x.author || '') + ' 于 ' + timeAgo((x.updatedAt || 0) * 1000 || x.updatedAt) + ' 发起 · ' + x.head + ' → ' + x.base)),
1336
+ h('span', { className: 'dgs-sub', style: { flex: 'none' } }, (x.comments != null ? '💬 ' + x.comments : '')))
1337
+ const toggleLabel = (id) => setSel((x) => {
1338
+ const next = new Set(x.labels)
1339
+ next.has(id) ? next.delete(id) : next.add(id)
1340
+ return { ...x, labels: next }
1341
+ })
1342
+ const createSide = h('div', null,
1343
+ h('div', { className: 'dgs-side-block' },
1344
+ h('div', { className: 'dgs-side-title' }, '标签 ⚙'),
1345
+ meta.labels.length === 0 ? h('div', { className: 'dgs-sub' }, '未选择标签') : null,
1346
+ meta.labels.map((l) => h('label', { key: l.id, className: 'dgs-sub', style: { display: 'flex', gap: 6, alignItems: 'center', margin: '4px 0', cursor: 'pointer' } },
1347
+ h('input', { type: 'checkbox', checked: sel.labels.has(l.id), onChange: () => toggleLabel(l.id) }),
1348
+ h('span', { className: 'dgs-badge', style: { background: (l.color || '#70c24a') + '33' } }, l.name)))),
1349
+ h('div', { className: 'dgs-side-block' },
1350
+ h('div', { className: 'dgs-side-title' }, '里程碑 ⚙'),
1351
+ h('select', { className: 'dgs-input', style: { width: '100%' }, value: sel.milestone, onChange: (e) => setSel({ ...sel, milestone: e.target.value }) },
1352
+ h('option', { value: '' }, '未选择里程碑'),
1353
+ meta.milestones.map((m) => h('option', { key: m.id, value: m.id }, m.title + (m.closed ? ' ✓' : ''))))),
1354
+ h('div', { className: 'dgs-side-block' },
1355
+ h('div', { className: 'dgs-side-title' }, '指派成员 ⚙'),
1356
+ h('select', { className: 'dgs-input', style: { width: '100%' }, value: sel.assignee, onChange: (e) => setSel({ ...sel, assignee: e.target.value }) },
1357
+ h('option', { value: '' }, '未指派成员'),
1358
+ meta.collabs.map((u) => h('option', { key: u.name, value: u.name }, u.name)))))
1359
+ const createForm = h('div', { className: 'dgs-card', style: { margin: '10px 0' } },
1360
+ h('div', { className: 'dgs-row', style: { marginBottom: 8 } },
1361
+ h('span', { style: { fontWeight: 700 } }, '对比文件变化'),
1362
+ h('span', { style: { flex: 1 } }),
1363
+ h('button', { className: 'dgs-btn ghost', onClick: () => setCreating(false) }, t('back'))),
1364
+ h('div', { className: 'dgs-sub', style: { marginBottom: 8 } }, '对比两个分支间的文件变化并发起一个合并请求。'),
1365
+ h('div', { className: 'dgs-issue-layout' },
1366
+ h('div', { className: 'dgs-issue-main' },
1367
+ h('div', { className: 'dgs-row', style: { marginBottom: 8 } },
1368
+ h('select', { className: 'dgs-input', style: { maxWidth: 170, flex: 'none', width: 'auto' }, value: form.base, onChange: (e) => setForm({ ...form, base: e.target.value }) },
1369
+ branches.map((b) => h('option', { key: b, value: b }, '基准分支: ' + b))),
1370
+ h('span', { className: 'dgs-sub' }, '…'),
1371
+ h('select', { className: 'dgs-input', style: { maxWidth: 170, flex: 'none', width: 'auto' }, value: form.head, onChange: (e) => setForm({ ...form, head: e.target.value }) },
1372
+ h('option', { value: '' }, '对比分支…'),
1373
+ branches.map((b) => h('option', { key: b, value: b }, '对比分支: ' + b)))),
1374
+ h('input', { className: 'dgs-input', placeholder: '标题', value: form.title, onChange: (e) => setForm({ ...form, title: e.target.value }) }),
1375
+ h('div', { className: 'dgs-row', style: { margin: '8px 0 4px' } },
1376
+ h('button', { className: 'dgs-subtab' + (!previewMode ? ' active' : ''), onClick: () => setPreviewMode(false) }, '内容编辑'),
1377
+ h('button', { className: 'dgs-subtab' + (previewMode ? ' active' : ''), onClick: () => {
1378
+ setPreviewMode(true)
1379
+ api('POST', '/dsh/markdown', { text: form.body }).then((d) => setPreviewHtml(d.html || ''))
1380
+ } }, '效果预览')),
1381
+ previewMode ? h('div', { className: 'dgs-md', style: { minHeight: 120, border: '1px solid var(--dsw-alias-border-l2)', borderRadius: 8, padding: 12 } },
1382
+ form.body ? h('div', { dangerouslySetInnerHTML: { __html: previewHtml } }) : h('span', { className: 'dgs-sub' }, '暂无内容'))
1383
+ : h('textarea', { className: 'dgs-input', style: { minHeight: 120 }, placeholder: '内容', value: form.body, onChange: (e) => setForm({ ...form, body: e.target.value }) }),
1384
+ preview ? h('div', { className: 'dgs-card', style: { margin: '8px 0', padding: 10 } },
1385
+ h('div', { className: 'dgs-sub', style: { marginBottom: 6 } }, `${preview.commits.length} commits`),
1386
+ h('pre', { className: 'dgs-file' }, preview.diffStat || '—')) : null,
1387
+ h('div', { className: 'dgs-row', style: { marginTop: 10, justifyContent: 'flex-end' } },
1388
+ h('button', { className: 'dgs-btn', disabled: busy || !form.title.trim() || !form.head,
1389
+ onClick: async () => {
1390
+ setBusy(true); setMsg('')
1391
+ const d = await api('POST', `/dsh/repos/${repo.owner}/${repo.name}/pulls`, {
1392
+ ...form,
1393
+ labels: [...sel.labels].map(Number),
1394
+ milestone: sel.milestone ? Number(sel.milestone) : undefined,
1395
+ assignee: sel.assignee || undefined,
1396
+ })
1397
+ setBusy(false)
1398
+ if (d.ok) { setCreating(false); setForm({ title: '', head: '', base: form.base, body: '' }); setSel({ labels: new Set(), milestone: '', assignee: '' }); reload() }
1399
+ else setMsg(d.error || 'failed')
1400
+ } }, '创建合并请求'))),
1401
+ h('div', { className: 'dgs-issue-side' }, createSide)))
1402
+ return h('div', null,
1403
+ h('div', { className: 'dgs-row', style: { margin: '0 0 12px' } },
1404
+ h('a', { style: { cursor: 'pointer', marginRight: 18, fontSize: 13, color: 'var(--dsw-alias-label-secondary)' }, onClick: () => { if (onGoIssuesSub) onGoIssuesSub('labels') } }, '标签管理'),
1405
+ h('a', { style: { cursor: 'pointer', fontSize: 13, color: 'var(--dsw-alias-label-secondary)' }, onClick: () => { if (onGoIssuesSub) onGoIssuesSub('milestones') } }, '里程碑'),
1406
+ h('span', { style: { flex: 1 } }),
1407
+ h('button', { className: 'dgs-btn', onClick: () => setCreating(!creating) }, '创建合并请求')),
1408
+ h('div', { className: 'dgs-row', style: { margin: '0 0 12px' } },
1409
+ h('button', { className: 'dgs-pill' + (state === 'open' ? ' active' : ''), onClick: () => setState('open') }, '⊘ ' + openCount + ' 个开启中'),
1410
+ h('button', { className: 'dgs-pill closed' + (state === 'closed' ? ' active' : ''), onClick: () => setState('closed') }, '✓ ' + closedCount + ' 个已关闭'),
1411
+ h('span', { style: { flex: 1 } }),
1412
+ fltSel('label', meta.labels, '标签筛选'),
1413
+ fltSel('milestone', meta.milestones, '里程碑筛选'),
1414
+ fltSel('assignee', meta.collabs, '指派人筛选'),
1415
+ h('select', { key: 'sort', className: 'dgs-input', style: { maxWidth: 130, flex: 'none', width: 'auto', padding: '5px 8px', marginLeft: 8 }, value: sort, onChange: (e) => setSort(e.target.value) },
1416
+ [['latest', '最新'], ['oldest', '最旧'], ['mostcomment', '最多评论']].map(([v, label]) => h('option', { key: v, value: v }, '排序: ' + label)))),
1417
+ msg ? h('div', { className: 'dgs-err' }, msg) : null,
1418
+ creating ? createForm : null,
1419
+ shown.length === 0 && !creating ? h('div', { className: 'dgs-empty' }, t('noPulls')) : null,
1420
+ shown.map(row))
1421
+ }
1422
+
1423
+ function PullDetail({ repo, idx, t, onBack }) {
1424
+ const [pr, setPr] = useState(null)
1425
+ const [comments, setComments] = useState(null)
1426
+ const [text, setText] = useState('')
1427
+ const [busy, setBusy] = useState(false)
1428
+ const [msg, setMsg] = useState('')
1429
+ const [mergeStyle, setMergeStyle] = useState('merge')
1430
+ const [tab, setTab] = useState('conv')
1431
+ const [cmp, setCmp] = useState(null)
1432
+ const [selCommit, setSelCommit] = useState(null)
1433
+ const [issue, setIssue] = useState(null)
1434
+ const [editing, setEditing] = useState(false)
1435
+ const [draft, setDraft] = useState({ title: '', body: '' })
1436
+ const [allLabels, setAllLabels] = useState([])
1437
+ const [allMs, setAllMs] = useState([])
1438
+ const [collabs, setCollabs] = useState([])
1439
+ const [mergeMsg, setMergeMsg] = useState('')
1440
+ useEffect(() => {
1441
+ api('GET', `/dsh/repos/${repo.owner}/${repo.name}/pulls`).then((d) => {
1442
+ const all = Array.isArray(d) ? d : (d.data || d.pulls || [])
1443
+ const found = all.find((x) => x.index === idx) || null
1444
+ setPr(found)
1445
+ if (found) api('GET', `/dsh/repos/${repo.owner}/${repo.name}/compare?base=${encodeURIComponent(found.base)}&head=${encodeURIComponent(found.head)}`).then((c) => c && c.commits && setCmp(c))
1446
+ })
1447
+ api('GET', `/repos/${repo.owner}/${repo.name}/issues/${idx}/comments`).then((d) => setComments(d.comments || []))
1448
+ api('GET', `/dsh/repos/${repo.owner}/${repo.name}/issues/${idx}`).then((d) => setIssue(d && d.number ? d : null))
1449
+ api('GET', `/dsh/repos/${repo.owner}/${repo.name}/labels`).then((d) => setAllLabels(Array.isArray(d) ? d : []))
1450
+ api('GET', `/dsh/repos/${repo.owner}/${repo.name}/milestones`).then((d) => setAllMs(Array.isArray(d) ? d : []))
1451
+ api('GET', `/dsh/repos/${repo.owner}/${repo.name}/collaborators`).then((d) => setCollabs(Array.isArray(d) ? d : []))
1452
+ }, [repo.owner, repo.name, idx])
1453
+ const patchIssue = async (fields) => {
1454
+ setBusy(true)
1455
+ await api('PATCH', `/dsh/repos/${repo.owner}/${repo.name}/issues/${idx}`, fields)
1456
+ setBusy(false)
1457
+ api('GET', `/dsh/repos/${repo.owner}/${repo.name}/issues/${idx}`).then((d) => setIssue(d && d.number ? d : null))
1458
+ }
1459
+
1460
+ if (pr === null) return h('div', { className: 'dgs-empty' }, t('loading'))
1461
+ const headRef = (pr.headUser || repo.owner) + '/' + pr.head
1462
+ const baseRef = repo.owner + '/' + pr.base
1463
+ const side = h('div', null,
1464
+ h('div', { className: 'dgs-side-block' },
1465
+ h('div', { className: 'dgs-side-title' }, '标签 ⚙'),
1466
+ !issue ? h('div', { className: 'dgs-sub' }, '…') : allLabels.length === 0 ? h('div', { className: 'dgs-sub' }, '未选择标签') : null,
1467
+ issue && allLabels.map((l) => h('label', { key: l.id, className: 'dgs-sub', style: { display: 'flex', gap: 6, alignItems: 'center', margin: '4px 0', cursor: 'pointer' } },
1468
+ h('input', { type: 'checkbox', checked: (issue.labels || []).some((x) => x.id === l.id),
1469
+ onChange: (e) => {
1470
+ const cur = new Set((issue.labels || []).map((x) => x.id))
1471
+ if (e.target.checked) cur.add(l.id); else cur.delete(l.id)
1472
+ patchIssue({ labels: [...cur] })
1473
+ } }),
1474
+ h('span', { className: 'dgs-badge', style: { background: (l.color || '#70c24a') + '33' } }, l.name)))),
1475
+ h('div', { className: 'dgs-side-block' },
1476
+ h('div', { className: 'dgs-side-title' }, '里程碑 ⚙'),
1477
+ !issue ? h('div', { className: 'dgs-sub' }, '…')
1478
+ : h('select', { className: 'dgs-input', style: { width: '100%' }, value: issue.milestone ? issue.milestone.id : '',
1479
+ onChange: (e) => patchIssue({ milestone: Number(e.target.value) || 0 }) },
1480
+ h('option', { value: '' }, '未选择里程碑'),
1481
+ allMs.map((m) => h('option', { key: m.id, value: m.id }, m.title + (m.closed ? ' ✓' : ''))))),
1482
+ h('div', { className: 'dgs-side-block' },
1483
+ h('div', { className: 'dgs-side-title' }, '指派成员 ⚙'),
1484
+ !issue ? h('div', { className: 'dgs-sub' }, '…')
1485
+ : h('select', { className: 'dgs-input', style: { width: '100%' }, value: issue.assignee || '',
1486
+ onChange: (e) => patchIssue({ assignee: e.target.value }) },
1487
+ h('option', { value: '' }, '未指派成员'),
1488
+ collabs.map((u) => h('option', { key: u.name, value: u.name }, u.name)))),
1489
+ h('div', { className: 'dgs-side-block' },
1490
+ h('div', { className: 'dgs-side-title' }, (issue && issue.participants || 1) + ' 名参与者'),
1491
+ h('div', { className: 'dgs-sub' }, pr.author || '')))
1492
+ const mergeBox = pr.state === 'open' ? h('div', { className: 'dgs-card', style: { margin: '10px 0', borderColor: 'var(--dsw-alias-state-success-primary)55' } },
1493
+ h('div', { className: 'dgs-row', style: { gap: 8 } },
1494
+ h('span', { style: { color: 'var(--dsw-alias-state-success-primary)', fontSize: 16 } }, '⑂'),
1495
+ h('span', { style: { color: 'var(--dsw-alias-state-success-primary)', fontSize: 13 } }, '该合并请求可以进行自动合并操作。')),
1496
+ h('div', { style: { margin: '10px 0' } },
1497
+ h('label', { className: 'dgs-sub', style: { display: 'flex', gap: 6, alignItems: 'center', margin: '4px 0' } },
1498
+ h('input', { type: 'radio', name: 'mergeStyle', checked: mergeStyle === 'merge', onChange: () => setMergeStyle('merge') }), '创建一个新的合并提交'),
1499
+ h('label', { className: 'dgs-sub', style: { display: 'flex', gap: 6, alignItems: 'center', margin: '4px 0' } },
1500
+ h('input', { type: 'radio', name: 'mergeStyle', checked: mergeStyle === 'squash', onChange: () => setMergeStyle('squash') }), '压缩提交并合并'),
1501
+ h('label', { className: 'dgs-sub', style: { display: 'flex', gap: 6, alignItems: 'center', margin: '4px 0' } },
1502
+ h('input', { type: 'radio', name: 'mergeStyle', checked: mergeStyle === 'rebase', onChange: () => setMergeStyle('rebase') }), '变基并合并')),
1503
+ h('div', { className: 'dgs-sub', style: { margin: '6px 0' } }, '提交说明:'),
1504
+ h('textarea', { className: 'dgs-input', style: { minHeight: 60 }, value: mergeMsg, onChange: (e) => setMergeMsg(e.target.value) }),
1505
+ h('div', { className: 'dgs-row', style: { marginTop: 10 } },
1506
+ h('button', { className: 'dgs-btn', disabled: busy,
1507
+ onClick: async () => {
1508
+ if (!confirm(t('mergeConfirm'))) return
1509
+ setBusy(true); setMsg('')
1510
+ const d = await api('POST', `/dsh/repos/${repo.owner}/${repo.name}/pulls/${idx}/merge`, { style: mergeStyle, message: mergeMsg })
1511
+ setBusy(false)
1512
+ d.ok !== false && !d.error ? onBack() : setMsg(d.error || 'merge failed')
1513
+ } }, '⇆ 合并请求'),
1514
+ h('button', { className: 'dgs-btn danger', disabled: busy,
1515
+ onClick: async () => {
1516
+ if (!confirm('不合并直接关闭此 PR?')) return
1517
+ setBusy(true)
1518
+ await api('POST', `/dsh/repos/${repo.owner}/${repo.name}/pulls/${idx}/close`)
1519
+ setBusy(false); onBack()
1520
+ } }, '关闭')))
1521
+ : h('div', { className: 'dgs-card', style: { margin: '10px 0' } },
1522
+ h('span', { className: 'dgs-badge' }, pr.state === 'merged' ? '已合并' : '已关闭'))
1523
+ const conv = h('div', null,
1524
+ (comments || []).map((c) =>
1525
+ h('div', { key: c.id, className: 'dgs-comment' },
1526
+ h('div', { className: 'dgs-sub', style: { marginBottom: 4 } }, `${c.user || ''} 评论于 ${fmtDate(c.created)}`),
1527
+ h('div', { style: { whiteSpace: 'pre-wrap' } }, c.body || '这个人很懒,什么都没留下。'))),
1528
+ h('div', { className: 'dgs-card', style: { margin: '10px 0' } },
1529
+ h('textarea', { className: 'dgs-input', placeholder: t('commentPlaceholder'), value: text, onChange: (e) => setText(e.target.value) }),
1530
+ h('div', { className: 'dgs-row', style: { marginTop: 8 } },
1531
+ h('button', { className: 'dgs-btn', disabled: busy || !text.trim(),
1532
+ onClick: async () => {
1533
+ setBusy(true)
1534
+ await api('POST', `/repos/${repo.owner}/${repo.name}/issues/${idx}/comments`, { body: text })
1535
+ setText(''); setBusy(false)
1536
+ api('GET', `/repos/${repo.owner}/${repo.name}/issues/${idx}/comments`).then((d) => setComments(d.comments || []))
1537
+ } }, t('comment')))))
1538
+ const commitsTab = selCommit
1539
+ ? h(CommitDetail, { repo, sha: selCommit, t, onBack: () => setSelCommit(null) })
1540
+ : cmp === null ? h('div', { className: 'dgs-empty' }, t('loading'))
1541
+ : cmp.commits.length === 0 ? h('div', { className: 'dgs-empty' }, '—')
1542
+ : h('table', { className: 'dgs-table' },
1543
+ h('tbody', null, cmp.commits.map((c) =>
1544
+ h('tr', { key: c.sha, style: { cursor: 'pointer' }, onClick: () => setSelCommit(c.sha) },
1545
+ h('td', null, h('div', { style: { fontWeight: 500 } }, (c.message || '').split('\n')[0]),
1546
+ h('div', { className: 'dgs-sub' }, c.author)),
1547
+ h('td', { className: 'dgs-sub', style: { textAlign: 'right' } },
1548
+ h('a', { style: { color: 'var(--dsw-alias-state-business-primary)', cursor: 'pointer' } }, (c.sha || '').slice(0, 10)))))))
1549
+ const filesTab = cmp === null ? h('div', { className: 'dgs-empty' }, t('loading'))
1550
+ : cmp.diff ? h(DiffView, { patch: cmp.diff })
1551
+ : h('div', { className: 'dgs-empty' }, '无差异')
1552
+ const tabBtn = (k, label, count) => h('button', { key: k, className: 'dgs-subtab' + (tab === k ? ' active' : ''), onClick: () => setTab(k) },
1553
+ label + (count !== undefined && count !== null ? ' ' + count : ''))
1554
+ return h('div', null,
1555
+ h('div', { className: 'dgs-row', style: { marginBottom: 6 } },
1556
+ h('button', { className: 'dgs-btn ghost', onClick: onBack }, t('back')),
1557
+ h('span', { className: 'dgs-h1', style: { fontSize: 17 } }, '#' + idx + ' ' + pr.title),
1558
+ h('span', { style: { flex: 1 } }),
1559
+ h('button', { className: 'dgs-btn ghost', onClick: () => {
1560
+ if (!issue) return
1561
+ setDraft({ title: issue.title, body: issue.body })
1562
+ setEditing(!editing)
1563
+ } }, '编辑')),
1564
+ h('div', { className: 'dgs-row', style: { marginBottom: 10 } },
1565
+ h('span', { className: 'dgs-issue-num' }, pr.state === 'open' ? '开启中' : pr.state === 'merged' ? '已合并' : '已关闭'),
1566
+ h('span', { className: 'dgs-sub' },
1567
+ (pr.author || '') + ' 请求将 ' + (cmp ? cmp.commits.length : 'N') + ' 次代码提交从 ' + headRef + ' 合并至 ' + baseRef)),
1568
+ msg ? h('div', { className: 'dgs-err' }, msg) : null,
1569
+ editing && issue ? h('div', { className: 'dgs-card', style: { margin: '10px 0' } },
1570
+ h('input', { className: 'dgs-input', value: draft.title, onChange: (e) => setDraft({ ...draft, title: e.target.value }) }),
1571
+ h('textarea', { className: 'dgs-input', style: { marginTop: 8, minHeight: 100 }, value: draft.body, onChange: (e) => setDraft({ ...draft, body: e.target.value }) }),
1572
+ h('div', { className: 'dgs-row', style: { marginTop: 8, justifyContent: 'flex-end' } },
1573
+ h('button', { className: 'dgs-btn ghost', onClick: () => setEditing(false) }, '取消'),
1574
+ h('button', { className: 'dgs-btn', disabled: busy || !draft.title.trim(),
1575
+ onClick: async () => { await patchIssue({ title: draft.title, body: draft.body }); setEditing(false) } }, '保存'))) : null,
1576
+ h('div', { className: 'dgs-subtabs', style: { margin: '8px 0 12px' } },
1577
+ tabBtn('conv', '对话内容', (comments || []).length),
1578
+ tabBtn('commits', '代码提交', cmp ? cmp.commits.length : ''),
1579
+ tabBtn('files', '文件变动', cmp && cmp.diff ? '1' : '0')),
1580
+ h('div', { className: 'dgs-issue-layout' },
1581
+ h('div', { className: 'dgs-issue-main' },
1582
+ tab === 'conv' ? h('div', null, conv, mergeBox) : tab === 'commits' ? commitsTab : filesTab),
1583
+ h('div', { className: 'dgs-issue-side' }, side)))
1584
+ }
1585
+
1586
+ function WikiView({ repo, t }) {
1587
+ const [pages, setPages] = useState(null)
1588
+ const [page, setPage] = useState(null) // {name, html, content}
1589
+ const [hist, setHist] = useState(null)
1590
+ const [histMeta, setHistMeta] = useState(null)
1591
+ const [editing, setEditing] = useState(false)
1592
+ const [draft, setDraft] = useState('')
1593
+ const [creating, setCreating] = useState(false)
1594
+ const [form, setForm] = useState({ title: '', content: '' })
1595
+ const [busy, setBusy] = useState(false)
1596
+ const [msg, setMsg] = useState('')
1597
+ const reload = useCallback(() => {
1598
+ setPages(null)
1599
+ api('GET', `/dsh/repos/${repo.owner}/${repo.name}/wiki`).then((d) => setPages(d.pages || []))
1600
+ }, [repo.owner, repo.name])
1601
+ useEffect(reload, [reload])
1602
+ const openPage = (name) => {
1603
+ setEditing(false); setCreating(false); setMsg(''); setHist(null); setHistMeta(null)
1604
+ api('GET', `/dsh/repos/${repo.owner}/${repo.name}/wiki/${encodeURIComponent(name)}`)
1605
+ .then((d) => d.html !== undefined ? setPage(d) : setPage({ name, missing: true, content: '', html: '' }))
1606
+ api('GET', `/dsh/repos/${repo.owner}/${repo.name}/wiki/${encodeURIComponent(name)}/commits`)
1607
+ .then((d) => { if (Array.isArray(d) && d.length) setHistMeta({ author: d[0].author, date: d[0].date }) })
1608
+ }
1609
+ return h('div', null,
1610
+ h('div', { className: 'dgs-row', style: { margin: '8px 0 12px' } },
1611
+ h('span', { style: { fontWeight: 700 } }, t('wiki')),
1612
+ h('span', { style: { flex: 1 } }),
1613
+ h('button', { className: 'dgs-btn', onClick: () => {
1614
+ if (creating) { setCreating(false); return }
1615
+ setCreating(true); setEditing(false); setPage(null); setForm({ title: '', content: '' })
1616
+ } }, '+ ' + t('newWiki'))),
1617
+ msg ? h('div', { className: 'dgs-err' }, msg) : null,
1618
+ creating ? h('div', { className: 'dgs-card', style: { margin: '10px 0' } },
1619
+ h('input', { className: 'dgs-input', placeholder: t('wikiTitle'), value: form.title, onChange: (e) => setForm({ ...form, title: e.target.value }) }),
1620
+ h('textarea', { className: 'dgs-input', style: { marginTop: 8, minHeight: '40vh' }, placeholder: '内容(Markdown)', value: form.content, onChange: (e) => setForm({ ...form, content: e.target.value }) }),
1621
+ h('div', { className: 'dgs-row', style: { marginTop: 8 } },
1622
+ h('button', { className: 'dgs-btn', disabled: busy || !form.title.trim(),
1623
+ onClick: async () => {
1624
+ setBusy(true); setMsg('')
1625
+ const name = form.title.trim()
1626
+ const d = await api('POST', `/dsh/repos/${repo.owner}/${repo.name}/wiki/${encodeURIComponent(name)}`, { content: form.content })
1627
+ setBusy(false)
1628
+ if (d.ok) { setCreating(false); reload(); openPage(name) } else setMsg(d.error || 'save failed')
1629
+ } }, t('submit'))))
1630
+ : h('div', { className: 'dgs-row', style: { alignItems: 'flex-start' } },
1631
+ h('div', { style: { width: 180, flex: 'none' } },
1632
+ pages === null ? h('div', { className: 'dgs-sub' }, t('loading'))
1633
+ : pages.length === 0 ? h('div', { className: 'dgs-sub' }, t('noWiki'))
1634
+ : h('div', null, pages.map((p) =>
1635
+ h('div', { key: p.name, className: 'dgs-crumb', style: { padding: '3px 0' }, onClick: () => openPage(p.name) }, p.name)))),
1636
+ h('div', { style: { flex: 1, minWidth: 0, paddingLeft: 16 } },
1637
+ page === null ? h('div', { className: 'dgs-card', style: { margin: '40px auto', maxWidth: 460, textAlign: 'center', padding: '36px 20px' } },
1638
+ h('div', { style: { fontSize: 34, marginBottom: 10 } }, '📖'),
1639
+ h('div', { style: { fontWeight: 700, fontSize: 16, marginBottom: 8 } }, '欢迎使用 Wiki!'),
1640
+ h('div', { className: 'dgs-sub' }, 'Wiki 是用于共同协作文档的地方,清晰的文档可以帮助其他人深入了解您的项目。'),
1641
+ pages && pages.length ? h('div', { className: 'dgs-sub', style: { marginTop: 10 } }, '← 从左侧选择页面') : null)
1642
+ : editing ? h('div', null,
1643
+ h('div', { className: 'dgs-row', style: { margin: '4px 0 8px' } },
1644
+ h('span', { style: { fontWeight: 600 } }, page.name + '.md'),
1645
+ h('span', { style: { flex: 1 } }),
1646
+ h('button', { className: 'dgs-btn ghost', onClick: () => page.html ? setEditing(false) : setPage(null) }, t('back')),
1647
+ h('button', { className: 'dgs-btn', disabled: busy,
1648
+ onClick: async () => {
1649
+ setBusy(true); setMsg('')
1650
+ const d = await api('POST', `/dsh/repos/${repo.owner}/${repo.name}/wiki/${encodeURIComponent(page.name)}`, { content: draft })
1651
+ setBusy(false)
1652
+ if (d.ok) { setEditing(false); reload(); openPage(page.name) } else setMsg(d.error || 'save failed')
1653
+ } }, t('save'))),
1654
+ h('textarea', { className: 'dgs-input', style: { minHeight: '50vh' }, value: draft, onChange: (e) => setDraft(e.target.value) }))
1655
+ : page.missing ? h('div', { className: 'dgs-empty' }, '404')
1656
+ : h('div', null,
1657
+ h('div', { className: 'dgs-row', style: { margin: '4px 0 8px' } },
1658
+ h('div', null,
1659
+ h('span', { style: { fontWeight: 600 } }, page.name),
1660
+ histMeta ? h('div', { className: 'dgs-sub', style: { marginTop: 2 } }, (histMeta.author || '') + ' 于 ' + timeAgo(histMeta.date) + ' 修改了此页面') : null),
1661
+ h('span', { style: { flex: 1 } }),
1662
+ h('button', { className: 'dgs-btn ghost', onClick: () => { setDraft(page.content || ''); setEditing(true) } }, t('edit')),
1663
+ h('button', { className: 'dgs-btn ghost', onClick: () => {
1664
+ api('GET', `/dsh/repos/${repo.owner}/${repo.name}/wiki/${encodeURIComponent(page.name)}/commits`).then((d) => setHist(Array.isArray(d) ? d : []))
1665
+ } }, '历史'),
1666
+ h('button', { className: 'dgs-btn danger', onClick: async () => {
1667
+ if (!confirm('删除页面 ' + page.name + ' ?')) return
1668
+ await api('DELETE', `/dsh/repos/${repo.owner}/${repo.name}/wiki/${encodeURIComponent(page.name)}`)
1669
+ setPage(null); reload()
1670
+ } }, t('delete'))),
1671
+ hist ? h('div', { className: 'dgs-card', style: { margin: '8px 0', padding: '8px 14px' } },
1672
+ hist.length === 0 ? h('span', { className: 'dgs-sub' }, '—')
1673
+ : hist.map((cm, i) => h('div', { key: i, className: 'dgs-row', style: { padding: '2px 0' } },
1674
+ h('span', { className: 'dgs-sub', style: { minWidth: 64 } }, (cm.sha || '').slice(0, 7)),
1675
+ h('span', { style: { flex: 1 } }, cm.message || ''),
1676
+ h('span', { className: 'dgs-sub' }, (cm.author || '') + ' · ' + fmtDate(cm.date))))) : null,
1677
+ h('div', { className: 'dgs-card' },
1678
+ h('div', { className: 'dgs-md', dangerouslySetInnerHTML: { __html: page.html } }))))))
1679
+ }
1680
+
1681
+ // ── 发版(列表 + 创建) ────────────────────────────────────────────────────
1682
+
1683
+ function Releases({ repo, t }) {
1684
+ const [list, setList] = useState(null)
1685
+ const [creating, setCreating] = useState(false)
1686
+ const [form, setForm] = useState({ tag: '', title: '', note: '', target: '' })
1687
+ const [editId, setEditId] = useState(null)
1688
+ const [editForm, setEditForm] = useState({ title: '', note: '' })
1689
+ const [busy, setBusy] = useState(false)
1690
+ const [msg, setMsg] = useState('')
1691
+ const [branches, setBranches] = useState([])
1692
+ const [plainTags, setPlainTags] = useState(null)
1693
+ const reload = useCallback(() => {
1694
+ setList(null)
1695
+ api('GET', `/dsh/repos/${repo.owner}/${repo.name}/releases`).then((d) => {
1696
+ setList(Array.isArray(d) ? d : (d.data || []))
1697
+ api('GET', `/dsh/repos/${repo.owner}/${repo.name}/overview`).then((ov) => {
1698
+ setBranches(ov.branches || [])
1699
+ const released = new Set((Array.isArray(d) ? d : (d.data || [])).map((r) => r.tag))
1700
+ setPlainTags((ov.tags || []).filter((tg) => !released.has(tg)))
1701
+ })
1702
+ })
1703
+ }, [repo.owner, repo.name])
1704
+ useEffect(reload, [reload])
1705
+ return h('div', null,
1706
+ h('div', { className: 'dgs-row', style: { margin: '8px 0 12px' } },
1707
+ h('span', { style: { fontWeight: 700 } }, t('releases')),
1708
+ h('span', { style: { flex: 1 } }),
1709
+ h('button', { className: 'dgs-btn', onClick: () => setCreating(!creating) }, '+ ' + t('newRelease'))),
1710
+ msg ? h('div', { className: 'dgs-err' }, msg) : null,
1711
+ creating ? h('div', { className: 'dgs-card', style: { margin: '10px 0' } },
1712
+ h('div', { className: 'dgs-row' },
1713
+ h('span', { style: { fontWeight: 700 } }, '发布新版本'),
1714
+ h('span', { style: { flex: 1 } }),
1715
+ h('button', { className: 'dgs-btn ghost', onClick: () => setCreating(false) }, t('back'))),
1716
+ h('div', { className: 'dgs-row', style: { margin: '8px 0 0' } },
1717
+ h('input', { className: 'dgs-input', style: { maxWidth: 180 }, placeholder: '标签名(如 v0.1.0)', value: form.tag, onChange: (e) => setForm({ ...form, tag: e.target.value }) }),
1718
+ h('select', { className: 'dgs-input', style: { maxWidth: 180, flex: 'none', width: 'auto' }, value: form.target, onChange: (e) => setForm({ ...form, target: e.target.value }) },
1719
+ h('option', { value: '' }, '基于分支:默认分支'),
1720
+ branches.map((b) => h('option', { key: b, value: b }, b))),
1721
+ h('input', { className: 'dgs-input', placeholder: t('relTitle'), value: form.title, onChange: (e) => setForm({ ...form, title: e.target.value }) })),
1722
+ h('div', { className: 'dgs-sub', style: { margin: '6px 0 0' } }, '标签不存在时会基于所选分支自动创建 git 标签'),
1723
+ h('textarea', { className: 'dgs-input', placeholder: t('relNote'), value: form.note, onChange: (e) => setForm({ ...form, note: e.target.value }) }),
1724
+ h('div', { className: 'dgs-row', style: { marginTop: 8 } },
1725
+ h('label', { className: 'dgs-sub', style: { display: 'flex', gap: 4, alignItems: 'center' } },
1726
+ h('input', { type: 'checkbox', checked: form.prerelease, onChange: (e) => setForm({ ...form, prerelease: e.target.checked }) }), '预发布版本'),
1727
+ h('label', { className: 'dgs-sub', style: { display: 'flex', gap: 4, alignItems: 'center' } },
1728
+ h('input', { type: 'checkbox', checked: form.draft, onChange: (e) => setForm({ ...form, draft: e.target.checked }) }), '存为草稿'),
1729
+ h('span', { style: { flex: 1 } }),
1730
+ h('button', { className: 'dgs-btn', disabled: busy || !form.tag.trim(),
1731
+ onClick: async () => {
1732
+ setBusy(true); setMsg('')
1733
+ const d = await api('POST', `/dsh/repos/${repo.owner}/${repo.name}/releases`, form)
1734
+ setBusy(false)
1735
+ if (d.ok) { setCreating(false); setForm({ tag: '', title: '', note: '', target: '', prerelease: false, draft: false }); reload() }
1736
+ else setMsg(d.error || 'failed')
1737
+ } }, t('submit')))) : null,
1738
+ list === null ? h('div', { className: 'dgs-empty' }, t('loading'))
1739
+ : list.length === 0 ? h('div', { className: 'dgs-empty' }, t('noReleases'))
1740
+ : list.map((r) =>
1741
+ editId === r.id ? h('div', { key: r.id, className: 'dgs-card' },
1742
+ h('div', { className: 'dgs-row' },
1743
+ h('span', { className: 'dgs-badge ok' }, r.tag),
1744
+ h('span', { className: 'dgs-sub' }, '编辑发版'),
1745
+ h('span', { style: { flex: 1 } }),
1746
+ h('button', { className: 'dgs-btn ghost', onClick: () => setEditId(null) }, t('back'))),
1747
+ h('div', { style: { margin: '8px 0' } },
1748
+ h('input', { className: 'dgs-input', value: editForm.title, onChange: (e) => setEditForm({ ...editForm, title: e.target.value }) }),
1749
+ h('textarea', { className: 'dgs-input', style: { marginTop: 8 }, value: editForm.note, onChange: (e) => setEditForm({ ...editForm, note: e.target.value }) })),
1750
+ h('button', { className: 'dgs-btn', disabled: busy, onClick: async () => {
1751
+ setBusy(true)
1752
+ const d = await api('PATCH', `/dsh/repos/${repo.owner}/${repo.name}/releases/${r.id}`, editForm)
1753
+ setBusy(false)
1754
+ if (d && d.ok) { setEditId(null); reload() } else setMsg((d && d.error) || 'failed')
1755
+ } }, t('save')))
1756
+ : h('div', { key: r.id, className: 'dgs-card', style: { display: 'flex', gap: 16 } },
1757
+ h('div', { style: { flex: 'none', width: 110, paddingTop: 2 } },
1758
+ h('div', { style: { fontWeight: 600 } }, '◎ ' + r.tag),
1759
+ h('div', { className: 'dgs-sub', style: { marginTop: 4 } }, r.sha || '')),
1760
+ h('div', { style: { flex: 1, minWidth: 0 } },
1761
+ h('div', { className: 'dgs-row' },
1762
+ r.draft ? h('span', { className: 'dgs-badge' }, '草稿') : null,
1763
+ r.prerelease ? h('span', { className: 'dgs-badge pri' }, '预发布') : null,
1764
+ h('span', { style: { fontWeight: 600 } }, r.title),
1765
+ h('span', { style: { flex: 1 } }),
1766
+ h('a', { className: 'dgs-sub', style: { cursor: 'pointer' }, onClick: () => { setEditId(r.id); setEditForm({ title: r.title || '', note: r.noteRaw || '' }) } }, '(编辑)')),
1767
+ h('div', { className: 'dgs-sub', style: { marginTop: 4 } },
1768
+ (r.author || '') + ' · ' + timeAgo((r.createdAt || 0) * 1000) + ' 发布 · ' +
1769
+ (r.behind ? '在该版本发布之后已有 ' + r.behind + ' 次代码提交到 ' + (r.target || '默认') + ' 分支' : '暂无后续提交')),
1770
+ r.noteHtml ? h('div', { className: 'dgs-md', style: { marginTop: 8 }, dangerouslySetInnerHTML: { __html: r.noteHtml } }) : null,
1771
+ h('div', { style: { marginTop: 10 } },
1772
+ h('div', { style: { fontWeight: 600, fontSize: 13, marginBottom: 4 } }, '下载附件'),
1773
+ h('div', { className: 'dgs-row', style: { gap: 8 } },
1774
+ h('a', { className: 'dgs-sub', href: `/dsh-git-server/api/dsh/repos/${repo.owner}/${repo.name}/archive/${encodeURIComponent(r.tag)}.zip`, download: `${repo.name}-${r.tag}.zip` }, '源代码 (ZIP)'),
1775
+ h('a', { className: 'dgs-sub', href: `/dsh-git-server/api/dsh/repos/${repo.owner}/${repo.name}/archive/${encodeURIComponent(r.tag)}.tar.gz`, download: `${repo.name}-${r.tag}.tar.gz` }, '源代码 (TAR.GZ)')))))),
1776
+ (plainTags && plainTags.length > 0) ? h('div', { style: { marginTop: 16 } },
1777
+ h('div', { style: { fontWeight: 700, margin: '4px 0 8px' } }, '未发版的标签'),
1778
+ plainTags.map((tg) => h('div', { key: tg, className: 'dgs-issue' },
1779
+ h('span', { className: 'dgs-badge' }, tg),
1780
+ h('span', { style: { flex: 1 } }),
1781
+ h('button', { className: 'dgs-btn ghost', onClick: () => {
1782
+ setCreating(true); setEditId(null); setForm({ tag: tg, title: '', note: '', target: '' })
1783
+ } }, '+ 为此标签创建发版')))) : null)
1784
+ }
1785
+
1786
+ // ── 探索 ───────────────────────────────────────────────────────────────────
1787
+
1788
+ function Explore({ t }) {
1789
+ const [q, setQ] = useState('')
1790
+ const [tab, setTab] = useState('repos')
1791
+ const [repos, setRepos] = useState(null)
1792
+ const [users, setUsers] = useState(null)
1793
+ const load = useCallback(() => {
1794
+ api('GET', '/dsh/explore/repos?q=' + encodeURIComponent(q)).then((d) => setRepos(Array.isArray(d) ? d : (d.data || [])))
1795
+ api('GET', '/dsh/explore/users?q=' + encodeURIComponent(q)).then((d) => setUsers(Array.isArray(d) ? d : (d.data || [])))
1796
+ }, [q])
1797
+ useEffect(() => { const tm = setTimeout(load, 300); return () => clearTimeout(tm) }, [q])
1798
+ const sideItem = (k, label) => h('span', {
1799
+ key: k, className: 'item' + (tab === k ? ' active' : ''), onClick: () => setTab(k),
1800
+ style: { display: 'block', padding: '7px 10px', borderRadius: 8, cursor: 'pointer', fontSize: 13,
1801
+ color: tab === k ? 'var(--dsw-alias-label-primary)' : 'var(--dsw-alias-label-secondary)',
1802
+ fontWeight: tab === k ? 600 : 400, background: tab === k ? 'var(--dsw-alias-interactive-bg-hover)' : 'transparent' },
1803
+ }, label)
1804
+ return h('div', { className: 'dgs-settings-layout' },
1805
+ h('div', { className: 'dgs-settings-nav' }, sideItem('repos', '仓库'), sideItem('users', '用户')),
1806
+ h('div', { style: { flex: 1, minWidth: 0 } },
1807
+ h('div', { className: 'dgs-row', style: { marginBottom: 12 } },
1808
+ h('input', { className: 'dgs-input', placeholder: '搜索仓库 / 用户', value: q, onChange: (e) => setQ(e.target.value) }),
1809
+ h('button', { className: 'dgs-btn', onClick: load }, '搜索')),
1810
+ tab === 'repos' && (repos === null ? h('div', { className: 'dgs-empty' }, t('loading'))
1811
+ : repos.length === 0 ? h('div', { className: 'dgs-empty' }, '—')
1812
+ : repos.map((r) =>
1813
+ h('div', { key: r.owner + '/' + r.name, className: 'dgs-card dgs-repo', onClick: () => nav('/r/' + r.owner + '/' + r.name), style: { cursor: 'pointer' } },
1814
+ h('div', { className: 'dgs-row' },
1815
+ h('span', { className: 'dgs-name' }, r.owner + '/' + r.name),
1816
+ h('span', { style: { flex: 1 } }),
1817
+ h('span', { className: 'dgs-sub' }, '★ ' + (r.stars || 0) + ' ⑂ ' + (r.forks || 0))),
1818
+ r.description ? h('div', { className: 'dgs-sub' }, r.description) : null,
1819
+ h('div', { className: 'dgs-sub' }, r.updated ? '最后更新于 ' + timeAgo(r.updated * 1000) : '')))),
1820
+ tab === 'users' && (users === null ? h('div', { className: 'dgs-empty' }, t('loading'))
1821
+ : users.length === 0 ? h('div', { className: 'dgs-empty' }, '—')
1822
+ : users.map((u) =>
1823
+ h('div', { key: u.name, className: 'dgs-card dgs-repo', onClick: () => nav('/u/' + u.name), style: { cursor: 'pointer' } },
1824
+ h('div', { className: 'dgs-row' },
1825
+ h('span', { className: 'dgs-name' }, u.name),
1826
+ u.full_name ? h('span', { className: 'dgs-sub', style: { marginLeft: 8 } }, u.full_name) : null),
1827
+ h('div', { className: 'dgs-sub' }, '关注者 - 关注中'))))))
1828
+ }
1829
+
1830
+ // ── 组织 ───────────────────────────────────────────────────────────────────
1831
+
1832
+ function Orgs({ t }) {
1833
+ const [list, setList] = useState(null)
1834
+ const [name, setName] = useState('')
1835
+ const [msg, setMsg] = useState('')
1836
+ const reload = () => api('GET', '/dsh/orgs').then((d) => setList(d.data || d || []))
1837
+ useEffect(() => { reload() }, [])
1838
+ return h('div', null,
1839
+ h('div', { className: 'dgs-row', style: { margin: '8px 0 12px' } },
1840
+ h('span', { style: { fontWeight: 700 } }, '我的组织'),
1841
+ h('input', { className: 'dgs-input', style: { maxWidth: 200 }, placeholder: '新组织名', value: name, onChange: (e) => setName(e.target.value) }),
1842
+ h('button', { className: 'dgs-btn', disabled: !name.trim(),
1843
+ onClick: async () => {
1844
+ const d = await api('POST', '/dsh/orgs', { name })
1845
+ d.ok ? (setMsg(''), reload()) : setMsg(d.error || 'failed')
1846
+ } }, '创建')),
1847
+ msg ? h('div', { className: 'dgs-err' }, msg) : null,
1848
+ list === null ? h('div', { className: 'dgs-empty' }, t('loading'))
1849
+ : list.length === 0 ? h('div', { className: 'dgs-empty' }, '—')
1850
+ : list.map((o) =>
1851
+ h('div', { key: o.name || o, className: 'dgs-card' },
1852
+ h('a', { className: 'dgs-name', href: '#/org/' + (o.name || o) }, o.name || o))))
1853
+ }
1854
+
1855
+ function OrgView({ name, t }) {
1856
+ const [org, setOrg] = useState(null)
1857
+ const [form, setForm] = useState({ fullName: '', description: '' })
1858
+ const [busy, setBusy] = useState(false)
1859
+ const [msg, setMsg] = useState('')
1860
+ const reload = useCallback(() => {
1861
+ api('GET', '/dsh/orgs/' + encodeURIComponent(name)).then((d) => {
1862
+ const o = d.data || d
1863
+ if (o && o.name) { setOrg(o); setForm({ fullName: o.fullName || '', description: o.description || '' }) }
1864
+ else setMsg((d && d.error) || t('loadFailed'))
1865
+ })
1866
+ }, [name])
1867
+ useEffect(() => { reload() }, [reload])
1868
+ if (org === null) return h('div', { className: 'dgs-empty' }, t('loading'))
1869
+ return h('div', null,
1870
+ h('div', { className: 'dgs-row', style: { marginBottom: 12 } },
1871
+ h('span', { className: 'dgs-h1' }, name),
1872
+ org.fullName ? h('span', { className: 'dgs-sub' }, org.fullName) : null),
1873
+ msg ? h('div', { className: 'dgs-err' }, msg) : null,
1874
+ h('div', { className: 'dgs-card' },
1875
+ h('div', { style: { fontWeight: 700, marginBottom: 8 } }, '成员'),
1876
+ (org.members || []).map((m) =>
1877
+ h('div', { key: m.name, className: 'dgs-row' },
1878
+ h('a', { className: 'dgs-name', href: '#/u/' + m.name }, m.name),
1879
+ m.owner ? h('span', { className: 'dgs-badge' }, 'owner') : null))),
1880
+ h('div', { className: 'dgs-card' },
1881
+ h('div', { style: { fontWeight: 700, marginBottom: 8 } }, '仓库'),
1882
+ (org.repos || []).length === 0 ? h('div', { className: 'dgs-sub' }, '—')
1883
+ : org.repos.map((r) =>
1884
+ h('div', { key: r.name }, h('a', { className: 'dgs-name', href: '#/r/' + name + '/' + r.name }, r.name)))),
1885
+ h('div', { className: 'dgs-card' },
1886
+ h('div', { style: { fontWeight: 700, marginBottom: 8 } }, '组织设置'),
1887
+ h('div', { className: 'dgs-row' },
1888
+ h('span', { className: 'dgs-sub', style: { minWidth: 50 } }, '名称'),
1889
+ h('input', { className: 'dgs-input', value: form.fullName, onChange: (e) => setForm({ ...form, fullName: e.target.value }) })),
1890
+ h('div', { className: 'dgs-row' },
1891
+ h('span', { className: 'dgs-sub', style: { minWidth: 50 } }, '描述'),
1892
+ h('input', { className: 'dgs-input', value: form.description, onChange: (e) => setForm({ ...form, description: e.target.value }) })),
1893
+ h('div', { className: 'dgs-row', style: { marginTop: 8 } },
1894
+ h('button', { className: 'dgs-btn', disabled: busy,
1895
+ onClick: async () => {
1896
+ setBusy(true); setMsg('')
1897
+ const d = await api('PATCH', '/dsh/orgs/' + encodeURIComponent(name), form)
1898
+ setBusy(false)
1899
+ d && d.ok ? (setMsg('✓ 已保存'), reload()) : setMsg((d && d.error) || 'failed')
1900
+ } }, t('save')))))
1901
+ }
1902
+
1903
+ // ── 用户 profile ───────────────────────────────────────────────────────────
1904
+
1905
+ function UserProfile({ name, t }) {
1906
+ const [profile, setProfile] = useState(null)
1907
+ useEffect(() => {
1908
+ api('GET', '/dsh/users/' + encodeURIComponent(name)).then((d) => setProfile(d.data || d))
1909
+ }, [name])
1910
+ if (profile === null) return h('div', { className: 'dgs-empty' }, t('loading'))
1911
+ return h('div', { className: 'dgs-settings-layout' },
1912
+ h('div', { className: 'dgs-settings-nav', style: { width: 230 } },
1913
+ h('div', { style: { width: 64, height: 64, borderRadius: 10, background: 'var(--dsw-alias-bg-layer-2)', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 26, fontWeight: 700, color: 'var(--dsw-alias-label-secondary)', marginBottom: 10 } },
1914
+ (name || '?').slice(0, 1).toUpperCase()),
1915
+ h('div', { className: 'dgs-h1', style: { fontSize: 17 } }, name),
1916
+ profile.isAdmin ? h('span', { className: 'dgs-badge ok', style: { marginTop: 4 } }, 'admin') : null,
1917
+ h('div', { className: 'dgs-sub', style: { marginTop: 10 } }, profile.email ? '✉ ' + profile.email : ''),
1918
+ h('div', { className: 'dgs-sub', style: { marginTop: 4 } }, '🕐 加入于 ' + (profile.created ? new Date(profile.created * 1000).toLocaleDateString() : '')),
1919
+ h('div', { className: 'dgs-sub', style: { marginTop: 10 } },
1920
+ `${profile.followers.length} 关注者 - ${profile.following.length} 关注中`)),
1921
+ h('div', { style: { flex: 1, minWidth: 0 } },
1922
+ h('div', { style: { fontWeight: 700, margin: '4px 0 10px' } }, '仓库'),
1923
+ profile.repos.length === 0 ? h('div', { className: 'dgs-empty' }, '—') : null,
1924
+ profile.repos.map((r) =>
1925
+ h('div', { key: r.name, className: 'dgs-card dgs-repo', onClick: () => nav('/r/' + r.owner + '/' + r.name), style: { cursor: 'pointer' } },
1926
+ h('div', { className: 'dgs-row' },
1927
+ h('span', { className: 'dgs-name' }, r.owner + '/' + r.name),
1928
+ h('span', { style: { flex: 1 } }),
1929
+ h('span', { className: 'dgs-sub' }, '★ ' + (r.stars || 0) + ' ⑂ ' + (r.forks || 0))),
1930
+ r.description ? h('div', { className: 'dgs-sub' }, r.description) : null,
1931
+ h('div', { className: 'dgs-sub' }, r.updated ? '最后更新于 ' + timeAgo(r.updated * 1000) : ''))),
1932
+ h('div', { style: { fontWeight: 700, margin: '14px 0 10px' } }, '动态'),
1933
+ profile.activity.length === 0 ? h('div', { className: 'dgs-empty' }, '—') : null,
1934
+ profile.activity.map((a, i) =>
1935
+ h('div', { key: i, className: 'dgs-card', style: { padding: '8px 14px' } },
1936
+ h('div', { className: 'dgs-row' },
1937
+ h('span', { style: { flex: 1 } }, activityText(a)),
1938
+ h('span', { className: 'dgs-sub' }, timeAgo(a.date * 1000)))))))
1939
+ }
1940
+
1941
+ // ── 管理面板 ───────────────────────────────────────────────────────────────
1942
+
1943
+ function Admin({ t }) {
1944
+ const [sub, setSub] = useState('panel')
1945
+ const [info, setInfo] = useState(null)
1946
+ const [data, setData] = useState(null)
1947
+ const [op, setOp] = useState('')
1948
+ const [opMsg, setOpMsg] = useState('')
1949
+ const [busy, setBusy] = useState(false)
1950
+ useEffect(() => {
1951
+ api('GET', '/dsh/admin/sysinfo').then((d) => setInfo(d))
1952
+ api('GET', '/dsh/admin/summary').then((d) => setData((x) => ({ ...(x || {}), summary: d })))
1953
+ api('GET', '/dsh/admin/users').then((d) => setData((x) => ({ ...(x || {}), users: d })))
1954
+ api('GET', '/dsh/admin/repos').then((d) => setData((x) => ({ ...(x || {}), repos: d })))
1955
+ api('GET', '/dsh/admin/orgs').then((d) => setData((x) => ({ ...(x || {}), orgs: d })))
1956
+ }, [])
1957
+ const navItem = (k, label) => h('span', { key: k, className: 'item' + (sub === k ? ' active' : ''), onClick: () => setSub(k) }, label)
1958
+ const card = (title, ...children) => h('div', { className: 'dgs-card', style: { marginBottom: 12 } },
1959
+ h('div', { style: { fontWeight: 700, marginBottom: 8, paddingBottom: 8, borderBottom: '1px solid var(--dsw-alias-border-l2)' } }, title),
1960
+ ...children)
1961
+ const kv = (k, v) => h('div', { key: k, className: 'dgs-row' },
1962
+ h('span', { style: { flex: 1 } }, k), h('span', { className: 'dgs-sub' }, String(v)))
1963
+ const summary = data && data.summary
1964
+ const panel = h('div', null,
1965
+ info ? card('构建信息',
1966
+ kv('应用程序版本', info.version),
1967
+ kv('Git 版本', info.gitVersion),
1968
+ kv('Node 版本', info.nodeVersion),
1969
+ kv('服务运行时间', Math.floor(info.uptimeSec / 3600) + ' 小时 ' + Math.floor((info.uptimeSec % 3600) / 60) + ' 分钟')) : null,
1970
+ summary ? card('应用统计数据',
1971
+ h('div', { className: 'dgs-sub' },
1972
+ `数据库统计:${summary.users} 位用户,${summary.orgs} 个组织,${summary.repos} 个仓库,${summary.issues} 张工单,${summary.pulls} 个合并请求。`)) : null,
1973
+ info ? card('系统监视状态',
1974
+ kv('RSS 内存', (info.mem.rss / 1048576).toFixed(1) + ' MB'),
1975
+ kv('Heap 已用', (info.mem.heapUsed / 1048576).toFixed(1) + ' MB'),
1976
+ kv('Heap 总量', (info.mem.heapTotal / 1048576).toFixed(1) + ' MB')) : null,
1977
+ card('管理员操作',
1978
+ h('div', { className: 'dgs-row' },
1979
+ h('select', { className: 'dgs-input', value: op, onChange: (e) => setOp(e.target.value) },
1980
+ h('option', { value: '' }, '请选择要运行的操作'),
1981
+ h('option', { value: 'sync_hooks' }, '同步所有仓库的 Git 钩子'),
1982
+ h('option', { value: 'gc' }, '对所有仓库运行垃圾回收 (git gc)')),
1983
+ h('button', { className: 'dgs-btn', disabled: busy || !op, onClick: async () => {
1984
+ setBusy(true); setOpMsg('')
1985
+ const d = await api('POST', '/dsh/admin/ops', { op })
1986
+ setBusy(false)
1987
+ setOpMsg(d && d.ok ? `✓ 完成(${d.n} 个仓库)` : (d && d.error) || '失败')
1988
+ } }, '执行')),
1989
+ opMsg ? h('div', { className: 'dgs-sub', style: { marginTop: 8 } }, opMsg) : null))
1990
+ const usersTab = data && data.users ? card('用户管理',
1991
+ h('table', { className: 'dgs-table' },
1992
+ h('tbody', null, data.users.map((u) =>
1993
+ h('tr', { key: u.name },
1994
+ h('td', null, h('a', { className: 'dgs-name', href: '#/u/' + u.name }, u.name), u.isAdmin ? h('span', { className: 'dgs-badge ok', style: { marginLeft: 8 } }, 'admin') : null),
1995
+ h('td', { className: 'dgs-sub' }, u.email || ''),
1996
+ h('td', { className: 'dgs-sub', style: { textAlign: 'right' } }, timeAgo((u.created || 0) * 1000))))))) : h('div', { className: 'dgs-empty' }, t('loading'))
1997
+ const reposTab = data && data.repos ? card('仓库管理',
1998
+ h('table', { className: 'dgs-table' },
1999
+ h('tbody', null, data.repos.map((r) =>
2000
+ h('tr', { key: r.name },
2001
+ h('td', null, h('a', { className: 'dgs-name', href: '#/r/' + r.name }, r.name), r.private ? h('span', { className: 'dgs-badge pri', style: { marginLeft: 8 } }, '私有') : null),
2002
+ h('td', { className: 'dgs-sub', style: { textAlign: 'right' } }, '★ ' + r.stars)))))) : h('div', { className: 'dgs-empty' }, t('loading'))
2003
+ const orgsTab = data && data.orgs ? card('组织管理',
2004
+ data.orgs.length === 0 ? h('div', { className: 'dgs-sub' }, '—')
2005
+ : h('table', { className: 'dgs-table' },
2006
+ h('tbody', null, data.orgs.map((o) =>
2007
+ h('tr', { key: o.name },
2008
+ h('td', null, h('a', { className: 'dgs-name', href: '#/org/' + o.name }, o.name), o.fullName ? h('span', { className: 'dgs-sub', style: { marginLeft: 8 } }, o.fullName) : null),
2009
+ h('td', { className: 'dgs-sub', style: { textAlign: 'right' } }, timeAgo(o.created * 1000))))))) : h('div', { className: 'dgs-empty' }, t('loading'))
2010
+ return h('div', { className: 'dgs-settings-layout' },
2011
+ h('div', { className: 'dgs-settings-nav' },
2012
+ navItem('panel', '控制面板'),
2013
+ navItem('users', '用户管理'),
2014
+ navItem('repos', '仓库管理'),
2015
+ navItem('orgs', '组织管理')),
2016
+ h('div', { style: { flex: 1, minWidth: 0 } },
2017
+ sub === 'panel' ? panel : sub === 'users' ? usersTab : sub === 'repos' ? reposTab : orgsTab))
2018
+ }
2019
+
2020
+ // ── 主页(仓库列表 + 建仓) ────────────────────────────────────────────────
2021
+
2022
+ function GitPage({ onClose, t }) {
2023
+ useEffect(ensureStyles, [])
2024
+ const route = useHashRoute()
2025
+ const [me, setMe] = useState(null)
2026
+ const [err, setErr] = useState('')
2027
+ const [name, setName] = useState('')
2028
+ const [isPrivate, setIsPrivate] = useState(false)
2029
+ const [busy, setBusy] = useState(false)
2030
+ const [toast, setToast] = useState('')
2031
+ const reload = useCallback(() => {
2032
+ setMe(null); setErr('')
2033
+ api('GET', '/me').then((d) => d.ok ? setMe(d) : setErr(d.error || 'failed'))
2034
+ }, [])
2035
+ useEffect(reload, [reload])
2036
+ useEffect(() => {
2037
+ const onKey = (e) => { if (e.key === 'Escape') onClose() }
2038
+ window.addEventListener('keydown', onKey)
2039
+ return () => window.removeEventListener('keydown', onKey)
2040
+ }, [onClose])
2041
+ const notify = (m) => { setToast(m); setTimeout(() => setToast(''), 1600) }
2042
+ const seg = route.split('/').filter(Boolean)
2043
+ const view = seg[0] || 'repos'
2044
+ const repoRoute = view === 'r' && seg[1] && seg[2] ? { owner: seg[1], name: seg[2] } : null
2045
+ const body = err ? h('div', { className: 'dgs-card dgs-err' }, err,
2046
+ h('button', { className: 'dgs-btn ghost', style: { marginLeft: 12 }, onClick: reload }, t('retry')))
2047
+ : repoRoute ? h(RepoBrowser, { repo: repoRoute, t, onBack: () => nav('/repos') })
2048
+ : view === 'explore' ? h(Explore, { t })
2049
+ : view === 'orgs' ? h(Orgs, { t })
2050
+ : view === 'org' && seg[1] ? h(OrgView, { name: seg[1], t })
2051
+ : view === 'u' && seg[1] ? h(UserProfile, { name: decodeURIComponent(seg[1]), t })
2052
+ : view === 'admin' ? h(Admin, { t })
2053
+ : me === null ? h('div', { className: 'dgs-empty' }, t('loading'))
2054
+ : h('div', null,
2055
+ h('div', { className: 'dgs-card' },
2056
+ h('div', { className: 'dgs-row' },
2057
+ h('span', { style: { fontWeight: 700 } }, t('newRepo')),
2058
+ h('input', { className: 'dgs-input', placeholder: t('repoName'), value: name,
2059
+ onChange: (e) => setName(e.target.value),
2060
+ onKeyDown: (e) => { if (e.key === 'Enter' && name.trim()) create() } }),
2061
+ h('label', { className: 'dgs-sub', style: { display: 'flex', gap: 4, alignItems: 'center' } },
2062
+ h('input', { type: 'checkbox', checked: isPrivate, onChange: (e) => setIsPrivate(e.target.checked) }), t('private')),
2063
+ h('button', { className: 'dgs-btn', disabled: busy || !name.trim(), onClick: create }, t('create')))),
2064
+ h('div', { style: { height: 10 } }),
2065
+ h('div', { style: { fontWeight: 700, margin: '4px 0 10px' } }, `${t('myRepos')}(${me.repos.length})`),
2066
+ me.repos.length === 0 ? h('div', { className: 'dgs-empty' }, t('emptyRepo'))
2067
+ : h('div', null, me.repos.map((r) => repoCardEl(r))))
2068
+
2069
+ function repoCardEl(r) {
2070
+ const title = h('a', { className: 'dgs-name', onClick: (e) => { e.stopPropagation(); nav('/r/' + r.owner + '/' + r.name) } },
2071
+ r.fullName || (r.owner + '/' + r.name))
2072
+ const badge = r.private ? h('span', { className: 'dgs-badge pri' }, t('private')) : null
2073
+ const stats = h('span', { className: 'dgs-sub' }, '★ ' + r.stars + ' · ' + t('issues') + ' ' + r.issues)
2074
+ const del = h('button', { className: 'dgs-btn danger', onClick: (e) => {
2075
+ e.stopPropagation()
2076
+ if (!confirm(t('deleteConfirm') + ' ' + r.name + ' ?')) return
2077
+ api('DELETE', '/repos/' + r.owner + '/' + r.name).then((d) => { d.ok ? reload() : notify(d.error || 'failed') })
2078
+ } }, t('delete'))
2079
+ const desc = r.description ? h('div', { className: 'dgs-sub' }, r.description) : null
2080
+ return h('div', { key: r.id, className: 'dgs-card dgs-repo', onClick: () => nav('/r/' + r.owner + '/' + r.name) },
2081
+ h('div', { className: 'dgs-row' }, title, badge, h('span', { style: { flex: 1 } }), stats, del), desc)
2082
+ }
2083
+
2084
+ const navItems = [['repos', t('myRepos')], ['explore', '探索'], ['orgs', '组织'], ['admin', '管理']]
2085
+ return h('div', { className: 'dgs-page' },
2086
+ h('div', { className: 'dgs-head' },
2087
+ h('span', { className: 'dgs-h1' }, t('title')),
2088
+ h('div', { className: 'dgs-row', style: { marginLeft: 8 } },
2089
+ navItems.map(([k, label]) =>
2090
+ h('button', { key: k, className: 'dgs-btn ghost',
2091
+ style: view === k ? { color: 'var(--dsw-alias-state-business-primary)', borderColor: 'var(--dsw-alias-state-business-primary)55' } : undefined,
2092
+ onClick: () => nav('/' + k) }, label))),
2093
+ h('button', { className: 'dgs-close', onClick: onClose, title: 'Esc' }, '✕')),
2094
+ h('div', { className: 'dgs-body' }, body),
2095
+ toast ? h('div', { className: 'dgs-toast' }, toast) : null)
2096
+
2097
+ async function create() {
2098
+ setBusy(true)
2099
+ const d = await api('POST', '/repos', { name, private: isPrivate, autoInit: true })
2100
+ setBusy(false)
2101
+ if (d.ok) { setName(''); notify('✓ ' + name); reload() } else notify(d.error || 'failed')
2102
+ }
2103
+ }
2104
+
2105
+ // ── 设置节(服务配置,无 iframe) ──────────────────────────────────────────
2106
+
2107
+ function SettingsSection({ t }) {
2108
+ const [status, setStatus] = useState(null)
2109
+ const [form, setForm] = useState({ enabled: false, host: '127.0.0.1', port: 3400, dataDir: '' })
2110
+ const [busy, setBusy] = useState(false)
2111
+ const [msg, setMsg] = useState('')
2112
+ useEffect(() => {
2113
+ fetch(API + '/status').then((r) => r.json()).then((d) => {
2114
+ setStatus(d)
2115
+ setForm({ enabled: !!d.enabled, host: d.host || '127.0.0.1', port: d.port || 3400, dataDir: d.dataDir || '' })
2116
+ }).catch(() => {})
2117
+ }, [])
2118
+ const save = async () => {
2119
+ setBusy(true); setMsg('…')
2120
+ try {
2121
+ const res = await fetch(API + '/settings', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(form) })
2122
+ const d = await res.json()
2123
+ setStatus(d); setMsg('')
2124
+ } catch (e) { setMsg(String(e)) } finally { setBusy(false) }
2125
+ }
2126
+ const field = (k) => ({ value: form[k], onChange: (e) => setForm({ ...form, [k]: e.target.value }) })
2127
+ return h('div', { className: 'dgs-settings' },
2128
+ h('div', { className: 'dgs-card' },
2129
+ h('div', { className: 'dgs-h1' }, t('title')),
2130
+ h('div', { className: 'dgs-row' },
2131
+ h('label', null, '启用'),
2132
+ h('input', { type: 'checkbox', checked: form.enabled, onChange: (e) => setForm({ ...form, enabled: e.target.checked }) })),
2133
+ h('div', { className: 'dgs-row' },
2134
+ h('label', null, '监听地址'), h('input', { className: 'dgs-input', ...field('host') })),
2135
+ h('div', { className: 'dgs-row' },
2136
+ h('label', null, '端口'), h('input', { className: 'dgs-input', type: 'number', ...field('port') })),
2137
+ h('div', { className: 'dgs-row' },
2138
+ h('label', null, '数据目录'), h('input', { className: 'dgs-input', placeholder: '~/.dsh/dsh-git-server/data', ...field('dataDir') })),
2139
+ status && status.umAvailable !== 'ok' ? h('div', { className: 'dgs-err' }, 'user-management 服务不可用(回退兜底账号)') : null,
2140
+ msg ? h('div', { className: 'dgs-sub' }, msg) : null,
2141
+ h('div', { className: 'dgs-row', style: { marginTop: 8 } },
2142
+ h('button', { className: 'dgs-btn', disabled: busy, onClick: save }, '保存'))))
2143
+ }
2144
+
2145
+ // ── 侧栏导航条目(工艺库/知识库下方,dsh-kb 同款 DOM 注入)───────────────
2146
+
2147
+ const GIT_ENTRY_ATTR = 'data-dsh-git-entry'
2148
+
2149
+ let gitPageHost = null
2150
+ function closeGitPage() {
2151
+ if (!gitPageHost) return
2152
+ try { gitPageHost.root.unmount() } catch {}
2153
+ try { gitPageHost.el.remove() } catch {}
2154
+ gitPageHost = null
2155
+ }
2156
+ function openGitPage(t) {
2157
+ if (gitPageHost) { closeGitPage(); return }
2158
+ const el = document.createElement('div')
2159
+ document.body.appendChild(el)
2160
+ const root = require('react-dom/client').createRoot(el)
2161
+ gitPageHost = { el, root }
2162
+ root.render(h(GitPage, { onClose: closeGitPage, t }))
2163
+ }
2164
+
2165
+ function gitSidebarRoot() {
2166
+ const column = document.querySelector('[data-pane="sidebar"], [class*="sidebarCol"], .dshDesktopUpstreamSidebar, .dshDesktopSidebarSurface')
2167
+ if (column === null) return undefined
2168
+ const logoOwner = column.querySelector('[class*="logoRow"]') && column.querySelector('[class*="logoRow"]').parentElement
2169
+ return logoOwner || (column.firstElementChild || undefined)
2170
+ }
2171
+
2172
+ function gitNewSessionButton(root) {
2173
+ const nested = root.querySelector('button[class*="newSession"]')
2174
+ if (nested) return nested
2175
+ for (const child of root.children) {
2176
+ if (child instanceof HTMLButtonElement && !child.matches('[' + GIT_ENTRY_ATTR + ']')) return child
2177
+ }
2178
+ return Array.from(root.querySelectorAll('button')).find((b) => !b.matches('[' + GIT_ENTRY_ATTR + ']') && /新会话|新建会话|new session/i.test(b.textContent || ''))
2179
+ }
2180
+
2181
+ function placeGitEntry(root, entry) {
2182
+ const button = gitNewSessionButton(root)
2183
+ if (!button) return false
2184
+ if (entry.parentElement !== root) {
2185
+ const family = Array.from(root.children).filter((el) => el instanceof HTMLElement
2186
+ && el.matches('[data-dsh-prc-entry],[data-dsh-atb-entry],[data-dsh-taskboard-entry],[data-dsh-ssh-entry],[data-dsh-kb-entry],[' + GIT_ENTRY_ATTR + ']'))
2187
+ if (family.length > 0) {
2188
+ const last = family[family.length - 1]
2189
+ last.parentElement.insertBefore(entry, last.nextSibling)
2190
+ } else {
2191
+ const row = button.closest('[class*="logoRow"]')
2192
+ const base = (row && row.parentElement === root) ? row : button
2193
+ root.insertBefore(entry, base.nextSibling)
2194
+ }
2195
+ }
2196
+ return true
2197
+ }
2198
+
2199
+ function mountGitSidebarEntry(t) {
2200
+ let style = document.getElementById('dsh-git-sidebar-style')
2201
+ if (!style) {
2202
+ style = document.createElement('style')
2203
+ style.id = 'dsh-git-sidebar-style'
2204
+ style.textContent = `
2205
+ .dsh-git-entry{display:flex;align-items:center;gap:8px;width:100%;height:34px;padding:0 10px;margin:2px 0 8px;border:0;border-radius:8px;background:transparent;color:var(--dsw-alias-label-primary,var(--dsw-text-primary,inherit));font:inherit;font-size:13px;cursor:pointer;text-align:left}
2206
+ .dsh-git-entry:hover{background:color-mix(in srgb,var(--dsw-alias-label-primary) 8%,transparent)}
2207
+ .dsh-git-entry .dsh-git-entry-icon{flex:none;line-height:1}
2208
+ .dsh-git-entry .dsh-git-entry-stats{margin-left:auto;display:inline-flex;align-items:center;gap:4px;font-size:11px;color:var(--dsw-alias-label-secondary,var(--dsw-text-secondary,gray));font-variant-numeric:tabular-nums;white-space:nowrap}
2209
+ .dsh-git-entry .dsh-git-entry-dot{width:6px;height:6px;border-radius:6px;background:var(--dsw-alias-state-success-primary);display:inline-block}
2210
+ [data-sidebar-collapsed] .dsh-git-entry,[class*="_collapsed"] .dsh-git-entry{width:36px;height:36px;min-width:36px;margin:0 0 12px;padding:0;justify-content:center;gap:0;text-align:center}
2211
+ [data-sidebar-collapsed] .dsh-git-entry .dsh-git-entry-label,[data-sidebar-collapsed] .dsh-git-entry .dsh-git-entry-stats,[class*="_collapsed"] .dsh-git-entry .dsh-git-entry-label,[class*="_collapsed"] .dsh-git-entry .dsh-git-entry-stats{display:none}
2212
+ `
2213
+ document.head.appendChild(style)
2214
+ }
2215
+ const entry = document.createElement('button')
2216
+ entry.type = 'button'
2217
+ entry.setAttribute(GIT_ENTRY_ATTR, '')
2218
+ entry.className = 'dsh-git-entry'
2219
+ entry.title = '代码仓库 — 仓库 / 工单 / PR / Wiki / 发版'
2220
+ entry.innerHTML = '<span class="dsh-git-entry-icon">📦</span><span class="dsh-git-entry-label">代码仓库</span><span class="dsh-git-entry-stats"></span>'
2221
+ entry.addEventListener('click', () => openGitPage(t))
2222
+ const stats = entry.querySelector('.dsh-git-entry-stats')
2223
+ const refreshStats = () => {
2224
+ fetch(API + '/me').then((r) => r.json()).then((d) => {
2225
+ if (stats && d && Array.isArray(d.repos)) stats.textContent = String(d.repos.length)
2226
+ }).catch(() => {})
2227
+ }
2228
+ refreshStats()
2229
+ const poll = setInterval(refreshStats, 30000)
2230
+ let root
2231
+ let placed = false
2232
+ const rootObserver = new MutationObserver(() => {
2233
+ if (!root || !root.isConnected) { placed = false; tryPlace(); return }
2234
+ if (!root.contains(entry)) placed = placeGitEntry(root, entry)
2235
+ })
2236
+ const tryPlace = () => {
2237
+ if (root && !root.isConnected) { rootObserver.disconnect(); root = undefined; placed = false }
2238
+ if (placed) { if (document.body.contains(entry)) return; rootObserver.disconnect(); root = undefined; placed = false }
2239
+ root = root || gitSidebarRoot()
2240
+ if (!root) return
2241
+ placed = placeGitEntry(root, entry)
2242
+ if (placed) rootObserver.observe(root, { childList: true })
2243
+ }
2244
+ const waitObserver = new MutationObserver(() => tryPlace())
2245
+ waitObserver.observe(document.body, { childList: true, subtree: true })
2246
+ const retry = setInterval(tryPlace, 2000)
2247
+ tryPlace()
2248
+ return () => {
2249
+ clearInterval(retry)
2250
+ clearInterval(poll)
2251
+ waitObserver.disconnect()
2252
+ rootObserver.disconnect()
2253
+ try { entry.remove() } catch {}
2254
+ closeGitPage()
2255
+ }
2256
+ }
2257
+
2258
+ // ── utils ─────────────────────────────────────────────────────────────────
2259
+
2260
+ function activityText(a) {
2261
+ const repo = a.repo || ''
2262
+ const ref = a.ref || ''
2263
+ const m = {
2264
+ 1: '创建了仓库 ' + repo, 2: '重命名仓库 ' + repo, 3: '点赞了仓库 ' + repo, 4: '关注了仓库 ' + repo,
2265
+ 5: '推送提交到 ' + repo + (ref ? '(' + ref + ')' : ''), 6: '创建了工单 · ' + (a.content || ''),
2266
+ 7: '发起合并请求 · ' + (a.content || ''), 8: '转移仓库 ' + repo, 9: '发布标签 ' + ref,
2267
+ 10: '评论了工单 · ' + (a.content || ''), 11: '合并了合并请求 · ' + (a.content || ''),
2268
+ 12: '关闭了工单 · ' + (a.content || ''), 13: '重开工单 · ' + (a.content || ''),
2269
+ 14: '关闭了合并请求 · ' + (a.content || ''), 15: '重开合并请求 · ' + (a.content || ''),
2270
+ 16: '创建分支 ' + ref + ' 于 ' + repo, 17: '删除分支 ' + ref + ' 于 ' + repo,
2271
+ 18: '删除标签 ' + ref, 19: '复刻了仓库 ' + repo, 20: '同步镜像 ' + repo,
2272
+ }
2273
+ return m[a.type] || ('活动于 ' + repo)
2274
+ }
2275
+
2276
+ function fmtSize(n) {
2277
+ if (n == null) return ''
2278
+ if (n < 1024) return n + ' B'
2279
+ if (n < 1048576) return (n / 1024).toFixed(1) + ' KB'
2280
+ return (n / 1048576).toFixed(1) + ' MB'
2281
+ }
2282
+ function fmtDate(s) {
2283
+ if (!s) return ''
2284
+ try { return new Date(s).toLocaleString() } catch { return s }
2285
+ }
2286
+ function timeAgo(ms) {
2287
+ const t = typeof ms === 'string' ? new Date(ms).getTime() : ms
2288
+ if (!t || Number.isNaN(t)) return ''
2289
+ const sec = Math.max(0, Math.floor((Date.now() - t) / 1000))
2290
+ if (sec < 60) return '刚刚'
2291
+ if (sec < 3600) return Math.floor(sec / 60) + ' 分钟之前'
2292
+ if (sec < 86400) return Math.floor(sec / 3600) + ' 小时之前'
2293
+ if (sec < 86400 * 30) return Math.floor(sec / 86400) + ' 天之前'
2294
+ if (sec < 86400 * 365) return Math.floor(sec / 86400 / 30) + ' 个月之前'
2295
+ return Math.floor(sec / 86400 / 365) + ' 年之前'
2296
+ }
2297
+
2298
+ // ── plugin plane ──────────────────────────────────────────────────────────
2299
+
2300
+ const CLIENT_NAME = '@weibaohui/dsh-git-server'
2301
+
2302
+ module.exports = {
2303
+ name: CLIENT_NAME,
2304
+ inject: ['slots', 'locale'],
2305
+ __internals: { NS, ZH, EN, GitPage, SettingsSection },
2306
+ __boot(container, opts = {}) {
2307
+ ensureStyles()
2308
+ let t = opts.t || ((key) => ZH[key] ?? EN[key] ?? key)
2309
+ const root = require('react-dom/client').createRoot(container)
2310
+ root.render(h(GitPage, { onClose: () => {}, t }))
2311
+ return root
2312
+ },
2313
+ apply(ctx) {
2314
+ let t = (key) => ZH[key] ?? EN[key] ?? key
2315
+ try {
2316
+ if (ctx.locale && typeof ctx.locale.register === 'function') {
2317
+ ctx.locale.register(NS, 'zh', ZH)
2318
+ ctx.locale.register(NS, 'en', EN)
2319
+ const bound = typeof ctx.locale.bind === 'function' ? ctx.locale.bind(NS) : null
2320
+ if (bound) t = (key) => bound(key) || (ZH[key] ?? EN[key] ?? key)
2321
+ }
2322
+ } catch (e) { try { console.error('[dsh-git-server] locale init:', e) } catch {} }
2323
+
2324
+ ctx.effect(() => mountGitSidebarEntry(t), 'dsh-git-server: sidebar entry')
2325
+
2326
+ ctx.effect(() => {
2327
+ try {
2328
+ ctx.slots.inject('settings.section', () => ctx.slots.register({
2329
+ name: 'settings.section',
2330
+ id: CLIENT_NAME,
2331
+ order: 97,
2332
+ locale: NS,
2333
+ label: () => t('title'),
2334
+ inject: () => ({}),
2335
+ }, function SettingsSectionSlot() {
2336
+ useEffect(ensureStyles, [])
2337
+ return h(SettingsSection, { t })
2338
+ }))
2339
+ } catch (e) { (globalThis.__skErrors = globalThis.__skErrors || []).push('settings:' + (e && e.message)); throw e }
2340
+ }, 'dsh-git-server: settings section')
2341
+ },
2342
+ }