@weibaohui/dsh-git-server 0.1.3 → 0.2.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.
- package/README.md +71 -2
- package/client/bundle.js +657 -75
- package/client/index.js +657 -75
- package/package.json +1 -1
- package/server/dist/api/v1.js +71 -3
- package/server/dist/dshapi.js +558 -66
- package/server/dist/gitx/git.js +39 -3
- package/server/dist/gitx/service.js +4 -3
- package/server/dist/hook.js +31 -2
- package/server/dist/notify.js +55 -0
- package/src/index.js +12 -1
package/client/bundle.js
CHANGED
|
@@ -12,8 +12,8 @@ window.__ModuleLoader__.load({
|
|
|
12
12
|
* @weibaohui/dsh-git-server — Browser half(原生 dsh 插件 UI,无 iframe/桥接)。
|
|
13
13
|
*
|
|
14
14
|
* 两个面:
|
|
15
|
-
* 1. sidebar.footer.action「Git」→
|
|
16
|
-
* 仓库浏览:文件树/文件内容/提交/分支/工单[列表/新建/评论/开关])。
|
|
15
|
+
* 1. sidebar.footer.action「Git」→ 中栏接管管理页(taskboard 同款,盖会话列不盖侧栏;
|
|
16
|
+
* 仓库列表/建仓/删除 + 仓库浏览:文件树/文件内容/提交/分支/工单[列表/新建/评论/开关])。
|
|
17
17
|
* 2. settings.section「Git 服务器」→ 服务配置(启停/端口/数据目录/兜底密码)。
|
|
18
18
|
*
|
|
19
19
|
* 数据通道:宿主同源路由 /dsh-git-server/api/*(dsh 登录门禁保护,
|
|
@@ -37,6 +37,12 @@ window.__ModuleLoader__.load({
|
|
|
37
37
|
// ── locale ────────────────────────────────────────────────────────────────
|
|
38
38
|
|
|
39
39
|
const NS = 'dshGitServer'
|
|
40
|
+
// 中栏接管(taskboard 同款)开合属性 + 互斥协议
|
|
41
|
+
const DGS_ACTIVE_ATTR = 'data-dsh-git-active'
|
|
42
|
+
const DGS_PANEL_NAME = 'dsh-git'
|
|
43
|
+
const DGS_ACTIVATE_EVENT = 'dsh-panel-activate'
|
|
44
|
+
const DGS_OTHER_ACTIVE_ATTRS = ['data-dsh-atb-active', 'data-dsh-taskboard-active', 'data-dsh-ssh-active', 'data-dsh-prc-active', 'data-dsh-kb-active']
|
|
45
|
+
const DGS_SIDEBAR_ROW_SELECTOR = '[class*="sessionRow"], [class*="projectRow"], [class*="searchResultRow"], [class*="newSession"]'
|
|
40
46
|
const ZH = {
|
|
41
47
|
nav: 'Git', title: '代码仓库',
|
|
42
48
|
myRepos: '我的仓库', newRepo: '新建仓库', repoName: '仓库名', private: '私有',
|
|
@@ -94,7 +100,16 @@ window.__ModuleLoader__.load({
|
|
|
94
100
|
holder.id = 'dgs-styles'
|
|
95
101
|
holder.style.display = 'none'
|
|
96
102
|
holder.innerHTML = `<style>
|
|
97
|
-
|
|
103
|
+
/* 中栏接管视图(taskboard 同款):容器挂进会话列末尾,html 属性驱动开合,只隐藏列内兄弟。
|
|
104
|
+
* 三代壳层选择器:dev shell data-pane / 官方 CSS-Module centerCol / Desktop 扩展框。 */
|
|
105
|
+
.dsh-git-view{display:none}
|
|
106
|
+
html[data-dsh-git-active] [data-pane="conversation"] > *:not([data-dsh-git-view]),
|
|
107
|
+
html[data-dsh-git-active] [class*="centerCol"] > *:not([data-dsh-git-view]),
|
|
108
|
+
html[data-dsh-git-active] .dshDesktopConversationSurface > *:not([data-dsh-git-view]){display:none !important}
|
|
109
|
+
html[data-dsh-git-active] .dsh-git-view{display:flex;flex-direction:column;height:100%;overflow:hidden}
|
|
110
|
+
/* 壳层退化(会话列缺席):容器兜底挂 body 时退回全屏浮层,保证入口点击永远有响应 */
|
|
111
|
+
.dsh-git-view[data-dsh-git-fallback]{position:fixed;inset:0;z-index:2147483000;display:flex}
|
|
112
|
+
.dgs-page{position:relative;flex:1;min-height:0;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}
|
|
98
113
|
.dgs-head{display:flex;align-items:center;gap:12px;padding:12px 28px;border-bottom:1px solid var(--dsw-alias-border-l2);flex:none}
|
|
99
114
|
.dgs-h1{font-size:16px;font-weight:700;margin:0}
|
|
100
115
|
.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}
|
|
@@ -203,6 +218,24 @@ window.__ModuleLoader__.load({
|
|
|
203
218
|
.dgs-md blockquote{border-left:3px solid var(--dsw-alias-border-l2);margin:8px 0;padding:2px 12px;color:var(--dsw-alias-label-secondary)}
|
|
204
219
|
.dgs-md table{border-collapse:collapse}
|
|
205
220
|
.dgs-md td,.dgs-md th{border:1px solid var(--dsw-alias-border-l2);padding:4px 10px}
|
|
221
|
+
/* 语法高亮(亮暗双主题可读配色) */
|
|
222
|
+
.tok-k{color:#cf222e;font-weight:500}
|
|
223
|
+
.tok-s{color:#1a7f37}
|
|
224
|
+
.tok-c{color:#6e7781;font-style:italic}
|
|
225
|
+
.tok-n{color:#0550ae}
|
|
226
|
+
.tok-b{color:#953800}
|
|
227
|
+
@media (prefers-color-scheme: dark){
|
|
228
|
+
.tok-k{color:#ff7b72}
|
|
229
|
+
.tok-s{color:#7ee787}
|
|
230
|
+
.tok-c{color:#8b949e}
|
|
231
|
+
.tok-n{color:#79c0ff}
|
|
232
|
+
.tok-b{color:#ffa657}
|
|
233
|
+
}
|
|
234
|
+
.dgs-hit{padding:4px 10px;border-bottom:1px solid var(--dsw-alias-border-l1);cursor:pointer}
|
|
235
|
+
.dgs-hit:hover{background:var(--dsw-alias-interactive-bg-hover)}
|
|
236
|
+
.dgs-hit-code{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px;color:var(--dsw-alias-label-secondary);white-space:pre-wrap;word-break:break-all}
|
|
237
|
+
.dgs-notif-dot{display:inline-block;min-width:16px;height:16px;line-height:16px;text-align:center;border-radius:999px;background:var(--dsw-alias-state-error-primary);color:#fff;font-size:10px;font-weight:700;padding:0 4px;margin-left:4px}
|
|
238
|
+
.dgs-guide{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12.5px;background:var(--dsw-alias-markdown-code-block);border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:10px 14px;margin:6px 0;white-space:pre-wrap;word-break:break-all}
|
|
206
239
|
</style>`
|
|
207
240
|
document.head.appendChild(holder)
|
|
208
241
|
}
|
|
@@ -232,25 +265,133 @@ window.__ModuleLoader__.load({
|
|
|
232
265
|
}
|
|
233
266
|
const nav = (hash) => { location.hash = hash }
|
|
234
267
|
|
|
268
|
+
// ── 语法高亮(零依赖:无 bundler,客户端不能 import,内置一个轻量 tokenizer) ──
|
|
269
|
+
|
|
270
|
+
function escapeHtml(s) {
|
|
271
|
+
return String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"')
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const HL = (() => {
|
|
275
|
+
const KW = {
|
|
276
|
+
clike: 'abstract assert boolean break byte case catch char class const continue default do double else enum extends final finally float for goto if implements import instanceof int interface long native new package private protected public return short static strictfp super switch synchronized this throw throws transient try void volatile while true false null',
|
|
277
|
+
js: 'const let var function return if else for while do switch case break continue class extends new delete typeof instanceof in of async await yield import export from default try catch finally throw this super static get set null undefined true false',
|
|
278
|
+
py: 'def class return if elif else for while import from as with try except finally raise lambda pass break continue global nonlocal yield assert async await in is not and or del print None True False self cls',
|
|
279
|
+
go: 'func return if else for range switch case break continue go defer select chan map struct interface package import const var type fallthrough default nil true false iota',
|
|
280
|
+
rs: 'fn let mut pub struct enum impl trait for while loop if else match return use mod crate self Self super const static type where async await move ref break continue in as dyn unsafe extern true false None Some Ok Err',
|
|
281
|
+
sh: 'if then else elif fi for while until do done case esac function return local export readonly declare shift eval exec source alias unalias set unset trap exit break continue in true false echo printf cd test',
|
|
282
|
+
sql: 'select from where insert into values update set delete join inner left right outer on group by order having limit offset as asc desc distinct union all create table alter drop index primary key foreign references not null default and or like in is between exists case when then else end',
|
|
283
|
+
}
|
|
284
|
+
const C = { line: '//[^\\n]*', block: '\\/\\*[\\s\\S]*?\\*\\/' }
|
|
285
|
+
const HASH = { line: '#[^\\n]*' }
|
|
286
|
+
const STR = ['"(?:\\\\.|[^"\\\\\\n])*"', "'(?:\\\\.|[^'\\\\\\n])*'", '`(?:\\\\.|[^`\\\\])*`']
|
|
287
|
+
const NUM = '\\b\\d[\\d_]*(?:\\.[\\d_]+)?(?:[eE][+-]?\\d+)?\\b'
|
|
288
|
+
const langs = {
|
|
289
|
+
js: { kw: KW.js, c: C }, mjs: { kw: KW.js, c: C }, cjs: { kw: KW.js, c: C }, jsx: { kw: KW.js, c: C },
|
|
290
|
+
ts: { kw: KW.js + ' interface type namespace declare readonly keyof infer never unknown any string number boolean', c: C },
|
|
291
|
+
tsx: { kw: KW.js + ' interface type namespace declare readonly keyof infer never unknown any string number boolean', c: C },
|
|
292
|
+
json: { kw: 'true false null', c: null },
|
|
293
|
+
py: { kw: KW.py, c: HASH }, rb: { kw: KW.py, c: HASH }, pl: { kw: KW.py, c: HASH },
|
|
294
|
+
go: { kw: KW.go, c: C },
|
|
295
|
+
rs: { kw: KW.rs, c: C },
|
|
296
|
+
java: { kw: KW.clike, c: C }, kt: { kw: KW.clike, c: C }, scala: { kw: KW.clike, c: C },
|
|
297
|
+
c: { kw: KW.clike, c: C }, h: { kw: KW.clike, c: C }, cpp: { kw: KW.clike, c: C }, cc: { kw: KW.clike, c: C }, cxx: { kw: KW.clike, c: C }, cs: { kw: KW.clike, c: C }, mm: { kw: KW.clike, c: C }, m: { kw: KW.clike, c: C },
|
|
298
|
+
sh: { kw: KW.sh, c: HASH }, bash: { kw: KW.sh, c: HASH }, zsh: { kw: KW.sh, c: HASH },
|
|
299
|
+
yaml: { kw: 'true false null yes no on off', c: HASH }, yml: { kw: 'true false null yes no on off', c: HASH },
|
|
300
|
+
toml: { kw: 'true false', c: HASH }, ini: { kw: 'true false', c: HASH }, cfg: { kw: 'true false', c: HASH },
|
|
301
|
+
sql: { kw: KW.sql, c: { line: '--[^\\n]*', block: C.block } },
|
|
302
|
+
css: { kw: '', c: { block: C.block } }, scss: { kw: '', c: C }, less: { kw: '', c: C },
|
|
303
|
+
dockerfile: { kw: 'FROM RUN CMD ENTRYPOINT COPY ADD WORKDIR EXPOSE ENV ARG VOLUME USER LABEL HEALTHCHECK ONBUILD SHELL STOPSIGNAL', c: HASH },
|
|
304
|
+
makefile: { kw: '', c: HASH },
|
|
305
|
+
vue: { kw: KW.js, c: C }, svelte: { kw: KW.js, c: C },
|
|
306
|
+
}
|
|
307
|
+
const byName = { dockerfile: 'dockerfile', makefile: 'makefile', 'cmakelists.txt': 'makefile' }
|
|
308
|
+
const cache = new Map()
|
|
309
|
+
function langFor(name) {
|
|
310
|
+
const lower = (name || '').toLowerCase()
|
|
311
|
+
const ext = byName[lower] || lower.split('.').pop() || ''
|
|
312
|
+
return langs[ext] || null
|
|
313
|
+
}
|
|
314
|
+
function regexFor(cfg) {
|
|
315
|
+
if (cache.has(cfg)) return cache.get(cfg)
|
|
316
|
+
const parts = []
|
|
317
|
+
if (cfg.c && cfg.c.block) parts.push(cfg.c.block)
|
|
318
|
+
if (cfg.c && cfg.c.line) parts.push(cfg.c.line)
|
|
319
|
+
parts.push(...STR, NUM)
|
|
320
|
+
if (cfg.kw) parts.push('\\b(?:' + cfg.kw.split(' ').join('|') + ')\\b')
|
|
321
|
+
const rx = new RegExp(parts.join('|'), 'g')
|
|
322
|
+
cache.set(cfg, rx)
|
|
323
|
+
return rx
|
|
324
|
+
}
|
|
325
|
+
return { langFor, regexFor }
|
|
326
|
+
})()
|
|
327
|
+
|
|
328
|
+
/** 源码 → 带 token 着色的 HTML(输入已转义)。 */
|
|
329
|
+
function highlightCode(text, filename) {
|
|
330
|
+
const cfg = HL.langFor(filename)
|
|
331
|
+
if (!cfg) return escapeHtml(text)
|
|
332
|
+
const rx = HL.regexFor(cfg)
|
|
333
|
+
let out = ''
|
|
334
|
+
let last = 0
|
|
335
|
+
for (const m of text.matchAll(rx)) {
|
|
336
|
+
const i = m.index
|
|
337
|
+
if (i > last) out += escapeHtml(text.slice(last, i))
|
|
338
|
+
const tok = m[0]
|
|
339
|
+
let cls = 'tok-n'
|
|
340
|
+
if (/^\/\/|^\/\*|^#|^--/.test(tok)) cls = 'tok-c'
|
|
341
|
+
else if (/^["'`]/.test(tok)) cls = 'tok-s'
|
|
342
|
+
else if (/^\d/.test(tok)) cls = 'tok-n'
|
|
343
|
+
else cls = 'tok-k'
|
|
344
|
+
out += '<span class="' + cls + '">' + escapeHtml(tok) + '</span>'
|
|
345
|
+
last = i + tok.length
|
|
346
|
+
}
|
|
347
|
+
if (last < text.length) out += escapeHtml(text.slice(last))
|
|
348
|
+
return out
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/** 提交/评论文本里的 #123 → 点击跳工单(返回 React 子节点数组)。 */
|
|
352
|
+
function linkifyIssues(text, repo) {
|
|
353
|
+
const parts = String(text || '').split(/(#\d+)/g)
|
|
354
|
+
if (parts.length === 1) return text
|
|
355
|
+
return parts.map((p, i) => {
|
|
356
|
+
const m = /^#(\d+)$/.exec(p)
|
|
357
|
+
if (!m) return p
|
|
358
|
+
return h('a', {
|
|
359
|
+
key: i, style: { color: 'var(--dsw-alias-state-business-primary)', cursor: 'pointer' },
|
|
360
|
+
onClick: (e) => { e.stopPropagation(); nav('/r/' + repo.owner + '/' + repo.name + '/issues/' + m[1]) },
|
|
361
|
+
}, p)
|
|
362
|
+
})
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function dirnameOf(p) {
|
|
366
|
+
const i = (p || '').lastIndexOf('/')
|
|
367
|
+
return i < 0 ? '' : p.slice(0, i)
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
|
|
235
371
|
// ── 全屏管理页 ────────────────────────────────────────────────────────────
|
|
236
372
|
|
|
237
|
-
function RepoBrowser({ repo, t, onBack }) {
|
|
238
|
-
const [tab, setTab] = useState(
|
|
373
|
+
function RepoBrowser({ repo, t, onBack, deep }) {
|
|
374
|
+
const [tab, setTab] = useState(() => {
|
|
375
|
+
const k = deep && deep.tab
|
|
376
|
+
return k && ['files', 'issues', 'pulls', 'wiki', 'releases', 'settings', 'commits', 'branches', 'tags'].includes(k) ? k : 'files'
|
|
377
|
+
})
|
|
239
378
|
const [prPreset, setPrPreset] = useState(null)
|
|
240
379
|
const [issuesSub, setIssuesSub] = useState(null)
|
|
241
380
|
const [ov, setOv] = useState(null)
|
|
242
|
-
const [rev, setRev] = useState('')
|
|
381
|
+
const [rev, setRev] = useState(deep && deep.tab === 'src' ? deep.ref || '' : '')
|
|
243
382
|
const [cloneUrl, setCloneUrl] = useState('')
|
|
244
383
|
const [copied, setCopied] = useState(false)
|
|
245
384
|
const [star, setStar] = useState(null)
|
|
246
385
|
const [watch, setWatch] = useState(null)
|
|
247
386
|
const [forkBusy, setForkBusy] = useState(false)
|
|
387
|
+
const [pop, setPop] = useState(null) // 'star' | 'watch' | 'fork' | null
|
|
388
|
+
const [forkList, setForkList] = useState(null)
|
|
248
389
|
const [msg, setMsg] = useState('')
|
|
249
390
|
useEffect(() => {
|
|
250
391
|
api('GET', `/dsh/repos/${repo.owner}/${repo.name}/overview`).then((d) => {
|
|
251
392
|
if (!d.defaultBranch) return
|
|
252
393
|
setOv(d)
|
|
253
|
-
setRev(d.defaultBranch)
|
|
394
|
+
setRev((cur) => cur || d.defaultBranch)
|
|
254
395
|
})
|
|
255
396
|
fetch(API + '/status').then((r) => r.json()).then((st) => {
|
|
256
397
|
if (st && st.running) setCloneUrl('git clone http://' + location.hostname + ':' + (st.port || 3400) + '/' + repo.owner + '/' + repo.name + '.git')
|
|
@@ -281,15 +422,37 @@ window.__ModuleLoader__.load({
|
|
|
281
422
|
h('span', { className: 'dgs-h1' }, repo.owner + '/' + repo.name),
|
|
282
423
|
repo.private || (ov && ov.private) ? h('span', { className: 'dgs-badge pri' }, t('private')) : null,
|
|
283
424
|
h('span', { style: { flex: 1 } }),
|
|
425
|
+
h('div', { style: { position: 'relative', display: 'flex', alignItems: 'center', gap: 10 } },
|
|
284
426
|
star ? h('span', { className: 'dgs-labeled' },
|
|
285
427
|
h('button', { className: 'dgs-btn ghost', onClick: () => toggle('star') }, star.on ? '★ Unstar' : '☆ Star'),
|
|
286
|
-
h('span', { className: 'dgs-labeled-count' }, star.count)) : null,
|
|
428
|
+
h('span', { className: 'dgs-labeled-count', style: { cursor: 'pointer' }, title: '谁点了星标', onClick: () => setPop(pop === 'star' ? null : 'star') }, star.count)) : null,
|
|
287
429
|
watch ? h('span', { className: 'dgs-labeled' },
|
|
288
430
|
h('button', { className: 'dgs-btn ghost', onClick: () => toggle('watch') }, watch.on ? '👁 Unwatch' : '👁 Watch'),
|
|
289
|
-
h('span', { className: 'dgs-labeled-count' }, watch.count)) : null,
|
|
290
|
-
h('
|
|
291
|
-
h('
|
|
292
|
-
|
|
431
|
+
h('span', { className: 'dgs-labeled-count', style: { cursor: 'pointer' }, title: '谁在关注', onClick: () => setPop(pop === 'watch' ? null : 'watch') }, watch.count)) : null,
|
|
432
|
+
h('span', { className: 'dgs-labeled' },
|
|
433
|
+
h('button', { className: 'dgs-btn ghost', disabled: forkBusy, onClick: fork, style: { display: 'inline-flex', alignItems: 'center', gap: 6 } },
|
|
434
|
+
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>' } }),
|
|
435
|
+
'Fork'),
|
|
436
|
+
h('span', { className: 'dgs-labeled-count', style: { cursor: 'pointer' }, title: '复刻列表', onClick: () => {
|
|
437
|
+
if (pop === 'fork') { setPop(null); return }
|
|
438
|
+
setPop('fork')
|
|
439
|
+
if (forkList === null) api('GET', `/dsh/repos/${repo.owner}/${repo.name}/forks`).then((d) => setForkList(Array.isArray(d) ? d : []))
|
|
440
|
+
} }, ov ? ov.numForks || 0 : '')),
|
|
441
|
+
pop ? h('div', { className: 'dgs-card', style: { position: 'absolute', top: 'calc(100% + 6px)', right: 0, zIndex: 50, minWidth: 230, maxHeight: 280, overflowY: 'auto', margin: 0, padding: '10px 12px' } },
|
|
442
|
+
h('div', { style: { fontWeight: 700, fontSize: 12.5, marginBottom: 6 } },
|
|
443
|
+
pop === 'star' ? '星标用户' : pop === 'watch' ? '关注者' : '复刻(Fork)'),
|
|
444
|
+
pop === 'fork'
|
|
445
|
+
? (forkList === null ? h('div', { className: 'dgs-sub' }, t('loading'))
|
|
446
|
+
: forkList.length === 0 ? h('div', { className: 'dgs-sub' }, '—')
|
|
447
|
+
: forkList.map((f) => h('div', { key: f.owner + '/' + f.name, style: { padding: '3px 0' } },
|
|
448
|
+
h('a', { className: 'dgs-sub', style: { color: 'var(--dsw-alias-state-business-primary)', cursor: 'pointer' }, onClick: () => { setPop(null); nav('/r/' + f.owner + '/' + f.name) } },
|
|
449
|
+
f.owner + '/' + f.name + (f.stars ? ' ★' + f.stars : '')))))
|
|
450
|
+
: ((pop === 'star' ? (star && star.users) : (watch && watch.users)) || []).length === 0
|
|
451
|
+
? h('div', { className: 'dgs-sub' }, '—')
|
|
452
|
+
: ((pop === 'star' ? star.users : watch.users) || []).map((u) =>
|
|
453
|
+
h('div', { key: u, style: { padding: '3px 0' } },
|
|
454
|
+
h('a', { className: 'dgs-sub', style: { color: 'var(--dsw-alias-state-business-primary)', cursor: 'pointer' }, onClick: () => { setPop(null); nav('/u/' + u) } }, '@' + u))))
|
|
455
|
+
: null),
|
|
293
456
|
),
|
|
294
457
|
msg ? h('div', { className: 'dgs-err' }, msg) : null,
|
|
295
458
|
h('div', { className: 'dgs-tabs' },
|
|
@@ -320,19 +483,19 @@ window.__ModuleLoader__.load({
|
|
|
320
483
|
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: '下载' }, '⭳'))
|
|
321
484
|
: null))
|
|
322
485
|
: null,
|
|
323
|
-
tab === 'files' && h(FileTree, { repo, rev: rev || 'master', t, overview: ov, onOverview: setOv }),
|
|
486
|
+
tab === 'files' && h(FileTree, { repo, rev: rev || 'master', t, overview: ov, onOverview: setOv, cloneUrl, initialPath: deep && deep.tab === 'src' ? deep.filePath : '' }),
|
|
324
487
|
tab === 'commits' && h(Commits, { repo, rev: rev || 'master', t }),
|
|
325
488
|
tab === 'branches' && h(Branches, { repo, t, onNewPR: (head, base) => { setPrPreset({ head, base }); setTab('pulls') } }),
|
|
326
|
-
tab === 'issues' && h(IssuesArea, { repo, t, initialSub: issuesSub || undefined }),
|
|
327
|
-
tab === 'pulls' && h(Pulls, { repo, t, preset: prPreset, onPresetDone: () => setPrPreset(null), onGoIssuesSub: (k) => { setIssuesSub(k); setTab('issues') } }),
|
|
489
|
+
tab === 'issues' && h(IssuesArea, { repo, t, initialSub: issuesSub || undefined, initialIdx: deep && deep.tab === 'issues' && deep.arg ? Number(deep.arg) : undefined }),
|
|
490
|
+
tab === 'pulls' && h(Pulls, { repo, t, preset: prPreset, onPresetDone: () => setPrPreset(null), onGoIssuesSub: (k) => { setIssuesSub(k); setTab('issues') }, initialIdx: deep && deep.tab === 'pulls' && deep.arg ? Number(deep.arg) : undefined }),
|
|
328
491
|
tab === 'wiki' && h(WikiView, { repo, t }),
|
|
329
492
|
tab === 'releases' && h(Releases, { repo, t }),
|
|
330
493
|
tab === 'settings' && h(RepoSettings, { repo, t }),
|
|
331
494
|
)
|
|
332
495
|
}
|
|
333
496
|
|
|
334
|
-
function FileTree({ repo, rev, t, overview, onOverview }) {
|
|
335
|
-
const [path, setPath] = useState('')
|
|
497
|
+
function FileTree({ repo, rev, t, overview, onOverview, initialPath, cloneUrl }) {
|
|
498
|
+
const [path, setPath] = useState(() => dirnameOf(initialPath || ''))
|
|
336
499
|
const [entries, setEntries] = useState(null)
|
|
337
500
|
const [file, setFile] = useState(null)
|
|
338
501
|
const [err, setErr] = useState('')
|
|
@@ -346,6 +509,9 @@ window.__ModuleLoader__.load({
|
|
|
346
509
|
const [editText, setEditText] = useState('')
|
|
347
510
|
const [editMsg, setEditMsg] = useState('')
|
|
348
511
|
const [histMode, setHistMode] = useState(null)
|
|
512
|
+
const [searchQ, setSearchQ] = useState('')
|
|
513
|
+
const [hits, setHits] = useState(null) // null=未搜索;[]=无结果
|
|
514
|
+
const [deepDone, setDeepDone] = useState(false)
|
|
349
515
|
const load = useCallback((p) => {
|
|
350
516
|
setFile(null); setEntries(null); setErr(''); setBlameOn(false); setEditing(false); setHistMode(null)
|
|
351
517
|
api('GET', `/dsh/repos/${repo.owner}/${repo.name}/tree?ref=${encodeURIComponent(rev)}&path=${encodeURIComponent(p)}`)
|
|
@@ -361,20 +527,38 @@ window.__ModuleLoader__.load({
|
|
|
361
527
|
if (p !== '') setReadme(null)
|
|
362
528
|
}, [repo.owner, repo.name, rev])
|
|
363
529
|
useEffect(() => { load(path) }, [load, path])
|
|
364
|
-
const
|
|
365
|
-
|
|
366
|
-
if (e.type === 'dir' || e.type === 'tree') { setPath(p); return }
|
|
367
|
-
api('GET', `/repos/${repo.owner}/${repo.name}/raw?ref=${encodeURIComponent(rev)}&path=${encodeURIComponent(p)}`)
|
|
530
|
+
const openFile = (fp, name, size) => {
|
|
531
|
+
api('GET', `/repos/${repo.owner}/${repo.name}/raw?ref=${encodeURIComponent(rev)}&path=${encodeURIComponent(fp)}`)
|
|
368
532
|
.then(async (d) => {
|
|
369
|
-
|
|
533
|
+
if (!d.ok) { setPath(fp); setFile(null); return } // 目录型永久链接:落到目录
|
|
534
|
+
const isMd = /\.md$/i.test(name)
|
|
370
535
|
let html = ''
|
|
371
|
-
if (isMd
|
|
536
|
+
if (isMd) {
|
|
372
537
|
const r = await api('POST', '/dsh/markdown', { text: d.text })
|
|
373
538
|
html = r.html || ''
|
|
374
539
|
}
|
|
375
|
-
setFile({ name:
|
|
540
|
+
setFile({ name, path: fp, text: d.text, size: size || 0, isMd, html })
|
|
376
541
|
})
|
|
377
542
|
}
|
|
543
|
+
const openEntry = (e) => {
|
|
544
|
+
if (e.type === 'dir' || e.type === 'tree') { setPath(e.path); return }
|
|
545
|
+
openFile(e.path, e.name, e.size)
|
|
546
|
+
}
|
|
547
|
+
// 深链直达:#/r/:o/:r/src/:ref/:path —— 打开指定文件(rev 由父组件同步)
|
|
548
|
+
useEffect(() => {
|
|
549
|
+
if (deepDone || !initialPath) return
|
|
550
|
+
setDeepDone(true)
|
|
551
|
+
openFile(initialPath, initialPath.split('/').pop(), 0)
|
|
552
|
+
}, [initialPath])
|
|
553
|
+
const doSearch = () => {
|
|
554
|
+
const q = searchQ.trim()
|
|
555
|
+
if (!q) return
|
|
556
|
+
setHits(null); setFile(null)
|
|
557
|
+
setMsg('')
|
|
558
|
+
api('GET', `/dsh/repos/${repo.owner}/${repo.name}/search?q=${encodeURIComponent(q)}&ref=${encodeURIComponent(rev)}`)
|
|
559
|
+
.then((d) => { setHits(Array.isArray(d.matches) ? d.matches : []); if (d.error) setMsg(d.error) })
|
|
560
|
+
.catch(() => setMsg(t('loadFailed')))
|
|
561
|
+
}
|
|
378
562
|
const startCreate = (mode) => {
|
|
379
563
|
setNewFile(mode); setMsg('')
|
|
380
564
|
setNf({ path: '', content: '', message: '' })
|
|
@@ -405,6 +589,11 @@ window.__ModuleLoader__.load({
|
|
|
405
589
|
h('span', { className: 'dgs-crumb' + (i === arr.length - 1 ? ' cur' : ''), onClick: () => setPath(arr.slice(0, i + 1).join('/')) }, seg),
|
|
406
590
|
i < arr.length - 1 ? h('span', { className: 'dgs-sub' }, '/') : null))),
|
|
407
591
|
h('span', { style: { flex: 1 } }),
|
|
592
|
+
h('input', {
|
|
593
|
+
className: 'dgs-input', style: { maxWidth: 180, flex: 'none' }, placeholder: '搜索代码…',
|
|
594
|
+
value: searchQ, onChange: (e) => setSearchQ(e.target.value),
|
|
595
|
+
onKeyDown: (e) => { if (e.key === 'Enter') doSearch() },
|
|
596
|
+
}),
|
|
408
597
|
h('button', { className: 'dgs-btn', onClick: () => startCreate('create') }, '新的文件'),
|
|
409
598
|
h('button', { className: 'dgs-btn ghost', onClick: () => startCreate('upload') }, '上传文件')),
|
|
410
599
|
msg ? h('div', { className: 'dgs-err' }, msg) : null,
|
|
@@ -443,15 +632,17 @@ window.__ModuleLoader__.load({
|
|
|
443
632
|
h('span', { style: { fontWeight: 600, fontSize: 12.5 } }, '📄 ' + file.name + ' ' + (file.size ? fmtSize(file.size) : '')),
|
|
444
633
|
h('span', { style: { flex: 1 } }),
|
|
445
634
|
file.text !== null ? h('a', { className: 'dgs-sub', style: { cursor: 'pointer', marginRight: 12 }, onClick: () => {
|
|
446
|
-
|
|
635
|
+
// 永久链接走面板的 hash 路由(旧网页 UI 已裁撤,裸路径链接是死的)
|
|
636
|
+
const link = location.origin + location.pathname + '#/r/' + repo.owner + '/' + repo.name + '/src/' + encodeURIComponent(rev) + '/' + (file.path || '').split('/').map(encodeURIComponent).join('/')
|
|
637
|
+
try { navigator.clipboard.writeText(link) } catch {}
|
|
447
638
|
setMsg('✓ 已复制永久链接'); setTimeout(() => setMsg(''), 1500)
|
|
448
639
|
} }, '永久链接') : null,
|
|
449
640
|
file.text !== null ? h('a', { className: 'dgs-sub', style: { cursor: 'pointer', marginRight: 12 }, onClick: () => setHistMode(histMode === 'history' ? null : 'history') }, '文件历史') : null,
|
|
450
|
-
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,
|
|
641
|
+
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(file.path || path)}`, target: '_blank', rel: 'noreferrer' }, '原始文件') : null,
|
|
451
642
|
file.text !== null ? h('button', { className: 'dgs-btn ghost', style: { padding: '2px 8px' }, onClick: () => { setEditing(true); setEditText(file.text || '') } }, '✏️') : null,
|
|
452
643
|
file.text !== null ? h('button', { className: 'dgs-btn danger', style: { padding: '2px 8px' }, onClick: async () => {
|
|
453
644
|
if (!confirm('删除文件 ' + file.name + ' ?')) return
|
|
454
|
-
const d = await api('DELETE', `/dsh/repos/${repo.owner}/${repo.name}/files`, { path, branch: rev })
|
|
645
|
+
const d = await api('DELETE', `/dsh/repos/${repo.owner}/${repo.name}/files`, { path: file.path || path, branch: rev })
|
|
455
646
|
if (d && d.ok) { setFile(null); load(path) } else setMsg((d && d.error) || '删除失败')
|
|
456
647
|
} }, '🗑️') : null,
|
|
457
648
|
file.text !== null ? h('button', { className: 'dgs-btn ghost', style: { padding: '2px 8px' }, onClick: () => setBlameOn(!blameOn) }, 'Blame') : null),
|
|
@@ -463,20 +654,37 @@ window.__ModuleLoader__.load({
|
|
|
463
654
|
h('button', { className: 'dgs-btn ghost', onClick: () => setEditing(false) }, '取消'),
|
|
464
655
|
h('button', { className: 'dgs-btn', disabled: busy, onClick: async () => {
|
|
465
656
|
setBusy(true); setMsg('')
|
|
466
|
-
const d = await api('POST', `/dsh/repos/${repo.owner}/${repo.name}/files`, { path, branch: rev, content: editText, message: editMsg, overwrite: true })
|
|
657
|
+
const d = await api('POST', `/dsh/repos/${repo.owner}/${repo.name}/files`, { path: file.path || path, branch: rev, content: editText, message: editMsg, overwrite: true })
|
|
467
658
|
setBusy(false)
|
|
468
|
-
if (d && d.ok) { setEditing(false); load(path) } else setMsg((d && d.error) || '保存失败')
|
|
659
|
+
if (d && d.ok) { setEditing(false); setFile(null); load(path) } else setMsg((d && d.error) || '保存失败')
|
|
469
660
|
} }, '提交修改')))
|
|
470
|
-
: histMode === 'history' ? h(FileHistory, { repo, rev, path })
|
|
661
|
+
: histMode === 'history' ? h(FileHistory, { repo, rev, path: file.path || path })
|
|
471
662
|
: blameOn && file.text !== null
|
|
472
|
-
? h(BlameView, { repo, rev, path, text: file.text })
|
|
663
|
+
? h(BlameView, { repo, rev, path: file.path || path, text: file.text })
|
|
473
664
|
: file.isMd
|
|
474
665
|
? h('div', { className: 'dgs-md', style: { padding: '14px 16px' }, dangerouslySetInnerHTML: { __html: file.html || '' } })
|
|
475
|
-
:
|
|
666
|
+
: file.text === null
|
|
667
|
+
? h('pre', { className: 'dgs-file', style: { border: 'none', borderRadius: 0 } }, t('emptyFile'))
|
|
668
|
+
: h('pre', { className: 'dgs-file', style: { border: 'none', borderRadius: 0 }, dangerouslySetInnerHTML: { __html: highlightCode(file.text, file.name) } })))
|
|
669
|
+
: hits !== null ? renderHits()
|
|
476
670
|
: entries === null ? h('div', { className: 'dgs-empty' }, t('loading'))
|
|
477
671
|
: renderDir(),
|
|
478
672
|
)
|
|
479
673
|
|
|
674
|
+
function renderHits() {
|
|
675
|
+
return h('div', null,
|
|
676
|
+
h('div', { className: 'dgs-row', style: { marginBottom: 8 } },
|
|
677
|
+
h('button', { className: 'dgs-btn ghost', onClick: () => setHits(null) }, t('back')),
|
|
678
|
+
h('span', { className: 'dgs-sub' }, '在 ' + rev + ' 中搜索「' + searchQ.trim() + '」')),
|
|
679
|
+
hits.length === 0 ? h('div', { className: 'dgs-empty' }, '—')
|
|
680
|
+
: hits.map((hit, i) =>
|
|
681
|
+
h('div', { key: i, className: 'dgs-hit', onClick: () => { setHits(null); setPath(dirnameOf(hit.path)); openFile(hit.path, hit.path.split('/').pop(), 0) } },
|
|
682
|
+
h('div', null,
|
|
683
|
+
h('span', { style: { color: 'var(--dsw-alias-state-business-primary)', fontWeight: 500, fontSize: 12.5 } }, hit.path),
|
|
684
|
+
h('span', { className: 'dgs-sub' }, ':' + hit.line)),
|
|
685
|
+
h('div', { className: 'dgs-hit-code' }, hit.text))))
|
|
686
|
+
}
|
|
687
|
+
|
|
480
688
|
function renderDir() {
|
|
481
689
|
const head = entries.find((e) => e.last)
|
|
482
690
|
const rows = entries.map((e) =>
|
|
@@ -493,7 +701,9 @@ window.__ModuleLoader__.load({
|
|
|
493
701
|
h('span', { style: { flex: 1 } }),
|
|
494
702
|
h('span', { className: 'dgs-sub' }, timeAgo(head.last.date))) : null
|
|
495
703
|
const table = entries.length === 0
|
|
496
|
-
?
|
|
704
|
+
? (path === '' && overview && overview.numCommits === 0
|
|
705
|
+
? renderEmptyRepoGuide()
|
|
706
|
+
: h('div', { className: 'dgs-empty' }, t('emptyDir')))
|
|
497
707
|
: h('div', null, headRow, h('table', { className: 'dgs-table' }, h('tbody', null, rows)))
|
|
498
708
|
const readmeEl = (path === '' && overview && overview.readmeHtml)
|
|
499
709
|
? h('div', { className: 'dgs-card', style: { marginTop: 12 } },
|
|
@@ -502,9 +712,26 @@ window.__ModuleLoader__.load({
|
|
|
502
712
|
: null
|
|
503
713
|
return h('div', null, table, readmeEl)
|
|
504
714
|
}
|
|
715
|
+
|
|
716
|
+
function renderEmptyRepoGuide() {
|
|
717
|
+
const remote = (cloneUrl || '').replace(/^git clone\s+/, '') || ('http://<服务器>:3400/' + repo.owner + '/' + repo.name + '.git')
|
|
718
|
+
const branch = (overview && overview.defaultBranch) || 'master'
|
|
719
|
+
const copyBtn = (text) => h('button', { className: 'dgs-btn ghost', style: { padding: '2px 10px', fontSize: 12 }, onClick: () => { try { navigator.clipboard.writeText(text) } catch {} } }, t('copy'))
|
|
720
|
+
return h('div', { className: 'dgs-card', style: { maxWidth: 660, margin: '24px auto' } },
|
|
721
|
+
h('div', { style: { fontWeight: 700, fontSize: 15, marginBottom: 6 } }, '这个仓库还是空的'),
|
|
722
|
+
h('div', { className: 'dgs-sub', style: { marginBottom: 12 } }, '克隆到本地开始开发,或把已有的本地仓库推送上来。git 凭据使用 dsh(user-management)的用户名和密码。'),
|
|
723
|
+
h('div', { className: 'dgs-row', style: { marginBottom: 4 } },
|
|
724
|
+
h('span', { style: { fontWeight: 600, fontSize: 12.5, flex: 1 } }, '克隆仓库'),
|
|
725
|
+
copyBtn('git clone ' + remote)),
|
|
726
|
+
h('div', { className: 'dgs-guide' }, 'git clone ' + remote),
|
|
727
|
+
h('div', { className: 'dgs-row', style: { margin: '12px 0 4px' } },
|
|
728
|
+
h('span', { style: { fontWeight: 600, fontSize: 12.5, flex: 1 } }, '推送已有仓库'),
|
|
729
|
+
copyBtn('git remote add origin ' + remote + '\ngit push -u origin ' + branch)),
|
|
730
|
+
h('div', { className: 'dgs-guide' }, 'git remote add origin ' + remote + '\ngit push -u origin ' + branch))
|
|
731
|
+
}
|
|
505
732
|
}
|
|
506
733
|
|
|
507
|
-
function IssuesArea({ repo, t, initialSub }) {
|
|
734
|
+
function IssuesArea({ repo, t, initialSub, initialIdx }) {
|
|
508
735
|
const [sub, setSub] = useState(initialSub || 'list')
|
|
509
736
|
const [labelJump, setLabelJump] = useState(null)
|
|
510
737
|
const [msJump, setMsJump] = useState(null)
|
|
@@ -520,12 +747,12 @@ window.__ModuleLoader__.load({
|
|
|
520
747
|
h('span', { style: { flex: 1 } }),
|
|
521
748
|
sub === 'list' ? h('button', { className: 'dgs-btn', onClick: () => setCreateSignal((n) => n + 1) }, t('issueNew')) : null),
|
|
522
749
|
sub === 'list' && h(Issues, { repo, t, presetLabel: labelJump, presetMilestone: msJump,
|
|
523
|
-
onPresetDone: () => { setLabelJump(null); setMsJump(null) }, createSignal }),
|
|
750
|
+
onPresetDone: () => { setLabelJump(null); setMsJump(null) }, createSignal, initialIdx }),
|
|
524
751
|
sub === 'labels' && h(LabelsManage, { repo, t, onFilterLabel: (lid) => { setLabelJump(lid); setSub('list') } }),
|
|
525
752
|
sub === 'milestones' && h(MilestonesManage, { repo, t, onOpenMilestone: (m) => { setMsJump(m.id); setSub('list') } }))
|
|
526
753
|
}
|
|
527
754
|
|
|
528
|
-
function Issues({ repo, t, presetLabel, presetMilestone, onPresetDone, createSignal }) {
|
|
755
|
+
function Issues({ repo, t, presetLabel, presetMilestone, onPresetDone, createSignal, initialIdx }) {
|
|
529
756
|
const [state, setState] = useState('open')
|
|
530
757
|
const [flt, setFlt] = useState({ label: '', milestone: '', assignee: '' })
|
|
531
758
|
useEffect(() => {
|
|
@@ -540,7 +767,7 @@ window.__ModuleLoader__.load({
|
|
|
540
767
|
const [meta, setMeta] = useState({ labels: [], milestones: [], collabs: [] })
|
|
541
768
|
const [creating, setCreating] = useState(false)
|
|
542
769
|
useEffect(() => { if (createSignal) setCreating(true) }, [createSignal])
|
|
543
|
-
const [openIdx, setOpenIdx] = useState(null)
|
|
770
|
+
const [openIdx, setOpenIdx] = useState(initialIdx ?? null)
|
|
544
771
|
const reload = useCallback(() => {
|
|
545
772
|
setList(null)
|
|
546
773
|
const q = new URLSearchParams({ state, sort })
|
|
@@ -685,6 +912,8 @@ window.__ModuleLoader__.load({
|
|
|
685
912
|
setComments(null)
|
|
686
913
|
api('GET', `/dsh/repos/${repo.owner}/${repo.name}/issues/${idx}`).then((d) => setIssue(d && d.number ? d : null))
|
|
687
914
|
api('GET', `/repos/${repo.owner}/${repo.name}/issues/${idx}/comments`).then((d) => setComments(d.comments || []))
|
|
915
|
+
// 打开详情即视为已读(通知中心据此消未读)
|
|
916
|
+
api('POST', `/dsh/repos/${repo.owner}/${repo.name}/issues/${idx}/read`).catch(() => {})
|
|
688
917
|
}, [repo.owner, repo.name, idx])
|
|
689
918
|
useEffect(reload, [reload])
|
|
690
919
|
useEffect(() => {
|
|
@@ -714,7 +943,13 @@ window.__ModuleLoader__.load({
|
|
|
714
943
|
comments === null ? h('div', { className: 'dgs-empty' }, t('loading'))
|
|
715
944
|
: comments.length === 0 ? h('div', { className: 'dgs-empty' }, t('noIssues'))
|
|
716
945
|
: comments.map((c) =>
|
|
717
|
-
|
|
946
|
+
c.type === 2 // 提交关单事件(push 时 fixes #N 触发)
|
|
947
|
+
? h('div', { key: c.id || ('ev' + c.created), className: 'dgs-row', style: { padding: '5px 4px', gap: 6 } },
|
|
948
|
+
h('span', null, '🔒'),
|
|
949
|
+
h('span', { className: 'dgs-sub' }, (c.user || '') + ' 通过提交 '),
|
|
950
|
+
h('code', { className: 'dgs-badge' }, (c.commit_sha || '').slice(0, 10) || '?'),
|
|
951
|
+
h('span', { className: 'dgs-sub' }, ' 关闭了此工单 · ' + fmtDate(c.created)))
|
|
952
|
+
: h('div', { key: c.id, className: 'dgs-comment' },
|
|
718
953
|
h('div', { className: 'dgs-row', style: { marginBottom: 4 } },
|
|
719
954
|
h('span', { className: 'dgs-sub' }, `${c.user || ''} · ${fmtDate(c.created)}`),
|
|
720
955
|
h('span', { style: { flex: 1 } }),
|
|
@@ -731,7 +966,7 @@ window.__ModuleLoader__.load({
|
|
|
731
966
|
await api('PATCH', `/repos/${repo.owner}/${repo.name}/issues/comments/${c.id}`, { body: editC.text })
|
|
732
967
|
setEditC(null); reload()
|
|
733
968
|
} }, t('save'))))
|
|
734
|
-
: h('div', { style: { whiteSpace: 'pre-wrap' } }, c.body))),
|
|
969
|
+
: h('div', { style: { whiteSpace: 'pre-wrap' } }, linkifyIssues(c.body, repo)))),
|
|
735
970
|
h('div', { className: 'dgs-card', style: { margin: '12px 0' } },
|
|
736
971
|
h('textarea', { className: 'dgs-input', placeholder: t('commentPlaceholder'), value: text, onChange: (e) => setText(e.target.value) }),
|
|
737
972
|
h('div', { className: 'dgs-row', style: { marginTop: 8 } },
|
|
@@ -814,7 +1049,7 @@ window.__ModuleLoader__.load({
|
|
|
814
1049
|
h('table', { className: 'dgs-table' },
|
|
815
1050
|
h('tbody', null, shown.map((c, i) =>
|
|
816
1051
|
h('tr', { key: i, style: { cursor: 'pointer' }, onClick: () => setSel(c.sha) },
|
|
817
|
-
h('td', null, h('div', { style: { fontWeight: 500 } }, (c.message || '').split('\n')[0]),
|
|
1052
|
+
h('td', null, h('div', { style: { fontWeight: 500 } }, linkifyIssues((c.message || '').split('\n')[0], repo)),
|
|
818
1053
|
h('div', { className: 'dgs-sub' }, c.author)),
|
|
819
1054
|
h('td', { className: 'dgs-sub', style: { textAlign: 'right', whiteSpace: 'nowrap' } }, (c.sha || '').slice(0, 10)),
|
|
820
1055
|
h('td', { className: 'dgs-sub', style: { textAlign: 'right', whiteSpace: 'nowrap', width: 100 } }, timeAgo(c.date)))))),
|
|
@@ -834,7 +1069,7 @@ window.__ModuleLoader__.load({
|
|
|
834
1069
|
return h('div', null,
|
|
835
1070
|
h('div', { className: 'dgs-row', style: { marginBottom: 10 } },
|
|
836
1071
|
h('button', { className: 'dgs-btn ghost', onClick: onBack }, t('back')),
|
|
837
|
-
h('span', { style: { fontWeight: 600 } }, (d.message || '').split('\n')[0]),
|
|
1072
|
+
h('span', { style: { fontWeight: 600 } }, linkifyIssues((d.message || '').split('\n')[0], repo)),
|
|
838
1073
|
h('span', { className: 'dgs-sub' }, (d.sha || '').slice(0, 10))),
|
|
839
1074
|
h('div', { className: 'dgs-sub', style: { marginBottom: 8 } },
|
|
840
1075
|
`${(d.author && d.author.name) || ''} · ${fmtDate(when)}`),
|
|
@@ -928,8 +1163,9 @@ window.__ModuleLoader__.load({
|
|
|
928
1163
|
b.name !== def ? h('button', { className: 'dgs-btn ghost', style: { marginLeft: 8, padding: '2px 8px' }, onClick: () => onNewPR && onNewPR(b.name, def) }, '+ PR') : null,
|
|
929
1164
|
b.name !== def ? h('button', { className: 'dgs-btn danger', style: { marginLeft: 4, padding: '2px 8px' }, onClick: async () => {
|
|
930
1165
|
if (!confirm('删除分支 ' + b.name + ' ?')) return
|
|
931
|
-
await api('DELETE', `/repos/${repo.owner}/${repo.name}/branches/${encodeURIComponent(b.name)}`)
|
|
932
|
-
reload()
|
|
1166
|
+
const d = await api('DELETE', `/dsh/repos/${repo.owner}/${repo.name}/branches/${encodeURIComponent(b.name)}`)
|
|
1167
|
+
if (d && d.ok) reload()
|
|
1168
|
+
else alert((d && d.error) || '删除失败')
|
|
933
1169
|
} }, '删') : null))))),
|
|
934
1170
|
h('div', { style: { fontWeight: 700, margin: '18px 0 8px' } }, '标签'),
|
|
935
1171
|
tags === null ? h('div', { className: 'dgs-empty' }, t('loading'))
|
|
@@ -1184,19 +1420,33 @@ window.__ModuleLoader__.load({
|
|
|
1184
1420
|
const [form, setForm] = useState({ description: '', private: false, website: '' })
|
|
1185
1421
|
const [collabs, setCollabs] = useState([])
|
|
1186
1422
|
const [addName, setAddName] = useState('')
|
|
1423
|
+
const [addMode, setAddMode] = useState('write')
|
|
1187
1424
|
const [hooks, setHooks] = useState(null)
|
|
1188
1425
|
const [hookUrl, setHookUrl] = useState('')
|
|
1426
|
+
const [hookDeliveries, setHookDeliveries] = useState({}) // id → rows | 'loading'
|
|
1427
|
+
const [prots, setProts] = useState(null)
|
|
1428
|
+
const [protBranch, setProtBranch] = useState('')
|
|
1429
|
+
const [branchList, setBranchList] = useState([])
|
|
1430
|
+
const [mirrorInfo, setMirrorInfo] = useState(null)
|
|
1431
|
+
const [syncBusy, setSyncBusy] = useState(false)
|
|
1189
1432
|
const [msg, setMsg] = useState('')
|
|
1190
1433
|
const [busy, setBusy] = useState(false)
|
|
1191
1434
|
const loadCollabs = () => api('GET', `/dsh/repos/${repo.owner}/${repo.name}/collaborators`).then((d) => setCollabs(Array.isArray(d) ? d : []))
|
|
1192
1435
|
const loadHooks = () => api('GET', `/dsh/repos/${repo.owner}/${repo.name}/hooks`).then((d) => setHooks(Array.isArray(d) ? d : []))
|
|
1436
|
+
const loadProts = () => api('GET', `/dsh/repos/${repo.owner}/${repo.name}/protections`).then((d) => setProts(Array.isArray(d) ? d : []))
|
|
1193
1437
|
useEffect(() => {
|
|
1194
1438
|
api('GET', `/dsh/repos/${repo.owner}/${repo.name}`).then((d) => {
|
|
1195
|
-
if (d && d.name) {
|
|
1439
|
+
if (d && d.name) {
|
|
1440
|
+
setInfo(d)
|
|
1441
|
+
setForm({ description: d.description || '', private: !!d.private, website: d.website || '' })
|
|
1442
|
+
if (d.isMirror) setMirrorInfo(d.mirror || { address: '', updatedAt: 0, nextAt: 0 })
|
|
1443
|
+
}
|
|
1196
1444
|
else setMsg((d && d.error) || t('loadFailed'))
|
|
1197
1445
|
})
|
|
1198
1446
|
loadCollabs()
|
|
1199
1447
|
loadHooks()
|
|
1448
|
+
loadProts()
|
|
1449
|
+
api('GET', `/repos/${repo.owner}/${repo.name}/branches`).then((d) => setBranchList((d && d.branches) || []))
|
|
1200
1450
|
}, [repo.owner, repo.name])
|
|
1201
1451
|
if (info === null && !msg) return h('div', { className: 'dgs-empty' }, t('loading'))
|
|
1202
1452
|
const navItem = (k, label) => h('span', { key: k, className: 'item' + (sub === k ? ' active' : ''), onClick: () => setSub(k) }, label)
|
|
@@ -1221,19 +1471,33 @@ window.__ModuleLoader__.load({
|
|
|
1221
1471
|
setBusy(false)
|
|
1222
1472
|
d && d.ok ? setMsg('✓ 已保存') : setMsg((d && d.error) || 'failed')
|
|
1223
1473
|
} }, '更新设置')))
|
|
1474
|
+
const collabModeName = { 1: '只读', 2: '可写', 3: '管理' }
|
|
1224
1475
|
const collab = h('div', { className: 'dgs-card' },
|
|
1225
1476
|
h('div', { style: { fontWeight: 700, marginBottom: 8 } }, '协作者'),
|
|
1226
1477
|
h('div', { className: 'dgs-row' },
|
|
1227
1478
|
h('input', { className: 'dgs-input', style: { maxWidth: 200 }, placeholder: '用户名', value: addName, onChange: (e) => setAddName(e.target.value) }),
|
|
1479
|
+
h('select', { className: 'dgs-input', style: { maxWidth: 110, flex: 'none', width: 'auto' }, value: addMode, onChange: (e) => setAddMode(e.target.value) },
|
|
1480
|
+
h('option', { value: 'read' }, '只读'),
|
|
1481
|
+
h('option', { value: 'write' }, '可写'),
|
|
1482
|
+
h('option', { value: 'admin' }, '管理')),
|
|
1228
1483
|
h('button', { className: 'dgs-btn', disabled: !addName.trim(),
|
|
1229
1484
|
onClick: async () => {
|
|
1230
|
-
const d = await api('POST', `/dsh/repos/${repo.owner}/${repo.name}/collaborators/${encodeURIComponent(addName.trim())}
|
|
1485
|
+
const d = await api('POST', `/dsh/repos/${repo.owner}/${repo.name}/collaborators/${encodeURIComponent(addName.trim())}`, { mode: addMode })
|
|
1231
1486
|
if (d && d.ok !== false && !d.error) { setAddName(''); setMsg(''); loadCollabs() } else setMsg((d && d.error) || 'failed')
|
|
1232
1487
|
} }, '+ 添加')),
|
|
1233
1488
|
collabs.length === 0 ? h('div', { className: 'dgs-sub', style: { marginTop: 8 } }, '—') : null,
|
|
1234
1489
|
collabs.map((u) => h('div', { key: u.name, className: 'dgs-row', style: { marginTop: 6 } },
|
|
1235
1490
|
h('a', { className: 'dgs-name', href: '#/u/' + u.name }, u.name),
|
|
1236
1491
|
h('span', { style: { flex: 1 } }),
|
|
1492
|
+
h('select', { className: 'dgs-input', style: { maxWidth: 96, flex: 'none', width: 'auto', padding: '3px 6px', fontSize: 12 }, value: String(u.mode || 2),
|
|
1493
|
+
onChange: async (e) => {
|
|
1494
|
+
const mode = { 1: 'read', 2: 'write', 3: 'admin' }[e.target.value] || 'write'
|
|
1495
|
+
await api('PATCH', `/dsh/repos/${repo.owner}/${repo.name}/collaborators/${encodeURIComponent(u.name)}`, { mode })
|
|
1496
|
+
loadCollabs()
|
|
1497
|
+
} },
|
|
1498
|
+
h('option', { value: '1' }, '只读'),
|
|
1499
|
+
h('option', { value: '2' }, '可写'),
|
|
1500
|
+
h('option', { value: '3' }, '管理')),
|
|
1237
1501
|
h('button', { className: 'dgs-btn danger', onClick: async () => {
|
|
1238
1502
|
await api('DELETE', `/dsh/repos/${repo.owner}/${repo.name}/collaborators/${encodeURIComponent(u.name)}`)
|
|
1239
1503
|
loadCollabs()
|
|
@@ -1246,16 +1510,77 @@ window.__ModuleLoader__.load({
|
|
|
1246
1510
|
const d = await api('POST', `/dsh/repos/${repo.owner}/${repo.name}/hooks`, { url: hookUrl.trim() })
|
|
1247
1511
|
if (d && !d.error) { setHookUrl(''); loadHooks() } else setMsg((d && d.error) || 'failed')
|
|
1248
1512
|
} }, '+ 添加')),
|
|
1249
|
-
(hooks || []).map((hk) => h('div', { key: hk.id,
|
|
1250
|
-
h('
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1513
|
+
(hooks || []).map((hk) => h('div', { key: hk.id, style: { marginTop: 6 } },
|
|
1514
|
+
h('div', { className: 'dgs-row' },
|
|
1515
|
+
h('span', { className: 'dgs-badge' + (hk.is_active ? ' ok' : ' closed') }, hk.is_active ? '启用' : '停用'),
|
|
1516
|
+
hk.last_status === 1 ? h('span', { className: 'dgs-badge ok', title: '最近一次投递成功' }, '✓ 投递成功')
|
|
1517
|
+
: hk.last_status === 2 ? h('span', { className: 'dgs-badge closed', title: '最近一次投递失败' }, '✗ 投递失败') : null,
|
|
1518
|
+
h('span', { className: 'dgs-sub', style: { flex: 1, overflow: 'hidden', textOverflow: 'ellipsis' } }, hk.url),
|
|
1519
|
+
h('button', { className: 'dgs-btn ghost', onClick: async () => {
|
|
1520
|
+
const d = await api('POST', `/dsh/repos/${repo.owner}/${repo.name}/hooks/${hk.id}/test`)
|
|
1521
|
+
if (d && d.ok) { setMsg('✓ 已发送测试事件(push)'); setTimeout(() => setMsg(''), 1600); setTimeout(loadHooks, 1200) }
|
|
1522
|
+
else setMsg((d && d.error) || 'failed')
|
|
1523
|
+
} }, '测试'),
|
|
1524
|
+
h('button', { className: 'dgs-btn ghost', onClick: async () => {
|
|
1525
|
+
if (hookDeliveries[hk.id]) { setHookDeliveries({ ...hookDeliveries, [hk.id]: undefined }); return }
|
|
1526
|
+
setHookDeliveries({ ...hookDeliveries, [hk.id]: 'loading' })
|
|
1527
|
+
const d = await api('GET', `/dsh/repos/${repo.owner}/${repo.name}/hooks/${hk.id}/deliveries`)
|
|
1528
|
+
setHookDeliveries((m) => ({ ...m, [hk.id]: Array.isArray(d) ? d : [] }))
|
|
1529
|
+
} }, '记录'),
|
|
1530
|
+
h('button', { className: 'dgs-btn ghost', onClick: async () => {
|
|
1531
|
+
await api('PATCH', `/dsh/repos/${repo.owner}/${repo.name}/hooks/${hk.id}`, { active: !hk.is_active }); loadHooks()
|
|
1532
|
+
} }, hk.is_active ? '停用' : '启用'),
|
|
1533
|
+
h('button', { className: 'dgs-btn danger', onClick: async () => {
|
|
1534
|
+
await api('DELETE', `/dsh/repos/${repo.owner}/${repo.name}/hooks/${hk.id}`); loadHooks()
|
|
1535
|
+
} }, t('delete'))),
|
|
1536
|
+
Array.isArray(hookDeliveries[hk.id]) ? h('div', { style: { margin: '4px 0 4px 24px' } },
|
|
1537
|
+
hookDeliveries[hk.id].length === 0 ? h('div', { className: 'dgs-sub' }, '暂无投递记录')
|
|
1538
|
+
: hookDeliveries[hk.id].map((dv) =>
|
|
1539
|
+
h('div', { key: dv.id, className: 'dgs-row', style: { padding: '2px 0', gap: 8 } },
|
|
1540
|
+
h('span', { className: 'dgs-badge' + (dv.ok ? ' ok' : ' closed') }, dv.ok ? '成功' : '失败'),
|
|
1541
|
+
h('span', { className: 'dgs-sub' }, '#' + dv.id + ' · ' + dv.event + (dv.status ? ' · HTTP ' + dv.status : '')),
|
|
1542
|
+
dv.err ? h('span', { className: 'dgs-sub', style: { color: 'var(--dsw-alias-state-error-primary)' } }, dv.err) : null)))
|
|
1543
|
+
: hookDeliveries[hk.id] === 'loading' ? h('div', { className: 'dgs-sub', style: { margin: '4px 0 4px 24px' } }, t('loading')) : null)),
|
|
1258
1544
|
hooks !== null && hooks.length === 0 ? h('div', { className: 'dgs-sub', style: { marginTop: 6 } }, '—') : null)
|
|
1545
|
+
const protsCard = h('div', { className: 'dgs-card' },
|
|
1546
|
+
h('div', { style: { fontWeight: 700, marginBottom: 8 } }, '分支保护'),
|
|
1547
|
+
h('div', { className: 'dgs-sub', style: { marginBottom: 10 } }, '受保护的分支不可删除,也不接受强推(非快进推送会被拒绝)。合并 PR、网页编辑等正常快进操作不受影响。'),
|
|
1548
|
+
h('div', { className: 'dgs-row' },
|
|
1549
|
+
h('select', { className: 'dgs-input', style: { maxWidth: 220, flex: 'none', width: 'auto' }, value: protBranch, onChange: (e) => setProtBranch(e.target.value) },
|
|
1550
|
+
h('option', { value: '' }, '选择分支…'),
|
|
1551
|
+
branchList.map((b) => h('option', { key: b.name, value: b.name }, b.name + (info && b.name === info.defaultBranch ? '(默认)' : '')))),
|
|
1552
|
+
h('button', { className: 'dgs-btn', disabled: !protBranch, onClick: async () => {
|
|
1553
|
+
const d = await api('POST', `/dsh/repos/${repo.owner}/${repo.name}/protections`, { branch: protBranch, protected: true })
|
|
1554
|
+
if (d && d.ok) { setProtBranch(''); loadProts() } else setMsg((d && d.error) || 'failed')
|
|
1555
|
+
} }, '+ 保护')),
|
|
1556
|
+
prots === null ? h('div', { className: 'dgs-sub', style: { marginTop: 8 } }, t('loading'))
|
|
1557
|
+
: prots.length === 0 ? h('div', { className: 'dgs-sub', style: { marginTop: 8 } }, '—')
|
|
1558
|
+
: prots.map((p) => h('div', { key: p.branch, className: 'dgs-row', style: { marginTop: 6 } },
|
|
1559
|
+
h('span', { className: 'dgs-badge ok' }, '🔒 ' + p.branch),
|
|
1560
|
+
h('span', { style: { flex: 1 } }),
|
|
1561
|
+
h('button', { className: 'dgs-btn danger', onClick: async () => {
|
|
1562
|
+
await api('POST', `/dsh/repos/${repo.owner}/${repo.name}/protections`, { branch: p.branch, protected: false })
|
|
1563
|
+
loadProts()
|
|
1564
|
+
} }, '解除保护'))))
|
|
1565
|
+
const mirrorCard = mirrorInfo ? h('div', { className: 'dgs-card' },
|
|
1566
|
+
h('div', { style: { fontWeight: 700, marginBottom: 8 } }, '镜像同步'),
|
|
1567
|
+
h('div', { className: 'dgs-row' },
|
|
1568
|
+
h('span', { className: 'dgs-sub', style: { minWidth: 70 } }, '上游地址'),
|
|
1569
|
+
h('span', { style: { fontFamily: 'ui-monospace,monospace', fontSize: 12.5, wordBreak: 'break-all' } }, mirrorInfo.address || '—')),
|
|
1570
|
+
h('div', { className: 'dgs-row' },
|
|
1571
|
+
h('span', { className: 'dgs-sub', style: { minWidth: 70 } }, '上次同步'),
|
|
1572
|
+
h('span', { className: 'dgs-sub' }, mirrorInfo.updatedAt ? timeAgo(mirrorInfo.updatedAt) : '从未'),
|
|
1573
|
+
h('span', { className: 'dgs-sub' }, mirrorInfo.nextAt ? '· 下次 ' + timeAgo(mirrorInfo.nextAt).replace('之前', '后') : '')),
|
|
1574
|
+
h('div', { className: 'dgs-row', style: { marginTop: 8 } },
|
|
1575
|
+
h('button', { className: 'dgs-btn', disabled: syncBusy, onClick: async () => {
|
|
1576
|
+
setSyncBusy(true); setMsg('')
|
|
1577
|
+
const d = await api('POST', `/dsh/repos/${repo.owner}/${repo.name}/mirror-sync`)
|
|
1578
|
+
setSyncBusy(false)
|
|
1579
|
+
if (d && d.ok) {
|
|
1580
|
+
setMirrorInfo({ ...mirrorInfo, updatedAt: d.updatedAt, nextAt: d.nextAt })
|
|
1581
|
+
setMsg('✓ 同步完成')
|
|
1582
|
+
} else setMsg((d && d.error) || '同步失败')
|
|
1583
|
+
} }, syncBusy ? '同步中…(拉取上游可能需要数十秒)' : '立即同步'))) : null
|
|
1259
1584
|
const danger = h('div', { className: 'dgs-card', style: { marginTop: 12, borderColor: 'var(--dsw-alias-state-error-primary)55' } },
|
|
1260
1585
|
h('div', { style: { fontWeight: 700, marginBottom: 8, color: 'var(--dsw-alias-state-error-primary)' } }, '危险区域'),
|
|
1261
1586
|
h('div', { className: 'dgs-row' },
|
|
@@ -1287,17 +1612,19 @@ window.__ModuleLoader__.load({
|
|
|
1287
1612
|
h('div', { className: 'dgs-settings-nav' },
|
|
1288
1613
|
navItem('basic', '基本设置'),
|
|
1289
1614
|
navItem('collab', '管理协作者'),
|
|
1615
|
+
navItem('prot', '分支保护'),
|
|
1290
1616
|
navItem('hooks', '管理 Web 钩子'),
|
|
1617
|
+
mirrorInfo ? navItem('mirror', '镜像同步') : null,
|
|
1291
1618
|
navItem('danger', '危险区域')),
|
|
1292
1619
|
h('div', { style: { flex: 1, minWidth: 0 } },
|
|
1293
|
-
sub === 'basic' ? basic : sub === 'collab' ? collab : sub === 'hooks' ? hooksCard : danger)))
|
|
1620
|
+
sub === 'basic' ? basic : sub === 'collab' ? collab : sub === 'prot' ? protsCard : sub === 'hooks' ? hooksCard : sub === 'mirror' ? mirrorCard : danger)))
|
|
1294
1621
|
}
|
|
1295
1622
|
|
|
1296
1623
|
// ── PR(列表 + 详情 + 合并 + 评论复用 issue 通道) ────────────────────────
|
|
1297
1624
|
|
|
1298
|
-
function Pulls({ repo, t, preset, onPresetDone, onGoIssuesSub }) {
|
|
1625
|
+
function Pulls({ repo, t, preset, onPresetDone, onGoIssuesSub, initialIdx }) {
|
|
1299
1626
|
const [list, setList] = useState(null)
|
|
1300
|
-
const [openIdx, setOpenIdx] = useState(null)
|
|
1627
|
+
const [openIdx, setOpenIdx] = useState(initialIdx ?? null)
|
|
1301
1628
|
const [creating, setCreating] = useState(false)
|
|
1302
1629
|
const [state, setState] = useState('open')
|
|
1303
1630
|
const [flt, setFlt] = useState({ label: '', milestone: '', assignee: '' })
|
|
@@ -1468,6 +1795,11 @@ window.__ModuleLoader__.load({
|
|
|
1468
1795
|
const [allMs, setAllMs] = useState([])
|
|
1469
1796
|
const [collabs, setCollabs] = useState([])
|
|
1470
1797
|
const [mergeMsg, setMergeMsg] = useState('')
|
|
1798
|
+
const [reviews, setReviews] = useState([])
|
|
1799
|
+
const [reviewText, setReviewText] = useState('')
|
|
1800
|
+
const loadReviews = useCallback(() => {
|
|
1801
|
+
api('GET', `/dsh/repos/${repo.owner}/${repo.name}/pulls/${idx}/reviews`).then((d) => setReviews(Array.isArray(d) ? d : []))
|
|
1802
|
+
}, [repo.owner, repo.name, idx])
|
|
1471
1803
|
useEffect(() => {
|
|
1472
1804
|
api('GET', `/dsh/repos/${repo.owner}/${repo.name}/pulls`).then((d) => {
|
|
1473
1805
|
const all = Array.isArray(d) ? d : (d.data || d.pulls || [])
|
|
@@ -1477,6 +1809,8 @@ window.__ModuleLoader__.load({
|
|
|
1477
1809
|
})
|
|
1478
1810
|
api('GET', `/repos/${repo.owner}/${repo.name}/issues/${idx}/comments`).then((d) => setComments(d.comments || []))
|
|
1479
1811
|
api('GET', `/dsh/repos/${repo.owner}/${repo.name}/issues/${idx}`).then((d) => setIssue(d && d.number ? d : null))
|
|
1812
|
+
api('POST', `/dsh/repos/${repo.owner}/${repo.name}/issues/${idx}/read`).catch(() => {})
|
|
1813
|
+
loadReviews()
|
|
1480
1814
|
api('GET', `/dsh/repos/${repo.owner}/${repo.name}/labels`).then((d) => setAllLabels(Array.isArray(d) ? d : []))
|
|
1481
1815
|
api('GET', `/dsh/repos/${repo.owner}/${repo.name}/milestones`).then((d) => setAllMs(Array.isArray(d) ? d : []))
|
|
1482
1816
|
api('GET', `/dsh/repos/${repo.owner}/${repo.name}/collaborators`).then((d) => setCollabs(Array.isArray(d) ? d : []))
|
|
@@ -1517,13 +1851,37 @@ window.__ModuleLoader__.load({
|
|
|
1517
1851
|
onChange: (e) => patchIssue({ assignee: e.target.value }) },
|
|
1518
1852
|
h('option', { value: '' }, '未指派成员'),
|
|
1519
1853
|
collabs.map((u) => h('option', { key: u.name, value: u.name }, u.name)))),
|
|
1854
|
+
h('div', { className: 'dgs-side-block' },
|
|
1855
|
+
h('div', { className: 'dgs-side-title' }, '评审'),
|
|
1856
|
+
reviews.length === 0 ? h('div', { className: 'dgs-sub' }, '暂无评审') : null,
|
|
1857
|
+
reviews.map((rv) => h('div', { key: rv.user, className: 'dgs-row', style: { padding: '3px 0', gap: 6 } },
|
|
1858
|
+
h('span', { style: { color: rv.approved ? 'var(--dsw-alias-state-success-primary)' : 'var(--dsw-alias-state-error-primary)', fontWeight: 600, fontSize: 12.5 } }, rv.approved ? '✓' : '✗'),
|
|
1859
|
+
h('span', { style: { fontSize: 12.5 } }, rv.user),
|
|
1860
|
+
h('span', { className: 'dgs-sub' }, rv.approved ? '通过' : '请求修改')))),
|
|
1520
1861
|
h('div', { className: 'dgs-side-block' },
|
|
1521
1862
|
h('div', { className: 'dgs-side-title' }, (issue && issue.participants || 1) + ' 名参与者'),
|
|
1522
1863
|
h('div', { className: 'dgs-sub' }, pr.author || '')))
|
|
1864
|
+
const approvals = reviews.filter((rv) => rv.approved).length
|
|
1865
|
+
const submitReview = async (approved) => {
|
|
1866
|
+
setBusy(true)
|
|
1867
|
+
const d = await api('POST', `/dsh/repos/${repo.owner}/${repo.name}/pulls/${idx}/reviews`, { approved, content: reviewText.trim() })
|
|
1868
|
+
setBusy(false)
|
|
1869
|
+
if (d && d.ok) {
|
|
1870
|
+
setReviewText('')
|
|
1871
|
+
loadReviews()
|
|
1872
|
+
if (reviewText.trim()) api('GET', `/repos/${repo.owner}/${repo.name}/issues/${idx}/comments`).then((d2) => setComments(d2.comments || []))
|
|
1873
|
+
} else setMsg((d && d.error) || 'failed')
|
|
1874
|
+
}
|
|
1523
1875
|
const mergeBox = pr.state === 'open' ? h('div', { className: 'dgs-card', style: { margin: '10px 0', borderColor: 'var(--dsw-alias-state-success-primary)55' } },
|
|
1524
1876
|
h('div', { className: 'dgs-row', style: { gap: 8 } },
|
|
1525
1877
|
h('span', { style: { color: 'var(--dsw-alias-state-success-primary)', fontSize: 16 } }, '⑂'),
|
|
1526
|
-
h('span', { style: { color: 'var(--dsw-alias-state-success-primary)', fontSize: 13 } }, '该合并请求可以进行自动合并操作。')
|
|
1878
|
+
h('span', { style: { color: 'var(--dsw-alias-state-success-primary)', fontSize: 13 } }, '该合并请求可以进行自动合并操作。'),
|
|
1879
|
+
approvals > 0 ? h('span', { className: 'dgs-badge ok' }, approvals + ' 个评审通过') : null),
|
|
1880
|
+
h('div', { style: { margin: '10px 0', borderTop: '1px solid var(--dsw-alias-border-l2)', paddingTop: 10 } },
|
|
1881
|
+
h('textarea', { className: 'dgs-input', style: { minHeight: 48 }, placeholder: '评审意见(可选,会同时出现在对话中)', value: reviewText, onChange: (e) => setReviewText(e.target.value) }),
|
|
1882
|
+
h('div', { className: 'dgs-row', style: { marginTop: 6 } },
|
|
1883
|
+
h('button', { className: 'dgs-btn ghost', disabled: busy, onClick: () => submitReview(true) }, '✓ 通过评审'),
|
|
1884
|
+
h('button', { className: 'dgs-btn ghost', disabled: busy, onClick: () => submitReview(false) }, '✗ 请求修改'))),
|
|
1527
1885
|
h('div', { style: { margin: '10px 0' } },
|
|
1528
1886
|
h('label', { className: 'dgs-sub', style: { display: 'flex', gap: 6, alignItems: 'center', margin: '4px 0' } },
|
|
1529
1887
|
h('input', { type: 'radio', name: 'mergeStyle', checked: mergeStyle === 'merge', onChange: () => setMergeStyle('merge') }), '创建一个新的合并提交'),
|
|
@@ -1555,7 +1913,7 @@ window.__ModuleLoader__.load({
|
|
|
1555
1913
|
(comments || []).map((c) =>
|
|
1556
1914
|
h('div', { key: c.id, className: 'dgs-comment' },
|
|
1557
1915
|
h('div', { className: 'dgs-sub', style: { marginBottom: 4 } }, `${c.user || ''} 评论于 ${fmtDate(c.created)}`),
|
|
1558
|
-
h('div', { style: { whiteSpace: 'pre-wrap' } }, c.body
|
|
1916
|
+
h('div', { style: { whiteSpace: 'pre-wrap' } }, c.body ? linkifyIssues(c.body, repo) : '这个人很懒,什么都没留下。'))),
|
|
1559
1917
|
h('div', { className: 'dgs-card', style: { margin: '10px 0' } },
|
|
1560
1918
|
h('textarea', { className: 'dgs-input', placeholder: t('commentPlaceholder'), value: text, onChange: (e) => setText(e.target.value) }),
|
|
1561
1919
|
h('div', { className: 'dgs-row', style: { marginTop: 8 } },
|
|
@@ -1573,7 +1931,7 @@ window.__ModuleLoader__.load({
|
|
|
1573
1931
|
: h('table', { className: 'dgs-table' },
|
|
1574
1932
|
h('tbody', null, cmp.commits.map((c) =>
|
|
1575
1933
|
h('tr', { key: c.sha, style: { cursor: 'pointer' }, onClick: () => setSelCommit(c.sha) },
|
|
1576
|
-
h('td', null, h('div', { style: { fontWeight: 500 } }, (c.message || '').split('\n')[0]),
|
|
1934
|
+
h('td', null, h('div', { style: { fontWeight: 500 } }, linkifyIssues((c.message || '').split('\n')[0], repo)),
|
|
1577
1935
|
h('div', { className: 'dgs-sub' }, c.author)),
|
|
1578
1936
|
h('td', { className: 'dgs-sub', style: { textAlign: 'right' } },
|
|
1579
1937
|
h('a', { style: { color: 'var(--dsw-alias-state-business-primary)', cursor: 'pointer' } }, (c.sha || '').slice(0, 10)))))))
|
|
@@ -1794,16 +2152,46 @@ window.__ModuleLoader__.load({
|
|
|
1794
2152
|
r.prerelease ? h('span', { className: 'dgs-badge pri' }, '预发布') : null,
|
|
1795
2153
|
h('span', { style: { fontWeight: 600 } }, r.title),
|
|
1796
2154
|
h('span', { style: { flex: 1 } }),
|
|
1797
|
-
h('a', { className: 'dgs-sub', style: { cursor: 'pointer' }, onClick: () => { setEditId(r.id); setEditForm({ title: r.title || '', note: r.noteRaw || '' }) } }, '(编辑)')
|
|
2155
|
+
h('a', { className: 'dgs-sub', style: { cursor: 'pointer' }, onClick: () => { setEditId(r.id); setEditForm({ title: r.title || '', note: r.noteRaw || '' }) } }, '(编辑)'),
|
|
2156
|
+
h('a', { className: 'dgs-sub', style: { cursor: 'pointer', color: 'var(--dsw-alias-state-error-primary)' }, onClick: async () => {
|
|
2157
|
+
if (!confirm('删除发版 ' + r.tag + '(git 标签保留)?')) return
|
|
2158
|
+
const d = await api('DELETE', `/dsh/repos/${repo.owner}/${repo.name}/releases/${r.id}`)
|
|
2159
|
+
if (d && d.ok) reload(); else setMsg((d && d.error) || '删除失败')
|
|
2160
|
+
} }, '(删除)')),
|
|
1798
2161
|
h('div', { className: 'dgs-sub', style: { marginTop: 4 } },
|
|
1799
2162
|
(r.author || '') + ' · ' + timeAgo((r.createdAt || 0) * 1000) + ' 发布 · ' +
|
|
1800
2163
|
(r.behind ? '在该版本发布之后已有 ' + r.behind + ' 次代码提交到 ' + (r.target || '默认') + ' 分支' : '暂无后续提交')),
|
|
1801
2164
|
r.noteHtml ? h('div', { className: 'dgs-md', style: { marginTop: 8 }, dangerouslySetInnerHTML: { __html: r.noteHtml } }) : null,
|
|
1802
2165
|
h('div', { style: { marginTop: 10 } },
|
|
1803
|
-
h('div', {
|
|
2166
|
+
h('div', { className: 'dgs-row', style: { marginBottom: 4 } },
|
|
2167
|
+
h('div', { style: { fontWeight: 600, fontSize: 13 } }, '下载附件'),
|
|
2168
|
+
h('span', { style: { flex: 1 } }),
|
|
2169
|
+
h('label', { className: 'dgs-sub', style: { cursor: 'pointer' } }, '⭱ 上传附件',
|
|
2170
|
+
h('input', { type: 'file', style: { display: 'none' }, onChange: async (e) => {
|
|
2171
|
+
const f = e.target.files && e.target.files[0]
|
|
2172
|
+
e.target.value = ''
|
|
2173
|
+
if (!f) return
|
|
2174
|
+
if (f.size > 25 * 1024 * 1024) { setMsg('附件超过 25MB 上限'); return }
|
|
2175
|
+
const bytes = new Uint8Array(await f.arrayBuffer())
|
|
2176
|
+
let bin = ''
|
|
2177
|
+
for (let i = 0; i < bytes.length; i += 32768) bin += String.fromCharCode.apply(null, bytes.subarray(i, i + 32768))
|
|
2178
|
+
setMsg('')
|
|
2179
|
+
const d = await api('POST', `/dsh/repos/${repo.owner}/${repo.name}/releases/${r.id}/assets`, { name: f.name, contentBase64: btoa(bin) })
|
|
2180
|
+
if (d && d.ok) reload(); else setMsg((d && d.error) || '上传失败')
|
|
2181
|
+
} }))),
|
|
1804
2182
|
h('div', { className: 'dgs-row', style: { gap: 8 } },
|
|
1805
2183
|
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)'),
|
|
1806
|
-
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)'))
|
|
2184
|
+
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)')),
|
|
2185
|
+
(r.assets || []).length === 0 ? null
|
|
2186
|
+
: h('div', { style: { marginTop: 6 } }, r.assets.map((a) =>
|
|
2187
|
+
h('div', { key: a.id, className: 'dgs-row', style: { gap: 8, padding: '2px 0' } },
|
|
2188
|
+
h('a', { className: 'dgs-sub', style: { color: 'var(--dsw-alias-state-business-primary)' }, href: `/dsh-git-server/api/dsh/repos/${repo.owner}/${repo.name}/attachments/${a.id}`, download: a.name }, '📦 ' + a.name),
|
|
2189
|
+
h('span', { className: 'dgs-sub' }, fmtSize(a.size)),
|
|
2190
|
+
h('a', { className: 'dgs-sub', style: { cursor: 'pointer', color: 'var(--dsw-alias-state-error-primary)' }, onClick: async () => {
|
|
2191
|
+
if (!confirm('删除附件 ' + a.name + ' ?')) return
|
|
2192
|
+
await api('DELETE', `/dsh/repos/${repo.owner}/${repo.name}/attachments/${a.id}`)
|
|
2193
|
+
reload()
|
|
2194
|
+
} }, '删除')))))))),
|
|
1807
2195
|
(plainTags && plainTags.length > 0) ? h('div', { style: { marginTop: 16 } },
|
|
1808
2196
|
h('div', { style: { fontWeight: 700, margin: '4px 0 8px' } }, '未发版的标签'),
|
|
1809
2197
|
plainTags.map((tg) => h('div', { key: tg, className: 'dgs-issue' },
|
|
@@ -1935,20 +2323,44 @@ window.__ModuleLoader__.load({
|
|
|
1935
2323
|
|
|
1936
2324
|
function UserProfile({ name, t }) {
|
|
1937
2325
|
const [profile, setProfile] = useState(null)
|
|
1938
|
-
|
|
2326
|
+
const [showList, setShowList] = useState(null) // 'followers' | 'following' | null
|
|
2327
|
+
const [followBusy, setFollowBusy] = useState(false)
|
|
2328
|
+
const reload = useCallback(() => {
|
|
1939
2329
|
api('GET', '/dsh/users/' + encodeURIComponent(name)).then((d) => setProfile(d.data || d))
|
|
1940
2330
|
}, [name])
|
|
2331
|
+
useEffect(() => { reload() }, [reload])
|
|
1941
2332
|
if (profile === null) return h('div', { className: 'dgs-empty' }, t('loading'))
|
|
2333
|
+
const followBtn = profile.isSelf === false ? h('button', {
|
|
2334
|
+
className: 'dgs-btn' + (profile.isFollowing ? ' ghost' : ''), style: { marginTop: 10, width: '100%' }, disabled: followBusy,
|
|
2335
|
+
onClick: async () => {
|
|
2336
|
+
setFollowBusy(true)
|
|
2337
|
+
const d = await api('POST', '/dsh/users/' + encodeURIComponent(name) + '/follow')
|
|
2338
|
+
setFollowBusy(false)
|
|
2339
|
+
if (d && typeof d.on === 'boolean') reload()
|
|
2340
|
+
},
|
|
2341
|
+
}, profile.isFollowing ? '取消关注' : '+ 关注') : null
|
|
2342
|
+
const countBtn = (k, n, label) => h('a', {
|
|
2343
|
+
className: 'dgs-sub', style: { cursor: 'pointer', color: showList === k ? 'var(--dsw-alias-state-business-primary)' : undefined },
|
|
2344
|
+
onClick: () => setShowList(showList === k ? null : k),
|
|
2345
|
+
}, `${n} ${label}`)
|
|
2346
|
+
const listNames = showList === 'followers' ? profile.followers : showList === 'following' ? profile.following : []
|
|
1942
2347
|
return h('div', { className: 'dgs-settings-layout' },
|
|
1943
2348
|
h('div', { className: 'dgs-settings-nav', style: { width: 230 } },
|
|
1944
2349
|
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 } },
|
|
1945
2350
|
(name || '?').slice(0, 1).toUpperCase()),
|
|
1946
2351
|
h('div', { className: 'dgs-h1', style: { fontSize: 17 } }, name),
|
|
1947
2352
|
profile.isAdmin ? h('span', { className: 'dgs-badge ok', style: { marginTop: 4 } }, 'admin') : null,
|
|
2353
|
+
followBtn,
|
|
1948
2354
|
h('div', { className: 'dgs-sub', style: { marginTop: 10 } }, profile.email ? '✉ ' + profile.email : ''),
|
|
1949
2355
|
h('div', { className: 'dgs-sub', style: { marginTop: 4 } }, '🕐 加入于 ' + (profile.created ? new Date(profile.created * 1000).toLocaleDateString() : '')),
|
|
1950
2356
|
h('div', { className: 'dgs-sub', style: { marginTop: 10 } },
|
|
1951
|
-
|
|
2357
|
+
countBtn('followers', profile.followers.length, '关注者'),
|
|
2358
|
+
' - ',
|
|
2359
|
+
countBtn('following', profile.following.length, '关注中')),
|
|
2360
|
+
showList ? h('div', { style: { marginTop: 8, borderTop: '1px solid var(--dsw-alias-border-l2)', paddingTop: 8 } },
|
|
2361
|
+
listNames.length === 0 ? h('div', { className: 'dgs-sub' }, '—')
|
|
2362
|
+
: listNames.map((u) => h('div', { key: u, style: { padding: '3px 0' } },
|
|
2363
|
+
h('a', { className: 'dgs-sub', style: { color: 'var(--dsw-alias-state-business-primary)', cursor: 'pointer' }, onClick: () => { setProfile(null); setShowList(null); nav('/u/' + u) } }, '@' + u)))) : null),
|
|
1952
2364
|
h('div', { style: { flex: 1, minWidth: 0 } },
|
|
1953
2365
|
h('div', { style: { fontWeight: 700, margin: '4px 0 10px' } }, '仓库'),
|
|
1954
2366
|
profile.repos.length === 0 ? h('div', { className: 'dgs-empty' }, '—') : null,
|
|
@@ -2048,15 +2460,80 @@ window.__ModuleLoader__.load({
|
|
|
2048
2460
|
sub === 'panel' ? panel : sub === 'users' ? usersTab : sub === 'repos' ? reposTab : orgsTab))
|
|
2049
2461
|
}
|
|
2050
2462
|
|
|
2463
|
+
// ── 通知中心 ───────────────────────────────────────────────────────────────
|
|
2464
|
+
|
|
2465
|
+
function Notifications({ t }) {
|
|
2466
|
+
const [data, setData] = useState(null)
|
|
2467
|
+
const reload = useCallback(() => {
|
|
2468
|
+
api('GET', '/dsh/notifications').then((d) => setData(d && Array.isArray(d.items) ? d : { items: [], unread: 0 }))
|
|
2469
|
+
}, [])
|
|
2470
|
+
useEffect(() => { reload() }, [reload])
|
|
2471
|
+
const reasonLabel = { mentioned: '@ 提及了你', assigned: '指派给你', poster: '你发起的', comment: '有新评论' }
|
|
2472
|
+
const open = (n) => {
|
|
2473
|
+
api('POST', `/dsh/repos/${n.repoOwner}/${n.repoName}/issues/${n.index}/read`).catch(() => {})
|
|
2474
|
+
nav('/r/' + n.repoOwner + '/' + n.repoName + '/' + (n.isPull ? 'pulls' : 'issues') + '/' + n.index)
|
|
2475
|
+
}
|
|
2476
|
+
return h('div', null,
|
|
2477
|
+
h('div', { className: 'dgs-row', style: { margin: '8px 0 12px' } },
|
|
2478
|
+
h('span', { style: { fontWeight: 700 } }, '未读通知' + (data && data.unread ? '(' + data.unread + ')' : '')),
|
|
2479
|
+
h('span', { style: { flex: 1 } }),
|
|
2480
|
+
h('button', { className: 'dgs-btn ghost', disabled: !data || !data.unread, onClick: async () => { await api('POST', '/dsh/notifications/read-all'); reload() } }, '全部标记已读')),
|
|
2481
|
+
data === null ? h('div', { className: 'dgs-empty' }, t('loading'))
|
|
2482
|
+
: data.items.length === 0 ? h('div', { className: 'dgs-empty' }, '没有未读通知 🎉')
|
|
2483
|
+
: data.items.map((n) =>
|
|
2484
|
+
h('div', { key: n.repoOwner + '/' + n.repoName + '#' + n.index, className: 'dgs-issue', style: { cursor: 'pointer', display: 'flex', alignItems: 'center', gap: 10 }, onClick: () => open(n) },
|
|
2485
|
+
h('span', { className: 'dgs-issue-num' + (n.isClosed ? ' closed' : '') }, '#' + n.index),
|
|
2486
|
+
h('div', { style: { flex: 1, minWidth: 0 } },
|
|
2487
|
+
h('div', null, h('span', { className: 'dgs-issue-title' }, n.title), n.isPull ? h('span', { className: 'dgs-badge', style: { marginLeft: 6 } }, 'PR') : null),
|
|
2488
|
+
h('div', { className: 'dgs-sub', style: { marginTop: 2 } }, n.repoOwner + '/' + n.repoName + ' · ' + (reasonLabel[n.reason] || '有更新') + ' · ' + timeAgo(n.updatedAt))),
|
|
2489
|
+
h('span', { className: 'dgs-sub', style: { flex: 'none' } }, '›'))))
|
|
2490
|
+
}
|
|
2491
|
+
|
|
2492
|
+
// ── 我的工单 / 我的 PR(跨仓库聚合) ────────────────────────────────────────
|
|
2493
|
+
|
|
2494
|
+
function MyIssues({ t, isPull }) {
|
|
2495
|
+
const [filter, setFilter] = useState('all')
|
|
2496
|
+
const [state, setState] = useState('open')
|
|
2497
|
+
const [list, setList] = useState(null)
|
|
2498
|
+
useEffect(() => {
|
|
2499
|
+
setList(null)
|
|
2500
|
+
api('GET', `/dsh/my/issues?type=${isPull ? 'pulls' : 'issues'}&state=${state}&filter=${filter}`)
|
|
2501
|
+
.then((d) => setList(Array.isArray(d) ? d : []))
|
|
2502
|
+
}, [isPull, state, filter])
|
|
2503
|
+
const fBtn = (k, label) => h('button', { key: k, className: 'dgs-subtab' + (filter === k ? ' active' : ''), onClick: () => setFilter(k) }, label)
|
|
2504
|
+
return h('div', null,
|
|
2505
|
+
h('div', { className: 'dgs-row', style: { margin: '8px 0 12px' } },
|
|
2506
|
+
h('button', { className: 'dgs-pill' + (state === 'open' ? ' active' : ''), onClick: () => setState('open') }, '⊘ ' + t('openState')),
|
|
2507
|
+
h('button', { className: 'dgs-pill closed' + (state === 'closed' ? ' active' : ''), onClick: () => setState('closed') }, '✓ ' + t('closedState')),
|
|
2508
|
+
h('span', { style: { flex: 1 } }),
|
|
2509
|
+
h('div', { className: 'dgs-subtabs', style: { margin: 0 } },
|
|
2510
|
+
fBtn('all', '与我相关'), fBtn('created', '我创建的'), fBtn('assigned', '指派给我的'))),
|
|
2511
|
+
list === null ? h('div', { className: 'dgs-empty' }, t('loading'))
|
|
2512
|
+
: list.length === 0 ? h('div', { className: 'dgs-empty' }, '—')
|
|
2513
|
+
: list.map((i) =>
|
|
2514
|
+
h('div', {
|
|
2515
|
+
key: i.repoOwner + '/' + i.repoName + '#' + i.index, className: 'dgs-issue', style: { cursor: 'pointer', display: 'flex', alignItems: 'center', gap: 10 },
|
|
2516
|
+
onClick: () => nav('/r/' + i.repoOwner + '/' + i.repoName + '/' + (isPull ? 'pulls' : 'issues') + '/' + i.index),
|
|
2517
|
+
},
|
|
2518
|
+
h('span', { className: 'dgs-issue-num' + (i.state === 'open' ? '' : ' closed') }, '#' + i.index),
|
|
2519
|
+
h('div', { style: { flex: 1, minWidth: 0 } },
|
|
2520
|
+
h('div', { className: 'dgs-issue-title' }, i.title),
|
|
2521
|
+
h('div', { className: 'dgs-sub', style: { marginTop: 2 } },
|
|
2522
|
+
i.repoOwner + '/' + i.repoName + ' · ' + (i.author || '') + ' · ' + timeAgo(i.updatedAt) + (i.assignee ? ' · @' + i.assignee : ''))),
|
|
2523
|
+
h('span', { className: 'dgs-sub', style: { flex: 'none' } }, '💬 ' + (i.comments || 0)))))
|
|
2524
|
+
}
|
|
2525
|
+
|
|
2051
2526
|
// ── 主页(仓库列表 + 建仓) ────────────────────────────────────────────────
|
|
2052
2527
|
|
|
2053
|
-
function GitPage({ onClose, t }) {
|
|
2528
|
+
function GitPage({ onClose, t, standalone }) {
|
|
2054
2529
|
useEffect(ensureStyles, [])
|
|
2055
2530
|
const route = useHashRoute()
|
|
2056
2531
|
const [me, setMe] = useState(null)
|
|
2057
2532
|
const [err, setErr] = useState('')
|
|
2058
2533
|
const [name, setName] = useState('')
|
|
2059
2534
|
const [isPrivate, setIsPrivate] = useState(false)
|
|
2535
|
+
const [createMode, setCreateMode] = useState('new') // new | import
|
|
2536
|
+
const [importForm, setImportForm] = useState({ url: '', mirror: false })
|
|
2060
2537
|
const [busy, setBusy] = useState(false)
|
|
2061
2538
|
const [toast, setToast] = useState('')
|
|
2062
2539
|
const reload = useCallback(() => {
|
|
@@ -2069,29 +2546,98 @@ window.__ModuleLoader__.load({
|
|
|
2069
2546
|
window.addEventListener('keydown', onKey)
|
|
2070
2547
|
return () => window.removeEventListener('keydown', onKey)
|
|
2071
2548
|
}, [onClose])
|
|
2549
|
+
// 中栏接管开合:挂载即打开 → html[data-dsh-git-active] + 广播互斥事件;卸载(关闭)时清理。
|
|
2550
|
+
// standalone(__boot 开发挂载)不动全局。
|
|
2551
|
+
useEffect(() => {
|
|
2552
|
+
if (standalone) return undefined
|
|
2553
|
+
for (const attr of DGS_OTHER_ACTIVE_ATTRS) document.documentElement.removeAttribute(attr)
|
|
2554
|
+
document.documentElement.setAttribute(DGS_ACTIVE_ATTR, '')
|
|
2555
|
+
document.dispatchEvent(new CustomEvent(DGS_ACTIVATE_EVENT, { detail: DGS_PANEL_NAME }))
|
|
2556
|
+
return () => { document.documentElement.removeAttribute(DGS_ACTIVE_ATTR) }
|
|
2557
|
+
}, [standalone])
|
|
2558
|
+
useEffect(() => {
|
|
2559
|
+
if (standalone) return undefined
|
|
2560
|
+
const onOther = (e) => { if (e && e.detail !== DGS_PANEL_NAME) onClose() }
|
|
2561
|
+
document.addEventListener(DGS_ACTIVATE_EVENT, onOther)
|
|
2562
|
+
return () => document.removeEventListener(DGS_ACTIVATE_EVENT, onOther)
|
|
2563
|
+
}, [standalone, onClose])
|
|
2564
|
+
// 点侧栏会话行自动关面板(自家入口子树豁免)
|
|
2565
|
+
useEffect(() => {
|
|
2566
|
+
if (standalone) return undefined
|
|
2567
|
+
const onClickRow = (e) => {
|
|
2568
|
+
const target = e.target
|
|
2569
|
+
if (!(target instanceof Element)) return
|
|
2570
|
+
if (target.closest('[' + GIT_ENTRY_ATTR + ']')) return
|
|
2571
|
+
if (target.closest(DGS_SIDEBAR_ROW_SELECTOR)) onClose()
|
|
2572
|
+
}
|
|
2573
|
+
document.addEventListener('click', onClickRow, true)
|
|
2574
|
+
return () => document.removeEventListener('click', onClickRow, true)
|
|
2575
|
+
}, [standalone, onClose])
|
|
2072
2576
|
const notify = (m) => { setToast(m); setTimeout(() => setToast(''), 1600) }
|
|
2577
|
+
const [unread, setUnread] = useState(0)
|
|
2578
|
+
useEffect(() => {
|
|
2579
|
+
api('GET', '/dsh/notifications/count').then((d) => { if (d && typeof d.unread === 'number') setUnread(d.unread) }).catch(() => {})
|
|
2580
|
+
}, [route])
|
|
2073
2581
|
const seg = route.split('/').filter(Boolean)
|
|
2074
2582
|
const view = seg[0] || 'repos'
|
|
2075
|
-
const
|
|
2583
|
+
const deepTab = seg[3] ? decodeURIComponent(seg[3]) : ''
|
|
2584
|
+
// 深链:#/r/:o/:r 之外支持 /issues/:idx /pulls/:idx /src/:ref/:path...
|
|
2585
|
+
const repoRoute = view === 'r' && seg[1] && seg[2] ? {
|
|
2586
|
+
owner: seg[1], name: seg[2],
|
|
2587
|
+
deep: {
|
|
2588
|
+
tab: deepTab,
|
|
2589
|
+
ref: deepTab === 'src' ? decodeURIComponent(seg[4] || '') : '',
|
|
2590
|
+
arg: deepTab && deepTab !== 'src' ? decodeURIComponent(seg[4] || '') : '',
|
|
2591
|
+
filePath: deepTab === 'src' && seg.length > 5 ? seg.slice(5).map((s) => decodeURIComponent(s)).join('/') : '',
|
|
2592
|
+
},
|
|
2593
|
+
} : null
|
|
2076
2594
|
const body = err ? h('div', { className: 'dgs-card dgs-err' }, err,
|
|
2077
2595
|
h('button', { className: 'dgs-btn ghost', style: { marginLeft: 12 }, onClick: reload }, t('retry')))
|
|
2078
|
-
: repoRoute ? h(RepoBrowser, { repo: repoRoute, t, onBack: () => nav('/repos') })
|
|
2596
|
+
: repoRoute ? h(RepoBrowser, { key: route, repo: repoRoute, t, deep: repoRoute.deep, onBack: () => nav('/repos') })
|
|
2079
2597
|
: view === 'explore' ? h(Explore, { t })
|
|
2080
2598
|
: view === 'orgs' ? h(Orgs, { t })
|
|
2081
2599
|
: view === 'org' && seg[1] ? h(OrgView, { name: seg[1], t })
|
|
2082
2600
|
: view === 'u' && seg[1] ? h(UserProfile, { name: decodeURIComponent(seg[1]), t })
|
|
2083
2601
|
: view === 'admin' ? h(Admin, { t })
|
|
2602
|
+
: view === 'notifications' ? h(Notifications, { t })
|
|
2603
|
+
: view === 'myissues' ? h(MyIssues, { t, isPull: false })
|
|
2604
|
+
: view === 'mypulls' ? h(MyIssues, { t, isPull: true })
|
|
2084
2605
|
: me === null ? h('div', { className: 'dgs-empty' }, t('loading'))
|
|
2085
2606
|
: h('div', null,
|
|
2086
2607
|
h('div', { className: 'dgs-card' },
|
|
2087
2608
|
h('div', { className: 'dgs-row' },
|
|
2088
2609
|
h('span', { style: { fontWeight: 700 } }, t('newRepo')),
|
|
2610
|
+
h('div', { className: 'dgs-subtabs', style: { margin: 0 } },
|
|
2611
|
+
h('button', { className: 'dgs-subtab' + (createMode === 'new' ? ' active' : ''), onClick: () => setCreateMode('new') }, '新建'),
|
|
2612
|
+
h('button', { className: 'dgs-subtab' + (createMode === 'import' ? ' active' : ''), onClick: () => setCreateMode('import') }, '导入'))),
|
|
2613
|
+
createMode === 'new' ? h('div', { className: 'dgs-row', style: { marginTop: 8 } },
|
|
2089
2614
|
h('input', { className: 'dgs-input', placeholder: t('repoName'), value: name,
|
|
2090
2615
|
onChange: (e) => setName(e.target.value),
|
|
2091
2616
|
onKeyDown: (e) => { if (e.key === 'Enter' && name.trim()) create() } }),
|
|
2092
2617
|
h('label', { className: 'dgs-sub', style: { display: 'flex', gap: 4, alignItems: 'center' } },
|
|
2093
2618
|
h('input', { type: 'checkbox', checked: isPrivate, onChange: (e) => setIsPrivate(e.target.checked) }), t('private')),
|
|
2094
|
-
h('button', { className: 'dgs-btn', disabled: busy || !name.trim(), onClick: create }, t('create')))
|
|
2619
|
+
h('button', { className: 'dgs-btn', disabled: busy || !name.trim(), onClick: create }, t('create')))
|
|
2620
|
+
: h('div', null,
|
|
2621
|
+
h('div', { className: 'dgs-row', style: { marginTop: 8 } },
|
|
2622
|
+
h('input', {
|
|
2623
|
+
className: 'dgs-input', placeholder: 'https://github.com/owner/repo.git', value: importForm.url,
|
|
2624
|
+
onChange: (e) => {
|
|
2625
|
+
const url = e.target.value
|
|
2626
|
+
setImportForm({ ...importForm, url })
|
|
2627
|
+
if (!name) {
|
|
2628
|
+
const m = /\/([^/]+?)(?:\.git)?\/?$/.exec(url.trim())
|
|
2629
|
+
if (m) setName(m[1])
|
|
2630
|
+
}
|
|
2631
|
+
},
|
|
2632
|
+
})),
|
|
2633
|
+
h('div', { className: 'dgs-row', style: { marginTop: 8 } },
|
|
2634
|
+
h('input', { className: 'dgs-input', placeholder: t('repoName'), value: name, onChange: (e) => setName(e.target.value) }),
|
|
2635
|
+
h('label', { className: 'dgs-sub', style: { display: 'flex', gap: 4, alignItems: 'center' } },
|
|
2636
|
+
h('input', { type: 'checkbox', checked: importForm.mirror, onChange: (e) => setImportForm({ ...importForm, mirror: e.target.checked }) }), '镜像同步'),
|
|
2637
|
+
h('label', { className: 'dgs-sub', style: { display: 'flex', gap: 4, alignItems: 'center' } },
|
|
2638
|
+
h('input', { type: 'checkbox', checked: isPrivate, onChange: (e) => setIsPrivate(e.target.checked) }), t('private')),
|
|
2639
|
+
h('button', { className: 'dgs-btn', disabled: busy || !name.trim() || !importForm.url.trim(), onClick: importRepo }, busy ? '导入中…' : '导入')),
|
|
2640
|
+
h('div', { className: 'dgs-sub', style: { marginTop: 6 } }, '私有仓库可在地址里带凭据:https://user:token@host/owner/repo.git;勾选「镜像同步」后每小时自动拉取上游更新。'))),
|
|
2095
2641
|
h('div', { style: { height: 10 } }),
|
|
2096
2642
|
h('div', { style: { fontWeight: 700, margin: '4px 0 10px' } }, `${t('myRepos')}(${me.repos.length})`),
|
|
2097
2643
|
me.repos.length === 0 ? h('div', { className: 'dgs-empty' }, t('emptyRepo'))
|
|
@@ -2112,7 +2658,7 @@ window.__ModuleLoader__.load({
|
|
|
2112
2658
|
h('div', { className: 'dgs-row' }, title, badge, h('span', { style: { flex: 1 } }), stats, del), desc)
|
|
2113
2659
|
}
|
|
2114
2660
|
|
|
2115
|
-
const navItems = [['repos', t('myRepos')], ['explore', '探索'], ['orgs', '组织'], ['admin', '管理']]
|
|
2661
|
+
const navItems = [['repos', t('myRepos')], ['myissues', '工单'], ['mypulls', 'PR'], ['notifications', '通知'], ['explore', '探索'], ['orgs', '组织'], ['admin', '管理']]
|
|
2116
2662
|
return h('div', { className: 'dgs-page' },
|
|
2117
2663
|
h('div', { className: 'dgs-head' },
|
|
2118
2664
|
h('span', { className: 'dgs-h1' }, t('title')),
|
|
@@ -2120,7 +2666,9 @@ window.__ModuleLoader__.load({
|
|
|
2120
2666
|
navItems.map(([k, label]) =>
|
|
2121
2667
|
h('button', { key: k, className: 'dgs-btn ghost',
|
|
2122
2668
|
style: view === k ? { color: 'var(--dsw-alias-state-business-primary)', borderColor: 'var(--dsw-alias-state-business-primary)55' } : undefined,
|
|
2123
|
-
onClick: () => nav('/' + k) },
|
|
2669
|
+
onClick: () => nav('/' + k) },
|
|
2670
|
+
label,
|
|
2671
|
+
k === 'notifications' && unread > 0 ? h('span', { className: 'dgs-notif-dot' }, unread > 99 ? '99+' : unread) : null))),
|
|
2124
2672
|
h('button', { className: 'dgs-close', onClick: onClose, title: 'Esc' }, '✕')),
|
|
2125
2673
|
h('div', { className: 'dgs-body' }, body),
|
|
2126
2674
|
toast ? h('div', { className: 'dgs-toast' }, toast) : null)
|
|
@@ -2131,6 +2679,20 @@ window.__ModuleLoader__.load({
|
|
|
2131
2679
|
setBusy(false)
|
|
2132
2680
|
if (d.ok) { setName(''); notify('✓ ' + name); reload() } else notify(d.error || 'failed')
|
|
2133
2681
|
}
|
|
2682
|
+
|
|
2683
|
+
async function importRepo() {
|
|
2684
|
+
setBusy(true)
|
|
2685
|
+
const d = await api('POST', '/dsh/migrate', {
|
|
2686
|
+
cloneAddr: importForm.url.trim(), name: name.trim(), private: isPrivate, mirror: importForm.mirror,
|
|
2687
|
+
})
|
|
2688
|
+
setBusy(false)
|
|
2689
|
+
if (d && d.ok) {
|
|
2690
|
+
const repoName = name
|
|
2691
|
+
setName(''); setImportForm({ url: '', mirror: false })
|
|
2692
|
+
notify('✓ ' + repoName)
|
|
2693
|
+
nav('/r/' + d.owner + '/' + d.name)
|
|
2694
|
+
} else notify((d && d.error) || 'failed')
|
|
2695
|
+
}
|
|
2134
2696
|
}
|
|
2135
2697
|
|
|
2136
2698
|
// ── 设置节(服务配置,无 iframe) ──────────────────────────────────────────
|
|
@@ -2214,10 +2776,21 @@ window.__ModuleLoader__.load({
|
|
|
2214
2776
|
try { gitPageHost.el.remove() } catch {}
|
|
2215
2777
|
gitPageHost = null
|
|
2216
2778
|
}
|
|
2779
|
+
function gitConversationColumn() {
|
|
2780
|
+
return document.querySelector('[data-pane="conversation"], [class*="centerCol"], .dshDesktopConversationSurface')
|
|
2781
|
+
}
|
|
2217
2782
|
function openGitPage(t) {
|
|
2218
2783
|
if (gitPageHost) { closeGitPage(); return }
|
|
2219
2784
|
const el = document.createElement('div')
|
|
2220
|
-
|
|
2785
|
+
el.setAttribute('data-dsh-git-view', '')
|
|
2786
|
+
el.className = 'dsh-git-view'
|
|
2787
|
+
const column = gitConversationColumn()
|
|
2788
|
+
if (column) {
|
|
2789
|
+
column.appendChild(el)
|
|
2790
|
+
} else {
|
|
2791
|
+
el.setAttribute('data-dsh-git-fallback', '')
|
|
2792
|
+
document.body.appendChild(el)
|
|
2793
|
+
}
|
|
2221
2794
|
const root = require('react-dom/client').createRoot(el)
|
|
2222
2795
|
gitPageHost = { el, root }
|
|
2223
2796
|
root.render(h(GitPage, { onClose: closeGitPage, t }))
|
|
@@ -2269,6 +2842,7 @@ window.__ModuleLoader__.load({
|
|
|
2269
2842
|
.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}
|
|
2270
2843
|
.dsh-git-entry .dsh-git-entry-dot{width:6px;height:6px;border-radius:6px;background:var(--dsw-alias-state-success-primary);display:inline-block}
|
|
2271
2844
|
[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}
|
|
2845
|
+
html[data-dsh-git-active] .dsh-git-entry{background:var(--dsw-active,rgba(128,128,128,.18));color:var(--dsw-text-primary,inherit);font-weight:500}
|
|
2272
2846
|
[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}
|
|
2273
2847
|
`
|
|
2274
2848
|
document.head.appendChild(style)
|
|
@@ -2282,9 +2856,17 @@ window.__ModuleLoader__.load({
|
|
|
2282
2856
|
entry.addEventListener('click', () => openGitPage(t))
|
|
2283
2857
|
const stats = entry.querySelector('.dsh-git-entry-stats')
|
|
2284
2858
|
const refreshStats = () => {
|
|
2859
|
+
let repos = ''
|
|
2285
2860
|
fetch(API + '/me').then((r) => r.json()).then((d) => {
|
|
2286
|
-
if (
|
|
2287
|
-
}).catch(() => {})
|
|
2861
|
+
if (d && Array.isArray(d.repos)) repos = String(d.repos.length)
|
|
2862
|
+
}).catch(() => {}).then(() =>
|
|
2863
|
+
fetch(API + '/dsh/notifications/count').then((r) => r.json()).then((d) => {
|
|
2864
|
+
if (!stats) return
|
|
2865
|
+
const unread = d && typeof d.unread === 'number' ? d.unread : 0
|
|
2866
|
+
stats.textContent = repos + (unread > 0 ? ' · ' + unread + ' 未读' : '')
|
|
2867
|
+
if (unread > 0) stats.style.color = 'var(--dsw-alias-state-error-primary)'
|
|
2868
|
+
else stats.style.color = ''
|
|
2869
|
+
}).catch(() => { if (stats) stats.textContent = repos }))
|
|
2288
2870
|
}
|
|
2289
2871
|
refreshStats()
|
|
2290
2872
|
const poll = setInterval(refreshStats, 30000)
|
|
@@ -2368,7 +2950,7 @@ window.__ModuleLoader__.load({
|
|
|
2368
2950
|
ensureStyles()
|
|
2369
2951
|
let t = opts.t || ((key) => ZH[key] ?? EN[key] ?? key)
|
|
2370
2952
|
const root = require('react-dom/client').createRoot(container)
|
|
2371
|
-
root.render(h(GitPage, { onClose: () => {}, t }))
|
|
2953
|
+
root.render(h(GitPage, { onClose: () => {}, t, standalone: true }))
|
|
2372
2954
|
return root
|
|
2373
2955
|
},
|
|
2374
2956
|
apply(ctx) {
|