@weibaohui/dsh-git-server 0.1.4 → 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 +70 -2
- package/client/bundle.js +597 -69
- package/client/index.js +597 -69
- 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
|
@@ -218,6 +218,24 @@ window.__ModuleLoader__.load({
|
|
|
218
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)}
|
|
219
219
|
.dgs-md table{border-collapse:collapse}
|
|
220
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}
|
|
221
239
|
</style>`
|
|
222
240
|
document.head.appendChild(holder)
|
|
223
241
|
}
|
|
@@ -247,25 +265,133 @@ window.__ModuleLoader__.load({
|
|
|
247
265
|
}
|
|
248
266
|
const nav = (hash) => { location.hash = hash }
|
|
249
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
|
+
|
|
250
371
|
// ── 全屏管理页 ────────────────────────────────────────────────────────────
|
|
251
372
|
|
|
252
|
-
function RepoBrowser({ repo, t, onBack }) {
|
|
253
|
-
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
|
+
})
|
|
254
378
|
const [prPreset, setPrPreset] = useState(null)
|
|
255
379
|
const [issuesSub, setIssuesSub] = useState(null)
|
|
256
380
|
const [ov, setOv] = useState(null)
|
|
257
|
-
const [rev, setRev] = useState('')
|
|
381
|
+
const [rev, setRev] = useState(deep && deep.tab === 'src' ? deep.ref || '' : '')
|
|
258
382
|
const [cloneUrl, setCloneUrl] = useState('')
|
|
259
383
|
const [copied, setCopied] = useState(false)
|
|
260
384
|
const [star, setStar] = useState(null)
|
|
261
385
|
const [watch, setWatch] = useState(null)
|
|
262
386
|
const [forkBusy, setForkBusy] = useState(false)
|
|
387
|
+
const [pop, setPop] = useState(null) // 'star' | 'watch' | 'fork' | null
|
|
388
|
+
const [forkList, setForkList] = useState(null)
|
|
263
389
|
const [msg, setMsg] = useState('')
|
|
264
390
|
useEffect(() => {
|
|
265
391
|
api('GET', `/dsh/repos/${repo.owner}/${repo.name}/overview`).then((d) => {
|
|
266
392
|
if (!d.defaultBranch) return
|
|
267
393
|
setOv(d)
|
|
268
|
-
setRev(d.defaultBranch)
|
|
394
|
+
setRev((cur) => cur || d.defaultBranch)
|
|
269
395
|
})
|
|
270
396
|
fetch(API + '/status').then((r) => r.json()).then((st) => {
|
|
271
397
|
if (st && st.running) setCloneUrl('git clone http://' + location.hostname + ':' + (st.port || 3400) + '/' + repo.owner + '/' + repo.name + '.git')
|
|
@@ -296,15 +422,37 @@ window.__ModuleLoader__.load({
|
|
|
296
422
|
h('span', { className: 'dgs-h1' }, repo.owner + '/' + repo.name),
|
|
297
423
|
repo.private || (ov && ov.private) ? h('span', { className: 'dgs-badge pri' }, t('private')) : null,
|
|
298
424
|
h('span', { style: { flex: 1 } }),
|
|
425
|
+
h('div', { style: { position: 'relative', display: 'flex', alignItems: 'center', gap: 10 } },
|
|
299
426
|
star ? h('span', { className: 'dgs-labeled' },
|
|
300
427
|
h('button', { className: 'dgs-btn ghost', onClick: () => toggle('star') }, star.on ? '★ Unstar' : '☆ Star'),
|
|
301
|
-
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,
|
|
302
429
|
watch ? h('span', { className: 'dgs-labeled' },
|
|
303
430
|
h('button', { className: 'dgs-btn ghost', onClick: () => toggle('watch') }, watch.on ? '👁 Unwatch' : '👁 Watch'),
|
|
304
|
-
h('span', { className: 'dgs-labeled-count' }, watch.count)) : null,
|
|
305
|
-
h('
|
|
306
|
-
h('
|
|
307
|
-
|
|
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),
|
|
308
456
|
),
|
|
309
457
|
msg ? h('div', { className: 'dgs-err' }, msg) : null,
|
|
310
458
|
h('div', { className: 'dgs-tabs' },
|
|
@@ -335,19 +483,19 @@ window.__ModuleLoader__.load({
|
|
|
335
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: '下载' }, '⭳'))
|
|
336
484
|
: null))
|
|
337
485
|
: null,
|
|
338
|
-
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 : '' }),
|
|
339
487
|
tab === 'commits' && h(Commits, { repo, rev: rev || 'master', t }),
|
|
340
488
|
tab === 'branches' && h(Branches, { repo, t, onNewPR: (head, base) => { setPrPreset({ head, base }); setTab('pulls') } }),
|
|
341
|
-
tab === 'issues' && h(IssuesArea, { repo, t, initialSub: issuesSub || undefined }),
|
|
342
|
-
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 }),
|
|
343
491
|
tab === 'wiki' && h(WikiView, { repo, t }),
|
|
344
492
|
tab === 'releases' && h(Releases, { repo, t }),
|
|
345
493
|
tab === 'settings' && h(RepoSettings, { repo, t }),
|
|
346
494
|
)
|
|
347
495
|
}
|
|
348
496
|
|
|
349
|
-
function FileTree({ repo, rev, t, overview, onOverview }) {
|
|
350
|
-
const [path, setPath] = useState('')
|
|
497
|
+
function FileTree({ repo, rev, t, overview, onOverview, initialPath, cloneUrl }) {
|
|
498
|
+
const [path, setPath] = useState(() => dirnameOf(initialPath || ''))
|
|
351
499
|
const [entries, setEntries] = useState(null)
|
|
352
500
|
const [file, setFile] = useState(null)
|
|
353
501
|
const [err, setErr] = useState('')
|
|
@@ -361,6 +509,9 @@ window.__ModuleLoader__.load({
|
|
|
361
509
|
const [editText, setEditText] = useState('')
|
|
362
510
|
const [editMsg, setEditMsg] = useState('')
|
|
363
511
|
const [histMode, setHistMode] = useState(null)
|
|
512
|
+
const [searchQ, setSearchQ] = useState('')
|
|
513
|
+
const [hits, setHits] = useState(null) // null=未搜索;[]=无结果
|
|
514
|
+
const [deepDone, setDeepDone] = useState(false)
|
|
364
515
|
const load = useCallback((p) => {
|
|
365
516
|
setFile(null); setEntries(null); setErr(''); setBlameOn(false); setEditing(false); setHistMode(null)
|
|
366
517
|
api('GET', `/dsh/repos/${repo.owner}/${repo.name}/tree?ref=${encodeURIComponent(rev)}&path=${encodeURIComponent(p)}`)
|
|
@@ -376,20 +527,38 @@ window.__ModuleLoader__.load({
|
|
|
376
527
|
if (p !== '') setReadme(null)
|
|
377
528
|
}, [repo.owner, repo.name, rev])
|
|
378
529
|
useEffect(() => { load(path) }, [load, path])
|
|
379
|
-
const
|
|
380
|
-
|
|
381
|
-
if (e.type === 'dir' || e.type === 'tree') { setPath(p); return }
|
|
382
|
-
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)}`)
|
|
383
532
|
.then(async (d) => {
|
|
384
|
-
|
|
533
|
+
if (!d.ok) { setPath(fp); setFile(null); return } // 目录型永久链接:落到目录
|
|
534
|
+
const isMd = /\.md$/i.test(name)
|
|
385
535
|
let html = ''
|
|
386
|
-
if (isMd
|
|
536
|
+
if (isMd) {
|
|
387
537
|
const r = await api('POST', '/dsh/markdown', { text: d.text })
|
|
388
538
|
html = r.html || ''
|
|
389
539
|
}
|
|
390
|
-
setFile({ name:
|
|
540
|
+
setFile({ name, path: fp, text: d.text, size: size || 0, isMd, html })
|
|
391
541
|
})
|
|
392
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
|
+
}
|
|
393
562
|
const startCreate = (mode) => {
|
|
394
563
|
setNewFile(mode); setMsg('')
|
|
395
564
|
setNf({ path: '', content: '', message: '' })
|
|
@@ -420,6 +589,11 @@ window.__ModuleLoader__.load({
|
|
|
420
589
|
h('span', { className: 'dgs-crumb' + (i === arr.length - 1 ? ' cur' : ''), onClick: () => setPath(arr.slice(0, i + 1).join('/')) }, seg),
|
|
421
590
|
i < arr.length - 1 ? h('span', { className: 'dgs-sub' }, '/') : null))),
|
|
422
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
|
+
}),
|
|
423
597
|
h('button', { className: 'dgs-btn', onClick: () => startCreate('create') }, '新的文件'),
|
|
424
598
|
h('button', { className: 'dgs-btn ghost', onClick: () => startCreate('upload') }, '上传文件')),
|
|
425
599
|
msg ? h('div', { className: 'dgs-err' }, msg) : null,
|
|
@@ -458,15 +632,17 @@ window.__ModuleLoader__.load({
|
|
|
458
632
|
h('span', { style: { fontWeight: 600, fontSize: 12.5 } }, '📄 ' + file.name + ' ' + (file.size ? fmtSize(file.size) : '')),
|
|
459
633
|
h('span', { style: { flex: 1 } }),
|
|
460
634
|
file.text !== null ? h('a', { className: 'dgs-sub', style: { cursor: 'pointer', marginRight: 12 }, onClick: () => {
|
|
461
|
-
|
|
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 {}
|
|
462
638
|
setMsg('✓ 已复制永久链接'); setTimeout(() => setMsg(''), 1500)
|
|
463
639
|
} }, '永久链接') : null,
|
|
464
640
|
file.text !== null ? h('a', { className: 'dgs-sub', style: { cursor: 'pointer', marginRight: 12 }, onClick: () => setHistMode(histMode === 'history' ? null : 'history') }, '文件历史') : null,
|
|
465
|
-
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,
|
|
466
642
|
file.text !== null ? h('button', { className: 'dgs-btn ghost', style: { padding: '2px 8px' }, onClick: () => { setEditing(true); setEditText(file.text || '') } }, '✏️') : null,
|
|
467
643
|
file.text !== null ? h('button', { className: 'dgs-btn danger', style: { padding: '2px 8px' }, onClick: async () => {
|
|
468
644
|
if (!confirm('删除文件 ' + file.name + ' ?')) return
|
|
469
|
-
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 })
|
|
470
646
|
if (d && d.ok) { setFile(null); load(path) } else setMsg((d && d.error) || '删除失败')
|
|
471
647
|
} }, '🗑️') : null,
|
|
472
648
|
file.text !== null ? h('button', { className: 'dgs-btn ghost', style: { padding: '2px 8px' }, onClick: () => setBlameOn(!blameOn) }, 'Blame') : null),
|
|
@@ -478,20 +654,37 @@ window.__ModuleLoader__.load({
|
|
|
478
654
|
h('button', { className: 'dgs-btn ghost', onClick: () => setEditing(false) }, '取消'),
|
|
479
655
|
h('button', { className: 'dgs-btn', disabled: busy, onClick: async () => {
|
|
480
656
|
setBusy(true); setMsg('')
|
|
481
|
-
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 })
|
|
482
658
|
setBusy(false)
|
|
483
|
-
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) || '保存失败')
|
|
484
660
|
} }, '提交修改')))
|
|
485
|
-
: histMode === 'history' ? h(FileHistory, { repo, rev, path })
|
|
661
|
+
: histMode === 'history' ? h(FileHistory, { repo, rev, path: file.path || path })
|
|
486
662
|
: blameOn && file.text !== null
|
|
487
|
-
? h(BlameView, { repo, rev, path, text: file.text })
|
|
663
|
+
? h(BlameView, { repo, rev, path: file.path || path, text: file.text })
|
|
488
664
|
: file.isMd
|
|
489
665
|
? h('div', { className: 'dgs-md', style: { padding: '14px 16px' }, dangerouslySetInnerHTML: { __html: file.html || '' } })
|
|
490
|
-
:
|
|
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()
|
|
491
670
|
: entries === null ? h('div', { className: 'dgs-empty' }, t('loading'))
|
|
492
671
|
: renderDir(),
|
|
493
672
|
)
|
|
494
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
|
+
|
|
495
688
|
function renderDir() {
|
|
496
689
|
const head = entries.find((e) => e.last)
|
|
497
690
|
const rows = entries.map((e) =>
|
|
@@ -508,7 +701,9 @@ window.__ModuleLoader__.load({
|
|
|
508
701
|
h('span', { style: { flex: 1 } }),
|
|
509
702
|
h('span', { className: 'dgs-sub' }, timeAgo(head.last.date))) : null
|
|
510
703
|
const table = entries.length === 0
|
|
511
|
-
?
|
|
704
|
+
? (path === '' && overview && overview.numCommits === 0
|
|
705
|
+
? renderEmptyRepoGuide()
|
|
706
|
+
: h('div', { className: 'dgs-empty' }, t('emptyDir')))
|
|
512
707
|
: h('div', null, headRow, h('table', { className: 'dgs-table' }, h('tbody', null, rows)))
|
|
513
708
|
const readmeEl = (path === '' && overview && overview.readmeHtml)
|
|
514
709
|
? h('div', { className: 'dgs-card', style: { marginTop: 12 } },
|
|
@@ -517,9 +712,26 @@ window.__ModuleLoader__.load({
|
|
|
517
712
|
: null
|
|
518
713
|
return h('div', null, table, readmeEl)
|
|
519
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
|
+
}
|
|
520
732
|
}
|
|
521
733
|
|
|
522
|
-
function IssuesArea({ repo, t, initialSub }) {
|
|
734
|
+
function IssuesArea({ repo, t, initialSub, initialIdx }) {
|
|
523
735
|
const [sub, setSub] = useState(initialSub || 'list')
|
|
524
736
|
const [labelJump, setLabelJump] = useState(null)
|
|
525
737
|
const [msJump, setMsJump] = useState(null)
|
|
@@ -535,12 +747,12 @@ window.__ModuleLoader__.load({
|
|
|
535
747
|
h('span', { style: { flex: 1 } }),
|
|
536
748
|
sub === 'list' ? h('button', { className: 'dgs-btn', onClick: () => setCreateSignal((n) => n + 1) }, t('issueNew')) : null),
|
|
537
749
|
sub === 'list' && h(Issues, { repo, t, presetLabel: labelJump, presetMilestone: msJump,
|
|
538
|
-
onPresetDone: () => { setLabelJump(null); setMsJump(null) }, createSignal }),
|
|
750
|
+
onPresetDone: () => { setLabelJump(null); setMsJump(null) }, createSignal, initialIdx }),
|
|
539
751
|
sub === 'labels' && h(LabelsManage, { repo, t, onFilterLabel: (lid) => { setLabelJump(lid); setSub('list') } }),
|
|
540
752
|
sub === 'milestones' && h(MilestonesManage, { repo, t, onOpenMilestone: (m) => { setMsJump(m.id); setSub('list') } }))
|
|
541
753
|
}
|
|
542
754
|
|
|
543
|
-
function Issues({ repo, t, presetLabel, presetMilestone, onPresetDone, createSignal }) {
|
|
755
|
+
function Issues({ repo, t, presetLabel, presetMilestone, onPresetDone, createSignal, initialIdx }) {
|
|
544
756
|
const [state, setState] = useState('open')
|
|
545
757
|
const [flt, setFlt] = useState({ label: '', milestone: '', assignee: '' })
|
|
546
758
|
useEffect(() => {
|
|
@@ -555,7 +767,7 @@ window.__ModuleLoader__.load({
|
|
|
555
767
|
const [meta, setMeta] = useState({ labels: [], milestones: [], collabs: [] })
|
|
556
768
|
const [creating, setCreating] = useState(false)
|
|
557
769
|
useEffect(() => { if (createSignal) setCreating(true) }, [createSignal])
|
|
558
|
-
const [openIdx, setOpenIdx] = useState(null)
|
|
770
|
+
const [openIdx, setOpenIdx] = useState(initialIdx ?? null)
|
|
559
771
|
const reload = useCallback(() => {
|
|
560
772
|
setList(null)
|
|
561
773
|
const q = new URLSearchParams({ state, sort })
|
|
@@ -700,6 +912,8 @@ window.__ModuleLoader__.load({
|
|
|
700
912
|
setComments(null)
|
|
701
913
|
api('GET', `/dsh/repos/${repo.owner}/${repo.name}/issues/${idx}`).then((d) => setIssue(d && d.number ? d : null))
|
|
702
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(() => {})
|
|
703
917
|
}, [repo.owner, repo.name, idx])
|
|
704
918
|
useEffect(reload, [reload])
|
|
705
919
|
useEffect(() => {
|
|
@@ -729,7 +943,13 @@ window.__ModuleLoader__.load({
|
|
|
729
943
|
comments === null ? h('div', { className: 'dgs-empty' }, t('loading'))
|
|
730
944
|
: comments.length === 0 ? h('div', { className: 'dgs-empty' }, t('noIssues'))
|
|
731
945
|
: comments.map((c) =>
|
|
732
|
-
|
|
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' },
|
|
733
953
|
h('div', { className: 'dgs-row', style: { marginBottom: 4 } },
|
|
734
954
|
h('span', { className: 'dgs-sub' }, `${c.user || ''} · ${fmtDate(c.created)}`),
|
|
735
955
|
h('span', { style: { flex: 1 } }),
|
|
@@ -746,7 +966,7 @@ window.__ModuleLoader__.load({
|
|
|
746
966
|
await api('PATCH', `/repos/${repo.owner}/${repo.name}/issues/comments/${c.id}`, { body: editC.text })
|
|
747
967
|
setEditC(null); reload()
|
|
748
968
|
} }, t('save'))))
|
|
749
|
-
: h('div', { style: { whiteSpace: 'pre-wrap' } }, c.body))),
|
|
969
|
+
: h('div', { style: { whiteSpace: 'pre-wrap' } }, linkifyIssues(c.body, repo)))),
|
|
750
970
|
h('div', { className: 'dgs-card', style: { margin: '12px 0' } },
|
|
751
971
|
h('textarea', { className: 'dgs-input', placeholder: t('commentPlaceholder'), value: text, onChange: (e) => setText(e.target.value) }),
|
|
752
972
|
h('div', { className: 'dgs-row', style: { marginTop: 8 } },
|
|
@@ -829,7 +1049,7 @@ window.__ModuleLoader__.load({
|
|
|
829
1049
|
h('table', { className: 'dgs-table' },
|
|
830
1050
|
h('tbody', null, shown.map((c, i) =>
|
|
831
1051
|
h('tr', { key: i, style: { cursor: 'pointer' }, onClick: () => setSel(c.sha) },
|
|
832
|
-
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)),
|
|
833
1053
|
h('div', { className: 'dgs-sub' }, c.author)),
|
|
834
1054
|
h('td', { className: 'dgs-sub', style: { textAlign: 'right', whiteSpace: 'nowrap' } }, (c.sha || '').slice(0, 10)),
|
|
835
1055
|
h('td', { className: 'dgs-sub', style: { textAlign: 'right', whiteSpace: 'nowrap', width: 100 } }, timeAgo(c.date)))))),
|
|
@@ -849,7 +1069,7 @@ window.__ModuleLoader__.load({
|
|
|
849
1069
|
return h('div', null,
|
|
850
1070
|
h('div', { className: 'dgs-row', style: { marginBottom: 10 } },
|
|
851
1071
|
h('button', { className: 'dgs-btn ghost', onClick: onBack }, t('back')),
|
|
852
|
-
h('span', { style: { fontWeight: 600 } }, (d.message || '').split('\n')[0]),
|
|
1072
|
+
h('span', { style: { fontWeight: 600 } }, linkifyIssues((d.message || '').split('\n')[0], repo)),
|
|
853
1073
|
h('span', { className: 'dgs-sub' }, (d.sha || '').slice(0, 10))),
|
|
854
1074
|
h('div', { className: 'dgs-sub', style: { marginBottom: 8 } },
|
|
855
1075
|
`${(d.author && d.author.name) || ''} · ${fmtDate(when)}`),
|
|
@@ -943,8 +1163,9 @@ window.__ModuleLoader__.load({
|
|
|
943
1163
|
b.name !== def ? h('button', { className: 'dgs-btn ghost', style: { marginLeft: 8, padding: '2px 8px' }, onClick: () => onNewPR && onNewPR(b.name, def) }, '+ PR') : null,
|
|
944
1164
|
b.name !== def ? h('button', { className: 'dgs-btn danger', style: { marginLeft: 4, padding: '2px 8px' }, onClick: async () => {
|
|
945
1165
|
if (!confirm('删除分支 ' + b.name + ' ?')) return
|
|
946
|
-
await api('DELETE', `/repos/${repo.owner}/${repo.name}/branches/${encodeURIComponent(b.name)}`)
|
|
947
|
-
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) || '删除失败')
|
|
948
1169
|
} }, '删') : null))))),
|
|
949
1170
|
h('div', { style: { fontWeight: 700, margin: '18px 0 8px' } }, '标签'),
|
|
950
1171
|
tags === null ? h('div', { className: 'dgs-empty' }, t('loading'))
|
|
@@ -1199,19 +1420,33 @@ window.__ModuleLoader__.load({
|
|
|
1199
1420
|
const [form, setForm] = useState({ description: '', private: false, website: '' })
|
|
1200
1421
|
const [collabs, setCollabs] = useState([])
|
|
1201
1422
|
const [addName, setAddName] = useState('')
|
|
1423
|
+
const [addMode, setAddMode] = useState('write')
|
|
1202
1424
|
const [hooks, setHooks] = useState(null)
|
|
1203
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)
|
|
1204
1432
|
const [msg, setMsg] = useState('')
|
|
1205
1433
|
const [busy, setBusy] = useState(false)
|
|
1206
1434
|
const loadCollabs = () => api('GET', `/dsh/repos/${repo.owner}/${repo.name}/collaborators`).then((d) => setCollabs(Array.isArray(d) ? d : []))
|
|
1207
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 : []))
|
|
1208
1437
|
useEffect(() => {
|
|
1209
1438
|
api('GET', `/dsh/repos/${repo.owner}/${repo.name}`).then((d) => {
|
|
1210
|
-
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
|
+
}
|
|
1211
1444
|
else setMsg((d && d.error) || t('loadFailed'))
|
|
1212
1445
|
})
|
|
1213
1446
|
loadCollabs()
|
|
1214
1447
|
loadHooks()
|
|
1448
|
+
loadProts()
|
|
1449
|
+
api('GET', `/repos/${repo.owner}/${repo.name}/branches`).then((d) => setBranchList((d && d.branches) || []))
|
|
1215
1450
|
}, [repo.owner, repo.name])
|
|
1216
1451
|
if (info === null && !msg) return h('div', { className: 'dgs-empty' }, t('loading'))
|
|
1217
1452
|
const navItem = (k, label) => h('span', { key: k, className: 'item' + (sub === k ? ' active' : ''), onClick: () => setSub(k) }, label)
|
|
@@ -1236,19 +1471,33 @@ window.__ModuleLoader__.load({
|
|
|
1236
1471
|
setBusy(false)
|
|
1237
1472
|
d && d.ok ? setMsg('✓ 已保存') : setMsg((d && d.error) || 'failed')
|
|
1238
1473
|
} }, '更新设置')))
|
|
1474
|
+
const collabModeName = { 1: '只读', 2: '可写', 3: '管理' }
|
|
1239
1475
|
const collab = h('div', { className: 'dgs-card' },
|
|
1240
1476
|
h('div', { style: { fontWeight: 700, marginBottom: 8 } }, '协作者'),
|
|
1241
1477
|
h('div', { className: 'dgs-row' },
|
|
1242
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' }, '管理')),
|
|
1243
1483
|
h('button', { className: 'dgs-btn', disabled: !addName.trim(),
|
|
1244
1484
|
onClick: async () => {
|
|
1245
|
-
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 })
|
|
1246
1486
|
if (d && d.ok !== false && !d.error) { setAddName(''); setMsg(''); loadCollabs() } else setMsg((d && d.error) || 'failed')
|
|
1247
1487
|
} }, '+ 添加')),
|
|
1248
1488
|
collabs.length === 0 ? h('div', { className: 'dgs-sub', style: { marginTop: 8 } }, '—') : null,
|
|
1249
1489
|
collabs.map((u) => h('div', { key: u.name, className: 'dgs-row', style: { marginTop: 6 } },
|
|
1250
1490
|
h('a', { className: 'dgs-name', href: '#/u/' + u.name }, u.name),
|
|
1251
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' }, '管理')),
|
|
1252
1501
|
h('button', { className: 'dgs-btn danger', onClick: async () => {
|
|
1253
1502
|
await api('DELETE', `/dsh/repos/${repo.owner}/${repo.name}/collaborators/${encodeURIComponent(u.name)}`)
|
|
1254
1503
|
loadCollabs()
|
|
@@ -1261,16 +1510,77 @@ window.__ModuleLoader__.load({
|
|
|
1261
1510
|
const d = await api('POST', `/dsh/repos/${repo.owner}/${repo.name}/hooks`, { url: hookUrl.trim() })
|
|
1262
1511
|
if (d && !d.error) { setHookUrl(''); loadHooks() } else setMsg((d && d.error) || 'failed')
|
|
1263
1512
|
} }, '+ 添加')),
|
|
1264
|
-
(hooks || []).map((hk) => h('div', { key: hk.id,
|
|
1265
|
-
h('
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
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)),
|
|
1273
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
|
|
1274
1584
|
const danger = h('div', { className: 'dgs-card', style: { marginTop: 12, borderColor: 'var(--dsw-alias-state-error-primary)55' } },
|
|
1275
1585
|
h('div', { style: { fontWeight: 700, marginBottom: 8, color: 'var(--dsw-alias-state-error-primary)' } }, '危险区域'),
|
|
1276
1586
|
h('div', { className: 'dgs-row' },
|
|
@@ -1302,17 +1612,19 @@ window.__ModuleLoader__.load({
|
|
|
1302
1612
|
h('div', { className: 'dgs-settings-nav' },
|
|
1303
1613
|
navItem('basic', '基本设置'),
|
|
1304
1614
|
navItem('collab', '管理协作者'),
|
|
1615
|
+
navItem('prot', '分支保护'),
|
|
1305
1616
|
navItem('hooks', '管理 Web 钩子'),
|
|
1617
|
+
mirrorInfo ? navItem('mirror', '镜像同步') : null,
|
|
1306
1618
|
navItem('danger', '危险区域')),
|
|
1307
1619
|
h('div', { style: { flex: 1, minWidth: 0 } },
|
|
1308
|
-
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)))
|
|
1309
1621
|
}
|
|
1310
1622
|
|
|
1311
1623
|
// ── PR(列表 + 详情 + 合并 + 评论复用 issue 通道) ────────────────────────
|
|
1312
1624
|
|
|
1313
|
-
function Pulls({ repo, t, preset, onPresetDone, onGoIssuesSub }) {
|
|
1625
|
+
function Pulls({ repo, t, preset, onPresetDone, onGoIssuesSub, initialIdx }) {
|
|
1314
1626
|
const [list, setList] = useState(null)
|
|
1315
|
-
const [openIdx, setOpenIdx] = useState(null)
|
|
1627
|
+
const [openIdx, setOpenIdx] = useState(initialIdx ?? null)
|
|
1316
1628
|
const [creating, setCreating] = useState(false)
|
|
1317
1629
|
const [state, setState] = useState('open')
|
|
1318
1630
|
const [flt, setFlt] = useState({ label: '', milestone: '', assignee: '' })
|
|
@@ -1483,6 +1795,11 @@ window.__ModuleLoader__.load({
|
|
|
1483
1795
|
const [allMs, setAllMs] = useState([])
|
|
1484
1796
|
const [collabs, setCollabs] = useState([])
|
|
1485
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])
|
|
1486
1803
|
useEffect(() => {
|
|
1487
1804
|
api('GET', `/dsh/repos/${repo.owner}/${repo.name}/pulls`).then((d) => {
|
|
1488
1805
|
const all = Array.isArray(d) ? d : (d.data || d.pulls || [])
|
|
@@ -1492,6 +1809,8 @@ window.__ModuleLoader__.load({
|
|
|
1492
1809
|
})
|
|
1493
1810
|
api('GET', `/repos/${repo.owner}/${repo.name}/issues/${idx}/comments`).then((d) => setComments(d.comments || []))
|
|
1494
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()
|
|
1495
1814
|
api('GET', `/dsh/repos/${repo.owner}/${repo.name}/labels`).then((d) => setAllLabels(Array.isArray(d) ? d : []))
|
|
1496
1815
|
api('GET', `/dsh/repos/${repo.owner}/${repo.name}/milestones`).then((d) => setAllMs(Array.isArray(d) ? d : []))
|
|
1497
1816
|
api('GET', `/dsh/repos/${repo.owner}/${repo.name}/collaborators`).then((d) => setCollabs(Array.isArray(d) ? d : []))
|
|
@@ -1532,13 +1851,37 @@ window.__ModuleLoader__.load({
|
|
|
1532
1851
|
onChange: (e) => patchIssue({ assignee: e.target.value }) },
|
|
1533
1852
|
h('option', { value: '' }, '未指派成员'),
|
|
1534
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 ? '通过' : '请求修改')))),
|
|
1535
1861
|
h('div', { className: 'dgs-side-block' },
|
|
1536
1862
|
h('div', { className: 'dgs-side-title' }, (issue && issue.participants || 1) + ' 名参与者'),
|
|
1537
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
|
+
}
|
|
1538
1875
|
const mergeBox = pr.state === 'open' ? h('div', { className: 'dgs-card', style: { margin: '10px 0', borderColor: 'var(--dsw-alias-state-success-primary)55' } },
|
|
1539
1876
|
h('div', { className: 'dgs-row', style: { gap: 8 } },
|
|
1540
1877
|
h('span', { style: { color: 'var(--dsw-alias-state-success-primary)', fontSize: 16 } }, '⑂'),
|
|
1541
|
-
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) }, '✗ 请求修改'))),
|
|
1542
1885
|
h('div', { style: { margin: '10px 0' } },
|
|
1543
1886
|
h('label', { className: 'dgs-sub', style: { display: 'flex', gap: 6, alignItems: 'center', margin: '4px 0' } },
|
|
1544
1887
|
h('input', { type: 'radio', name: 'mergeStyle', checked: mergeStyle === 'merge', onChange: () => setMergeStyle('merge') }), '创建一个新的合并提交'),
|
|
@@ -1570,7 +1913,7 @@ window.__ModuleLoader__.load({
|
|
|
1570
1913
|
(comments || []).map((c) =>
|
|
1571
1914
|
h('div', { key: c.id, className: 'dgs-comment' },
|
|
1572
1915
|
h('div', { className: 'dgs-sub', style: { marginBottom: 4 } }, `${c.user || ''} 评论于 ${fmtDate(c.created)}`),
|
|
1573
|
-
h('div', { style: { whiteSpace: 'pre-wrap' } }, c.body
|
|
1916
|
+
h('div', { style: { whiteSpace: 'pre-wrap' } }, c.body ? linkifyIssues(c.body, repo) : '这个人很懒,什么都没留下。'))),
|
|
1574
1917
|
h('div', { className: 'dgs-card', style: { margin: '10px 0' } },
|
|
1575
1918
|
h('textarea', { className: 'dgs-input', placeholder: t('commentPlaceholder'), value: text, onChange: (e) => setText(e.target.value) }),
|
|
1576
1919
|
h('div', { className: 'dgs-row', style: { marginTop: 8 } },
|
|
@@ -1588,7 +1931,7 @@ window.__ModuleLoader__.load({
|
|
|
1588
1931
|
: h('table', { className: 'dgs-table' },
|
|
1589
1932
|
h('tbody', null, cmp.commits.map((c) =>
|
|
1590
1933
|
h('tr', { key: c.sha, style: { cursor: 'pointer' }, onClick: () => setSelCommit(c.sha) },
|
|
1591
|
-
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)),
|
|
1592
1935
|
h('div', { className: 'dgs-sub' }, c.author)),
|
|
1593
1936
|
h('td', { className: 'dgs-sub', style: { textAlign: 'right' } },
|
|
1594
1937
|
h('a', { style: { color: 'var(--dsw-alias-state-business-primary)', cursor: 'pointer' } }, (c.sha || '').slice(0, 10)))))))
|
|
@@ -1809,16 +2152,46 @@ window.__ModuleLoader__.load({
|
|
|
1809
2152
|
r.prerelease ? h('span', { className: 'dgs-badge pri' }, '预发布') : null,
|
|
1810
2153
|
h('span', { style: { fontWeight: 600 } }, r.title),
|
|
1811
2154
|
h('span', { style: { flex: 1 } }),
|
|
1812
|
-
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
|
+
} }, '(删除)')),
|
|
1813
2161
|
h('div', { className: 'dgs-sub', style: { marginTop: 4 } },
|
|
1814
2162
|
(r.author || '') + ' · ' + timeAgo((r.createdAt || 0) * 1000) + ' 发布 · ' +
|
|
1815
2163
|
(r.behind ? '在该版本发布之后已有 ' + r.behind + ' 次代码提交到 ' + (r.target || '默认') + ' 分支' : '暂无后续提交')),
|
|
1816
2164
|
r.noteHtml ? h('div', { className: 'dgs-md', style: { marginTop: 8 }, dangerouslySetInnerHTML: { __html: r.noteHtml } }) : null,
|
|
1817
2165
|
h('div', { style: { marginTop: 10 } },
|
|
1818
|
-
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
|
+
} }))),
|
|
1819
2182
|
h('div', { className: 'dgs-row', style: { gap: 8 } },
|
|
1820
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)'),
|
|
1821
|
-
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
|
+
} }, '删除')))))))),
|
|
1822
2195
|
(plainTags && plainTags.length > 0) ? h('div', { style: { marginTop: 16 } },
|
|
1823
2196
|
h('div', { style: { fontWeight: 700, margin: '4px 0 8px' } }, '未发版的标签'),
|
|
1824
2197
|
plainTags.map((tg) => h('div', { key: tg, className: 'dgs-issue' },
|
|
@@ -1950,20 +2323,44 @@ window.__ModuleLoader__.load({
|
|
|
1950
2323
|
|
|
1951
2324
|
function UserProfile({ name, t }) {
|
|
1952
2325
|
const [profile, setProfile] = useState(null)
|
|
1953
|
-
|
|
2326
|
+
const [showList, setShowList] = useState(null) // 'followers' | 'following' | null
|
|
2327
|
+
const [followBusy, setFollowBusy] = useState(false)
|
|
2328
|
+
const reload = useCallback(() => {
|
|
1954
2329
|
api('GET', '/dsh/users/' + encodeURIComponent(name)).then((d) => setProfile(d.data || d))
|
|
1955
2330
|
}, [name])
|
|
2331
|
+
useEffect(() => { reload() }, [reload])
|
|
1956
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 : []
|
|
1957
2347
|
return h('div', { className: 'dgs-settings-layout' },
|
|
1958
2348
|
h('div', { className: 'dgs-settings-nav', style: { width: 230 } },
|
|
1959
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 } },
|
|
1960
2350
|
(name || '?').slice(0, 1).toUpperCase()),
|
|
1961
2351
|
h('div', { className: 'dgs-h1', style: { fontSize: 17 } }, name),
|
|
1962
2352
|
profile.isAdmin ? h('span', { className: 'dgs-badge ok', style: { marginTop: 4 } }, 'admin') : null,
|
|
2353
|
+
followBtn,
|
|
1963
2354
|
h('div', { className: 'dgs-sub', style: { marginTop: 10 } }, profile.email ? '✉ ' + profile.email : ''),
|
|
1964
2355
|
h('div', { className: 'dgs-sub', style: { marginTop: 4 } }, '🕐 加入于 ' + (profile.created ? new Date(profile.created * 1000).toLocaleDateString() : '')),
|
|
1965
2356
|
h('div', { className: 'dgs-sub', style: { marginTop: 10 } },
|
|
1966
|
-
|
|
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),
|
|
1967
2364
|
h('div', { style: { flex: 1, minWidth: 0 } },
|
|
1968
2365
|
h('div', { style: { fontWeight: 700, margin: '4px 0 10px' } }, '仓库'),
|
|
1969
2366
|
profile.repos.length === 0 ? h('div', { className: 'dgs-empty' }, '—') : null,
|
|
@@ -2063,6 +2460,69 @@ window.__ModuleLoader__.load({
|
|
|
2063
2460
|
sub === 'panel' ? panel : sub === 'users' ? usersTab : sub === 'repos' ? reposTab : orgsTab))
|
|
2064
2461
|
}
|
|
2065
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
|
+
|
|
2066
2526
|
// ── 主页(仓库列表 + 建仓) ────────────────────────────────────────────────
|
|
2067
2527
|
|
|
2068
2528
|
function GitPage({ onClose, t, standalone }) {
|
|
@@ -2072,6 +2532,8 @@ window.__ModuleLoader__.load({
|
|
|
2072
2532
|
const [err, setErr] = useState('')
|
|
2073
2533
|
const [name, setName] = useState('')
|
|
2074
2534
|
const [isPrivate, setIsPrivate] = useState(false)
|
|
2535
|
+
const [createMode, setCreateMode] = useState('new') // new | import
|
|
2536
|
+
const [importForm, setImportForm] = useState({ url: '', mirror: false })
|
|
2075
2537
|
const [busy, setBusy] = useState(false)
|
|
2076
2538
|
const [toast, setToast] = useState('')
|
|
2077
2539
|
const reload = useCallback(() => {
|
|
@@ -2112,28 +2574,70 @@ window.__ModuleLoader__.load({
|
|
|
2112
2574
|
return () => document.removeEventListener('click', onClickRow, true)
|
|
2113
2575
|
}, [standalone, onClose])
|
|
2114
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])
|
|
2115
2581
|
const seg = route.split('/').filter(Boolean)
|
|
2116
2582
|
const view = seg[0] || 'repos'
|
|
2117
|
-
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
|
|
2118
2594
|
const body = err ? h('div', { className: 'dgs-card dgs-err' }, err,
|
|
2119
2595
|
h('button', { className: 'dgs-btn ghost', style: { marginLeft: 12 }, onClick: reload }, t('retry')))
|
|
2120
|
-
: repoRoute ? h(RepoBrowser, { repo: repoRoute, t, onBack: () => nav('/repos') })
|
|
2596
|
+
: repoRoute ? h(RepoBrowser, { key: route, repo: repoRoute, t, deep: repoRoute.deep, onBack: () => nav('/repos') })
|
|
2121
2597
|
: view === 'explore' ? h(Explore, { t })
|
|
2122
2598
|
: view === 'orgs' ? h(Orgs, { t })
|
|
2123
2599
|
: view === 'org' && seg[1] ? h(OrgView, { name: seg[1], t })
|
|
2124
2600
|
: view === 'u' && seg[1] ? h(UserProfile, { name: decodeURIComponent(seg[1]), t })
|
|
2125
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 })
|
|
2126
2605
|
: me === null ? h('div', { className: 'dgs-empty' }, t('loading'))
|
|
2127
2606
|
: h('div', null,
|
|
2128
2607
|
h('div', { className: 'dgs-card' },
|
|
2129
2608
|
h('div', { className: 'dgs-row' },
|
|
2130
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 } },
|
|
2131
2614
|
h('input', { className: 'dgs-input', placeholder: t('repoName'), value: name,
|
|
2132
2615
|
onChange: (e) => setName(e.target.value),
|
|
2133
2616
|
onKeyDown: (e) => { if (e.key === 'Enter' && name.trim()) create() } }),
|
|
2134
2617
|
h('label', { className: 'dgs-sub', style: { display: 'flex', gap: 4, alignItems: 'center' } },
|
|
2135
2618
|
h('input', { type: 'checkbox', checked: isPrivate, onChange: (e) => setIsPrivate(e.target.checked) }), t('private')),
|
|
2136
|
-
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;勾选「镜像同步」后每小时自动拉取上游更新。'))),
|
|
2137
2641
|
h('div', { style: { height: 10 } }),
|
|
2138
2642
|
h('div', { style: { fontWeight: 700, margin: '4px 0 10px' } }, `${t('myRepos')}(${me.repos.length})`),
|
|
2139
2643
|
me.repos.length === 0 ? h('div', { className: 'dgs-empty' }, t('emptyRepo'))
|
|
@@ -2154,7 +2658,7 @@ window.__ModuleLoader__.load({
|
|
|
2154
2658
|
h('div', { className: 'dgs-row' }, title, badge, h('span', { style: { flex: 1 } }), stats, del), desc)
|
|
2155
2659
|
}
|
|
2156
2660
|
|
|
2157
|
-
const navItems = [['repos', t('myRepos')], ['explore', '探索'], ['orgs', '组织'], ['admin', '管理']]
|
|
2661
|
+
const navItems = [['repos', t('myRepos')], ['myissues', '工单'], ['mypulls', 'PR'], ['notifications', '通知'], ['explore', '探索'], ['orgs', '组织'], ['admin', '管理']]
|
|
2158
2662
|
return h('div', { className: 'dgs-page' },
|
|
2159
2663
|
h('div', { className: 'dgs-head' },
|
|
2160
2664
|
h('span', { className: 'dgs-h1' }, t('title')),
|
|
@@ -2162,7 +2666,9 @@ window.__ModuleLoader__.load({
|
|
|
2162
2666
|
navItems.map(([k, label]) =>
|
|
2163
2667
|
h('button', { key: k, className: 'dgs-btn ghost',
|
|
2164
2668
|
style: view === k ? { color: 'var(--dsw-alias-state-business-primary)', borderColor: 'var(--dsw-alias-state-business-primary)55' } : undefined,
|
|
2165
|
-
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))),
|
|
2166
2672
|
h('button', { className: 'dgs-close', onClick: onClose, title: 'Esc' }, '✕')),
|
|
2167
2673
|
h('div', { className: 'dgs-body' }, body),
|
|
2168
2674
|
toast ? h('div', { className: 'dgs-toast' }, toast) : null)
|
|
@@ -2173,6 +2679,20 @@ window.__ModuleLoader__.load({
|
|
|
2173
2679
|
setBusy(false)
|
|
2174
2680
|
if (d.ok) { setName(''); notify('✓ ' + name); reload() } else notify(d.error || 'failed')
|
|
2175
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
|
+
}
|
|
2176
2696
|
}
|
|
2177
2697
|
|
|
2178
2698
|
// ── 设置节(服务配置,无 iframe) ──────────────────────────────────────────
|
|
@@ -2336,9 +2856,17 @@ window.__ModuleLoader__.load({
|
|
|
2336
2856
|
entry.addEventListener('click', () => openGitPage(t))
|
|
2337
2857
|
const stats = entry.querySelector('.dsh-git-entry-stats')
|
|
2338
2858
|
const refreshStats = () => {
|
|
2859
|
+
let repos = ''
|
|
2339
2860
|
fetch(API + '/me').then((r) => r.json()).then((d) => {
|
|
2340
|
-
if (
|
|
2341
|
-
}).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 }))
|
|
2342
2870
|
}
|
|
2343
2871
|
refreshStats()
|
|
2344
2872
|
const poll = setInterval(refreshStats, 30000)
|