dsh-app-deploy 0.0.0-stage → 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/client.js ADDED
@@ -0,0 +1,1472 @@
1
+ window.__ModuleLoader__.load({
2
+ id: 'dsh-app-deploy',
3
+ factory(require) {
4
+ const React = require('react')
5
+ const { useEffect, useRef, useState } = React
6
+ const h = React.createElement
7
+
8
+ const API = '/api/dsh-app-deploy'
9
+ const SSE = API + '/deploy'
10
+ const HSTATUS = { success: '成功', failed: '失败', running: '进行中', aborted: '中断' }
11
+ const KIND_LABEL = { front: '前端', server: '服务端' }
12
+
13
+ // ---------- 基础工具 ----------
14
+
15
+ async function rpc(method, args) {
16
+ const response = await fetch(API, {
17
+ method: 'POST',
18
+ headers: { 'content-type': 'application/json' },
19
+ body: JSON.stringify({ method, args }),
20
+ })
21
+ let payload
22
+ try { payload = await response.json() } catch { throw new Error('响应解析失败 (' + response.status + ')') }
23
+ if (!payload || payload.ok !== true) throw new Error((payload && payload.error) || ('请求失败 (' + response.status + ')'))
24
+ return payload.value
25
+ }
26
+
27
+ const pad2 = (n) => String(n).padStart(2, '0')
28
+ function fmtTime(ms) {
29
+ if (ms == null) return ''
30
+ const d = new Date(ms)
31
+ return pad2(d.getMonth() + 1) + '-' + pad2(d.getDate()) + ' ' + pad2(d.getHours()) + ':' + pad2(d.getMinutes()) + ':' + pad2(d.getSeconds())
32
+ }
33
+ function fmtDur(ms) {
34
+ if (ms == null) return ''
35
+ const s = Math.round(ms / 1000)
36
+ return s >= 60 ? Math.floor(s / 60) + 'm' + (s % 60) + 's' : s + 's'
37
+ }
38
+ function fmtAgo(ms) {
39
+ const s = Math.max(1, Math.round((Date.now() - ms) / 1000))
40
+ if (s < 60) return '刚刚'
41
+ const m = Math.round(s / 60)
42
+ if (m < 60) return m + ' 分钟前'
43
+ const hh = Math.round(m / 60)
44
+ if (hh < 24) return hh + ' 小时前'
45
+ const d = Math.round(hh / 24)
46
+ if (d < 30) return d + ' 天前'
47
+ return fmtTime(ms)
48
+ }
49
+ function clone(v) { return JSON.parse(JSON.stringify(v)) }
50
+ function normPath(p) { return (p || '').replace(/\/+$/, '') }
51
+ // 绝对路径:posix 以 / 开头,Windows 为盘符或 \ 开头(如 C:\proj、D:/proj)
52
+ function isAbsPath(p) { return /^([a-zA-Z]:[\\/]|[\\/])/.test(p || '') }
53
+ function joinPath(base, name) {
54
+ if (!name) return base || ''
55
+ if (isAbsPath(name)) return name
56
+ const b = base || ''
57
+ if (!b) return name
58
+ return b === '/' ? '/' + name : b.replace(/[\\/]+$/, '') + '/' + name
59
+ }
60
+ function parentPath(p) {
61
+ const base = normPath(p || '')
62
+ if (!base || base === '/' || /^[a-zA-Z]:[\\/]?$/.test(base)) return base || '/'
63
+ const i = Math.max(base.lastIndexOf('/'), base.lastIndexOf('\\'))
64
+ return i <= 0 ? '/' : base.slice(0, i)
65
+ }
66
+ function newEnvId(envs) {
67
+ let id = 'env' + Date.now().toString(36)
68
+ let n = 1
69
+ while (envs.some((e) => e.id === id)) id = 'env' + Date.now().toString(36) + (n++)
70
+ return id
71
+ }
72
+ function makeModule(modules) {
73
+ let n = 1
74
+ while (modules.some((m) => m.id === 'mod' + n)) n++
75
+ return {
76
+ id: 'mod' + n, name: '新模块', kind: 'front', enabled: true,
77
+ gradleTask: '', key: '', extraArgs: '', localPath: '', remotePath: '',
78
+ postCommand: '', healthCheckUrl: '', frontEnv: '',
79
+ }
80
+ }
81
+ function normalizeModule(m) {
82
+ return {
83
+ id: m.id || '', name: m.name || '', kind: m.kind === 'server' ? 'server' : 'front',
84
+ enabled: m.enabled !== false,
85
+ gradleTask: m.gradleTask || '', key: m.key || '', extraArgs: m.extraArgs || '',
86
+ localPath: m.localPath || '', remotePath: m.remotePath || '', postCommand: m.postCommand || '',
87
+ healthCheckUrl: m.healthCheckUrl || '', frontEnv: m.frontEnv || '',
88
+ }
89
+ }
90
+ function normalizeEnv(env) {
91
+ const e = clone(env || {})
92
+ const ssh = e.ssh || {}
93
+ return {
94
+ id: e.id || '',
95
+ name: e.name || '',
96
+ note: e.note || '',
97
+ group: e.group || '',
98
+ projectPath: e.projectPath || '',
99
+ jdkHome: e.jdkHome || '',
100
+ expTime: e.expTime || '',
101
+ ssh: {
102
+ host: ssh.host || '', port: Number(ssh.port) || 22, user: ssh.user || 'root',
103
+ password: ssh.password || '', options: ssh.options || '',
104
+ },
105
+ modules: (e.modules || []).map(normalizeModule),
106
+ }
107
+ }
108
+
109
+ // ---------- 图标 ----------
110
+
111
+ function Icon({ size }) {
112
+ return h('svg', {
113
+ viewBox: '0 0 24 24', width: size || 20, height: size || 20, fill: 'none',
114
+ stroke: 'currentColor', strokeWidth: 1.8, strokeLinecap: 'round', strokeLinejoin: 'round',
115
+ 'aria-hidden': true,
116
+ },
117
+ h('path', { d: 'M12 3v11' }),
118
+ h('path', { d: 'M7.5 9.5 12 14l4.5-4.5' }),
119
+ h('path', { d: 'M4 15v4a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-4' }))
120
+ }
121
+
122
+ // ---------- 小组件 ----------
123
+
124
+ function Field({ label, wide, children }) {
125
+ return h('div', { className: 'dad-field' + (wide ? ' dad-field-wide' : '') },
126
+ h('span', { className: 'dad-label' }, label),
127
+ children)
128
+ }
129
+
130
+ function EnvCard({ env, selected, lastBuild, onSelect, onCopy, onDelete }) {
131
+ const lb = lastBuild[env.id]
132
+ const ssh = env.ssh || {}
133
+ return h('div', {
134
+ className: 'dad-env',
135
+ 'data-sel': selected ? '1' : '0',
136
+ },
137
+ h('button', {
138
+ type: 'button', className: 'dad-env-main', onClick: onSelect,
139
+ 'aria-current': selected ? 'true' : undefined,
140
+ },
141
+ h('span', { className: 'dad-env-name' },
142
+ h('i', { className: 'dad-dot' + (lb ? ' st-' + lb.status : ''), 'aria-hidden': true }),
143
+ env.name || env.id),
144
+ h('span', { className: 'dad-sub' }, env.id + (env.note ? ' · ' + env.note : '')),
145
+ h('span', { className: 'dad-sub' }, ssh.host ? (ssh.user || 'root') + '@' + ssh.host + ':' + (ssh.port || 22) : 'SSH 未配置'),
146
+ h('span', { className: 'dad-sub dad-build' + (lb ? ' ' + lb.status : '') },
147
+ h('i', { className: 'dad-dot', 'aria-hidden': true }),
148
+ lb ? '最后构建: ' + fmtTime(lb.time) + '(' + (HSTATUS[lb.status] || lb.status) + ')' : '最后构建: 从未')),
149
+ h('div', { className: 'dad-envops' },
150
+ h('button', {
151
+ type: 'button', className: 'dad-linkbtn',
152
+ onClick: () => onCopy(),
153
+ }, '复制'),
154
+ h('button', {
155
+ type: 'button', className: 'dad-linkbtn dad-linkbtn-danger',
156
+ onClick: () => onDelete(),
157
+ }, '删除')))
158
+ }
159
+
160
+ function EnvList({ envs, lastBuild, draft, onSelect, onCopy, onDelete, onAdd, onImport, onExport }) {
161
+ const buckets = new Map()
162
+ const order = []
163
+ const ungrouped = []
164
+ for (const e of envs) {
165
+ const g = (e.group || '').trim()
166
+ if (!g) { ungrouped.push(e); continue }
167
+ if (!buckets.has(g)) { buckets.set(g, []); order.push(g) }
168
+ buckets.get(g).push(e)
169
+ }
170
+ const nodes = []
171
+ for (const g of order) {
172
+ nodes.push(h('div', { key: 'grp:' + g, className: 'dad-grp' }, g))
173
+ for (const e of buckets.get(g)) {
174
+ nodes.push(h(EnvCard, {
175
+ key: e.id, env: e, lastBuild,
176
+ selected: !!draft && draft.id === e.id,
177
+ onSelect: () => onSelect(e), onCopy: () => onCopy(e), onDelete: () => onDelete(e),
178
+ }))
179
+ }
180
+ }
181
+ if (ungrouped.length) {
182
+ nodes.push(h('div', { key: 'grp:__ungrouped', className: 'dad-grp' }, '未分组'))
183
+ for (const e of ungrouped) {
184
+ nodes.push(h(EnvCard, {
185
+ key: e.id, env: e, lastBuild,
186
+ selected: !!draft && draft.id === e.id,
187
+ onSelect: () => onSelect(e), onCopy: () => onCopy(e), onDelete: () => onDelete(e),
188
+ }))
189
+ }
190
+ }
191
+ return h('div', { className: 'dad-left' },
192
+ h('div', { className: 'dad-left-head' },
193
+ h('span', { className: 'dad-left-title' }, '环境'),
194
+ h('button', {
195
+ type: 'button', className: 'dad-btn', title: '从 JSON 配置文件导入环境(相同 ID 覆盖本地)',
196
+ onClick: onImport,
197
+ }, '导入'),
198
+ h('button', {
199
+ type: 'button', className: 'dad-btn', title: '导出全部环境配置为 JSON(含 SSH 密码)',
200
+ onClick: onExport, disabled: envs.length === 0,
201
+ }, '导出'),
202
+ h('button', { type: 'button', className: 'dad-btn dad-btn-primary', onClick: onAdd }, '+ 添加环境')),
203
+ h('div', { className: 'dad-envs' },
204
+ envs.length === 0 ? h('div', { className: 'dad-hint' }, '(暂无环境,点击「+ 添加环境」)') : null,
205
+ nodes))
206
+ }
207
+
208
+ function ModuleCard({ mod, onChange, onDelete }) {
209
+ const envFile = mod.id === 'bsFront' ? 'client/client-front/.env'
210
+ : mod.id === 'adminFront' ? 'admin/admin-front/.env' : ''
211
+ const feLabel = 'frontEnv(KEY=VALUE 多行,构建时注入'
212
+ + (envFile ? ' ' + envFile + (mod.key ? '.' + mod.key : '') : '') + ')'
213
+ return h('div', { className: 'dad-mcard', 'data-off': mod.enabled ? '0' : '1' },
214
+ h('button', {
215
+ type: 'button', className: 'dad-btndel', title: '删除模块', 'aria-label': '删除模块',
216
+ onClick: onDelete,
217
+ }, '×'),
218
+ h('div', { className: 'dad-row' },
219
+ h(Field, { label: '显示名' },
220
+ h('input', {
221
+ className: 'dad-input', type: 'text', value: mod.name, 'aria-label': '显示名',
222
+ onChange: (ev) => onChange('name', ev.target.value),
223
+ })),
224
+ h(Field, { label: '类型' },
225
+ h('select', {
226
+ className: 'dad-select', value: mod.kind, 'aria-label': '类型',
227
+ onChange: (ev) => onChange('kind', ev.target.value),
228
+ },
229
+ h('option', { value: 'front' }, '前端'),
230
+ h('option', { value: 'server' }, '服务端'))),
231
+ h(Field, { label: 'ID' },
232
+ h('input', {
233
+ className: 'dad-input', type: 'text', value: mod.id, 'aria-label': '模块 ID',
234
+ onChange: (ev) => onChange('id', ev.target.value),
235
+ })),
236
+ h('label', { className: 'dad-chk' },
237
+ h('input', {
238
+ type: 'checkbox', checked: !!mod.enabled,
239
+ onChange: (ev) => onChange('enabled', ev.target.checked),
240
+ }), '启用')),
241
+ h('div', { className: 'dad-row' },
242
+ h(Field, { label: 'gradle 任务' },
243
+ h('input', {
244
+ className: 'dad-input', type: 'text', value: mod.gradleTask, 'aria-label': 'gradle 任务',
245
+ onChange: (ev) => onChange('gradleTask', ev.target.value),
246
+ })),
247
+ h(Field, { label: '-Pkey(可空)' },
248
+ h('input', {
249
+ className: 'dad-input', type: 'text', value: mod.key, 'aria-label': '-Pkey',
250
+ onChange: (ev) => onChange('key', ev.target.value),
251
+ })),
252
+ h(Field, { label: '额外参数(如 -PexpTime=…)' },
253
+ h('input', {
254
+ className: 'dad-input', type: 'text', value: mod.extraArgs, 'aria-label': '额外参数',
255
+ onChange: (ev) => onChange('extraArgs', ev.target.value),
256
+ }))),
257
+ h('div', { className: 'dad-row' },
258
+ h(Field, { label: '本地产物路径' },
259
+ h('input', {
260
+ className: 'dad-input', type: 'text', value: mod.localPath, 'aria-label': '本地产物路径',
261
+ onChange: (ev) => onChange('localPath', ev.target.value),
262
+ })),
263
+ h(Field, { label: '远程目标路径' },
264
+ h('input', {
265
+ className: 'dad-input', type: 'text', value: mod.remotePath, 'aria-label': '远程目标路径',
266
+ onChange: (ev) => onChange('remotePath', ev.target.value),
267
+ }))),
268
+ h('div', { className: 'dad-row' },
269
+ h(Field, { label: '部署后远程命令(可空)' },
270
+ h('input', {
271
+ className: 'dad-input', type: 'text', value: mod.postCommand, 'aria-label': '部署后远程命令',
272
+ onChange: (ev) => onChange('postCommand', ev.target.value),
273
+ })),
274
+ h(Field, { label: '健康检查 URL(该模块部署后检查,留空跳过)', wide: true },
275
+ h('input', {
276
+ className: 'dad-input', type: 'url', value: mod.healthCheckUrl, 'aria-label': '健康检查 URL',
277
+ onChange: (ev) => onChange('healthCheckUrl', ev.target.value),
278
+ }))),
279
+ h('div', { className: 'dad-hint' },
280
+ '健康检查:HTTP 2xx、success=true、code=200;单次最多 5 秒,间隔 3 秒,最多等待 120 秒,失败中止后续模块。'),
281
+ mod.kind === 'front'
282
+ ? h('div', { className: 'dad-row' },
283
+ h(Field, { label: feLabel, wide: true },
284
+ h('textarea', {
285
+ className: 'dad-textarea', rows: 4, spellCheck: false, value: mod.frontEnv,
286
+ 'aria-label': 'frontEnv',
287
+ onChange: (ev) => onChange('frontEnv', ev.target.value),
288
+ })))
289
+ : null)
290
+ }
291
+
292
+ function EnvForm({
293
+ env, isNew, groups, message, open, onToggle,
294
+ onPatch, onPatchSsh, onPatchModule, onAddModule, onDeleteModule,
295
+ onSave, onDelete, onBrowse,
296
+ jdks, jdkOpen, jdkRef, onJdkFocus, onJdkPick,
297
+ }) {
298
+ const title = env ? '环境配置' : '选择左侧环境进行编辑'
299
+ const groupOptions = groups.map((g) => h('option', { key: g, value: g }))
300
+ const ssh = env ? env.ssh : { host: '', port: 22, user: '', password: '', options: '' }
301
+ return h('div', { className: 'dad-card' },
302
+ h('div', { className: 'dad-card-head' },
303
+ h('span', { className: 'dad-card-title', title }, title),
304
+ // 收起时把最近一次保存结果留在标题栏,避免反馈丢失
305
+ !open && message ? h('span', { className: 'dad-msg ' + (message.kind || '') }, message.text) : null,
306
+ h('button', {
307
+ type: 'button', className: 'dad-btn', onClick: onToggle,
308
+ title: open ? '收缩编辑面板' : '展开编辑面板',
309
+ 'aria-expanded': open ? 'true' : 'false',
310
+ }, open ? '‹' : '›')),
311
+ !env
312
+ ? h('div', { className: 'dad-hint' }, '选择或新增一个环境后编辑配置。')
313
+ : (open ? h('div', null,
314
+ h('div', { className: 'dad-sec' },
315
+ h('div', { className: 'dad-sec-title' }, '基本'),
316
+ h('div', { className: 'dad-row' },
317
+ h(Field, { label: '名称' },
318
+ h('input', {
319
+ className: 'dad-input', type: 'text', value: env.name, 'aria-label': '名称',
320
+ onChange: (ev) => onPatch({ name: ev.target.value }),
321
+ })),
322
+ h(Field, { label: 'ID(小写字母/数字/连字符)' },
323
+ h('input', {
324
+ className: 'dad-input', type: 'text', value: env.id, disabled: !isNew,
325
+ 'aria-label': '环境 ID', title: isNew ? '' : '已保存环境的 ID 不可修改',
326
+ onChange: (ev) => onPatch({ id: ev.target.value }),
327
+ })),
328
+ h(Field, { label: '备注' },
329
+ h('input', {
330
+ className: 'dad-input', type: 'text', value: env.note, 'aria-label': '备注',
331
+ onChange: (ev) => onPatch({ note: ev.target.value }),
332
+ })),
333
+ h(Field, { label: '分组(可留空)' },
334
+ h('input', {
335
+ className: 'dad-input', type: 'text', value: env.group, list: 'dad-group-list',
336
+ 'aria-label': '分组', onChange: (ev) => onPatch({ group: ev.target.value }),
337
+ })),
338
+ h('datalist', { id: 'dad-group-list' }, groupOptions)),
339
+ h('div', { className: 'dad-row' },
340
+ h(Field, { label: '项目路径(本地仓库根目录)', wide: true },
341
+ h('div', { className: 'dad-inline' },
342
+ h('input', {
343
+ className: 'dad-input', type: 'text', value: env.projectPath,
344
+ 'aria-label': '项目路径', onChange: (ev) => onPatch({ projectPath: ev.target.value }),
345
+ }),
346
+ h('button', { type: 'button', className: 'dad-btn', onClick: onBrowse }, '浏览…'))),
347
+ h(Field, { label: 'JDK 路径(JAVA_HOME,可留空)', wide: true },
348
+ h('div', { className: 'dad-jdkwrap', ref: jdkRef },
349
+ h('input', {
350
+ className: 'dad-input', type: 'text', value: env.jdkHome, 'aria-label': 'JDK 路径',
351
+ onFocus: onJdkFocus, onChange: (ev) => onPatch({ jdkHome: ev.target.value }),
352
+ }),
353
+ jdkOpen ? h('div', { className: 'dad-jdkdd' },
354
+ jdks.length
355
+ ? jdks.map((j) => h('button', {
356
+ key: j.path, type: 'button', className: 'dad-jitem',
357
+ onClick: () => onJdkPick(j.path),
358
+ },
359
+ h('span', { className: 'dad-jlbl' }, j.label),
360
+ h('span', { className: 'dad-jfull' }, j.path)))
361
+ : h('div', { className: 'dad-jempty' }, '未发现可用 JDK')) : null))),
362
+ h('div', { className: 'dad-row' },
363
+ h(Field, { label: '过期时间 expTime(YYYY-MM-DD,可留空)' },
364
+ h('input', {
365
+ className: 'dad-input', type: 'date', value: env.expTime, 'aria-label': '过期时间',
366
+ onChange: (ev) => onPatch({ expTime: ev.target.value }),
367
+ })))),
368
+ h('div', { className: 'dad-sec' },
369
+ h('div', { className: 'dad-sec-title' }, 'SSH'),
370
+ h('div', { className: 'dad-row' },
371
+ h(Field, { label: 'Host' },
372
+ h('input', {
373
+ className: 'dad-input', type: 'text', value: ssh.host, 'aria-label': 'SSH Host',
374
+ onChange: (ev) => onPatchSsh({ host: ev.target.value }),
375
+ })),
376
+ h(Field, { label: 'Port' },
377
+ h('input', {
378
+ className: 'dad-input', type: 'number', value: ssh.port, 'aria-label': 'SSH Port',
379
+ onChange: (ev) => onPatchSsh({ port: ev.target.value }),
380
+ })),
381
+ h(Field, { label: 'User' },
382
+ h('input', {
383
+ className: 'dad-input', type: 'text', value: ssh.user, 'aria-label': 'SSH User',
384
+ onChange: (ev) => onPatchSsh({ user: ev.target.value }),
385
+ }))),
386
+ h('div', { className: 'dad-row' },
387
+ h(Field, { label: '密码(留空=ssh-agent/密钥)' },
388
+ h('input', {
389
+ className: 'dad-input', type: 'password', value: ssh.password, 'aria-label': 'SSH 密码',
390
+ autoComplete: 'new-password',
391
+ onChange: (ev) => onPatchSsh({ password: ev.target.value }),
392
+ })),
393
+ h(Field, { label: '自定义 ssh 选项(如 -o StrictHostKeyChecking=no)', wide: true },
394
+ h('input', {
395
+ className: 'dad-input', type: 'text', value: ssh.options, 'aria-label': '自定义 ssh 选项',
396
+ onChange: (ev) => onPatchSsh({ options: ev.target.value }),
397
+ }))),
398
+ h('div', { className: 'dad-hint' },
399
+ ssh.password
400
+ ? '已配置密码,部署时将使用 sshpass(密码不会写入日志)'
401
+ : '密码留空时使用本机 ssh-agent / 密钥认证')),
402
+ h('div', { className: 'dad-sec' },
403
+ h('div', { className: 'dad-sec-title' }, '构建 / 发布模块'),
404
+ env.modules.length === 0 ? h('div', { className: 'dad-hint' }, '(暂无模块)') : null,
405
+ env.modules.map((m, i) => h(ModuleCard, {
406
+ key: i, mod: m,
407
+ onChange: (field, value) => onPatchModule(i, field, value),
408
+ onDelete: () => onDeleteModule(i),
409
+ })),
410
+ h('div', { className: 'dad-row' },
411
+ h('button', { type: 'button', className: 'dad-btn', onClick: onAddModule }, '+ 添加模块'))),
412
+ h('div', { className: 'dad-formfoot' },
413
+ h('button', { type: 'button', className: 'dad-btn dad-btn-primary', onClick: onSave }, '保存环境'),
414
+ h('button', { type: 'button', className: 'dad-btn dad-btn-danger', onClick: onDelete }, '删除此环境'),
415
+ message ? h('span', { className: 'dad-msg ' + (message.kind || '') }, message.text) : null))
416
+ : null))
417
+ }
418
+
419
+ function GitBar({ env, git, buildPath, onPickPath }) {
420
+ if (!env || !normPath(env.projectPath)) {
421
+ return h('div', { className: 'dad-gitbar' }, h('span', { className: 'dad-muted' }, '(未选择环境或项目路径为空)'))
422
+ }
423
+ const eff = buildPath || normPath(env.projectPath)
424
+ const worktrees = (git && git.worktrees) || []
425
+ let head = null
426
+ if (!git || git.state === 'loading') head = h('span', { className: 'dad-muted' }, '正在读取 git 信息…')
427
+ else if (git.state === 'error') head = h('span', { className: 'dad-err' }, '无法读取 git 信息')
428
+ else if (git.state === 'nogit') head = h('span', { className: 'dad-muted' }, '该项目不是 git 仓库')
429
+ else {
430
+ head = h('span', { className: 'dad-ghead' },
431
+ h('span', { className: 'dad-branch' }, git.detached ? 'HEAD(游离)' : (git.branch || '?')),
432
+ git.detached ? null : h('span', { className: 'dad-mono' }, git.shortCommit ? '#' + git.shortCommit : ''))
433
+ }
434
+ return h('div', { className: 'dad-gitbar' },
435
+ head,
436
+ buildPath ? h('span', { className: 'dad-gtmp' }, '临时构建路径') : null,
437
+ h('span', { className: 'dad-gpath', title: eff }, eff),
438
+ worktrees.length > 1
439
+ ? h('select', {
440
+ className: 'dad-select dad-gsel', value: eff,
441
+ title: '临时选择本次部署要构建的 worktree(仅本次会话有效,不修改环境配置)',
442
+ 'aria-label': '临时构建路径',
443
+ onChange: (ev) => onPickPath(ev.target.value),
444
+ }, worktrees.map((w) => h('option', { key: w.path, value: w.path },
445
+ (w.branch || w.head || '?') + ' — ' + w.path)))
446
+ : null)
447
+ }
448
+
449
+ function ConfirmDialog({ state, onClose }) {
450
+ return h('div', {
451
+ className: 'dad-mask', role: 'presentation',
452
+ onClick: onClose,
453
+ },
454
+ h('div', {
455
+ className: 'dad-modal', role: 'dialog', 'aria-modal': 'true', 'aria-label': state.title,
456
+ onClick: (ev) => ev.stopPropagation(),
457
+ },
458
+ h('div', { className: 'dad-modal-head' },
459
+ h('strong', null, state.title),
460
+ h('span', { className: 'dad-spacer' }),
461
+ h('button', { type: 'button', className: 'dad-btn', onClick: onClose, 'aria-label': '关闭' }, '×')),
462
+ h('div', { className: 'dad-modal-body' },
463
+ state.pre ? h('pre', { className: 'dad-plan' }, state.body) : h('div', { className: 'dad-plan-text' }, state.body)),
464
+ h('div', { className: 'dad-modal-foot' },
465
+ h('button', { type: 'button', className: 'dad-btn', onClick: onClose }, '取消'),
466
+ h('button', {
467
+ type: 'button',
468
+ className: 'dad-btn ' + (state.danger ? 'dad-btn-danger-solid' : 'dad-btn-primary'),
469
+ onClick: () => { const fn = state.onConfirm; onClose(); if (fn) fn() },
470
+ }, state.confirmLabel || '确定'))))
471
+ }
472
+
473
+ function BrowseModal({ state, onClose, onNavigate, onUp, onPick, onToggleHidden }) {
474
+ return h('div', { className: 'dad-mask', role: 'presentation', onClick: onClose },
475
+ h('div', {
476
+ className: 'dad-modal', role: 'dialog', 'aria-modal': 'true', 'aria-label': '选择目录',
477
+ onClick: (ev) => ev.stopPropagation(),
478
+ },
479
+ h('div', { className: 'dad-modal-head' },
480
+ h('strong', null, '选择目录'),
481
+ h('label', { className: 'dad-chk dad-autoflex' },
482
+ h('input', {
483
+ type: 'checkbox', checked: !!state.hidden,
484
+ onChange: (ev) => onToggleHidden(ev.target.checked),
485
+ }), '显示隐藏目录'),
486
+ h('span', { className: 'dad-spacer' }),
487
+ h('button', { type: 'button', className: 'dad-btn', onClick: onClose, 'aria-label': '关闭' }, '×')),
488
+ h('div', { className: 'dad-modal-body' },
489
+ h('div', { className: 'dad-browse-path' }, state.path || '(读取中…)'),
490
+ state.error ? h('div', { className: 'dad-msg err' }, state.error) : null,
491
+ state.worktrees && state.worktrees.length
492
+ ? h('div', { className: 'dad-wts' },
493
+ h('span', { className: 'dad-label' }, 'Worktrees'),
494
+ state.worktrees.map((w) => h('button', {
495
+ key: w.path, type: 'button', className: 'dad-wt',
496
+ 'data-cur': w.path === state.path ? '1' : '0',
497
+ title: w.path, disabled: w.path === state.path,
498
+ onClick: () => onNavigate(w.path),
499
+ },
500
+ h('span', { className: 'dad-wbr' }, w.branch || w.head || '(detached)'),
501
+ w.head ? h('span', { className: 'dad-mono' }, w.head) : null)))
502
+ : null,
503
+ h('div', { className: 'dad-dirs' },
504
+ state.error ? null : (state.loading ? h('div', { className: 'dad-hint' }, '读取中…')
505
+ : (state.dirs || []).length ? null : h('div', { className: 'dad-hint' }, '(空目录)')),
506
+ (state.dirs || []).map((dir) => h('button', {
507
+ key: dir, type: 'button', className: 'dad-dir',
508
+ onClick: () => onNavigate(dir),
509
+ }, dir)))),
510
+ h('div', { className: 'dad-modal-foot' },
511
+ h('button', {
512
+ type: 'button', className: 'dad-btn', onClick: onUp,
513
+ disabled: state.loading || !state.path || state.path === '/',
514
+ }, '上级'),
515
+ h('button', {
516
+ type: 'button', className: 'dad-btn dad-btn-primary', onClick: onPick,
517
+ disabled: state.loading || !state.path,
518
+ }, '选择当前目录'))))
519
+ }
520
+
521
+ // ---------- 主面板 ----------
522
+
523
+ function Panel() {
524
+ const [envs, setEnvs] = useState([])
525
+ const [defProject, setDefProject] = useState('')
526
+ const [lastBuild, setLastBuild] = useState({})
527
+ const [loadError, setLoadError] = useState('')
528
+ const [draft, setDraft] = useState(null)
529
+ const [formOpen, setFormOpen] = useState(true)
530
+ const [formMsg, setFormMsg] = useState(null)
531
+ // 全局提示:导入等不依赖当前选中环境的操作也必须有可见反馈
532
+ const [notice, setNotice] = useState(null)
533
+
534
+ const [jdks, setJdks] = useState([])
535
+ const [jdkOpen, setJdkOpen] = useState(false)
536
+ const jdkRef = useRef(null)
537
+
538
+ const [browse, setBrowse] = useState(null)
539
+
540
+ const [selected, setSelected] = useState([])
541
+ const [buildPath, setBuildPath] = useState('')
542
+ const [gitInfo, setGitInfo] = useState(null)
543
+
544
+ const [logText, setLogText] = useState('')
545
+ const [deployState, setDeployState] = useState({ running: false, text: '', kind: '' })
546
+ const [history, setHistory] = useState([])
547
+ const [historyState, setHistoryState] = useState('')
548
+ const [historyTick, setHistoryTick] = useState(0)
549
+
550
+ const [confirmState, setConfirmState] = useState(null)
551
+
552
+ const logRef = useRef(null)
553
+ const esRef = useRef(null)
554
+ const importRef = useRef(null)
555
+ const dragIdRef = useRef(null)
556
+ const [draggingId, setDraggingId] = useState('')
557
+ const [dragOverId, setDragOverId] = useState('')
558
+ const frontEnvFetched = useRef(new Set())
559
+
560
+ const envId = draft && envs.some((e) => e.id === draft.id) ? draft.id : ''
561
+ const isNew = !!draft && !envs.some((e) => e.id === draft.id)
562
+ const groups = Array.from(new Set(envs.map((e) => (e.group || '').trim()).filter(Boolean)))
563
+ const enabledModules = draft ? draft.modules.filter((m) => m.enabled) : []
564
+ const moduleById = {}
565
+ if (draft) for (const m of draft.modules) moduleById[m.id] = m
566
+
567
+ // 首次加载配置;之后只刷新最后构建时间
568
+ useEffect(() => {
569
+ let cancelled = false
570
+ rpc('config').then((d) => {
571
+ if (cancelled) return
572
+ const list = Array.isArray(d && d.envs) ? d.envs : []
573
+ setEnvs(list)
574
+ setDefProject((d && d.defaultProjectPath) || '')
575
+ setLastBuild((d && d.lastBuild) || {})
576
+ setDraft((prev) => {
577
+ if (prev) {
578
+ const match = list.find((e) => e.id === prev.id)
579
+ if (match) return normalizeEnv(match)
580
+ }
581
+ return list.length ? normalizeEnv(list[0]) : null
582
+ })
583
+ }).catch((error) => { if (!cancelled) setLoadError(error.message || String(error)) })
584
+ return () => { cancelled = true }
585
+ }, [])
586
+
587
+ // 全局提示 6 秒后自动消失
588
+ useEffect(() => {
589
+ if (!notice) return undefined
590
+ const timer = setTimeout(() => setNotice(null), 6000)
591
+ return () => clearTimeout(timer)
592
+ }, [notice])
593
+
594
+ // 组件卸载时断开日志流
595
+ useEffect(() => () => {
596
+ if (esRef.current) { try { esRef.current.close() } catch { /* 已关闭 */ } }
597
+ }, [])
598
+
599
+ function refreshLastBuild() {
600
+ rpc('config').then((d) => setLastBuild((d && d.lastBuild) || {})).catch(() => {})
601
+ }
602
+
603
+ // 选中环境/模块变化时清理失效的勾选
604
+ useEffect(() => {
605
+ setSelected((prev) => {
606
+ const ids = new Set(enabledModules.map((m) => m.id))
607
+ const next = prev.filter((id) => ids.has(id))
608
+ return next.length === prev.length ? prev : next
609
+ })
610
+ }, [draft && draft.id, draft && draft.modules])
611
+
612
+ // 切换环境时临时构建路径回落到环境自身路径
613
+ useEffect(() => { setBuildPath('') }, [draft && draft.id])
614
+
615
+ // 读取 git 信息(生效路径 = 临时构建路径 || 项目路径)
616
+ useEffect(() => {
617
+ if (!envId || !draft || !normPath(draft.projectPath)) { setGitInfo(null); return undefined }
618
+ let cancelled = false
619
+ setGitInfo({ state: 'loading' })
620
+ const args = { env: envId }
621
+ if (buildPath) args.path = buildPath
622
+ rpc('git', args).then((d) => {
623
+ if (cancelled) return
624
+ if (!d || !d.ok || !d.isGit) {
625
+ setGitInfo({ state: d && d.ok ? 'nogit' : 'error' })
626
+ return
627
+ }
628
+ const worktrees = Array.isArray(d.worktrees) ? d.worktrees : []
629
+ if (buildPath && !worktrees.some((w) => w.path === buildPath)) setBuildPath('')
630
+ setGitInfo({
631
+ state: 'ok', branch: d.branch || '', shortCommit: d.shortCommit || '',
632
+ detached: !!d.detached, worktrees,
633
+ })
634
+ }).catch(() => { if (!cancelled) setGitInfo({ state: 'error' }) })
635
+ return () => { cancelled = true }
636
+ }, [envId, draft && draft.projectPath, buildPath, envs])
637
+
638
+ // 执行记录(当前环境最近 20 条)
639
+ useEffect(() => {
640
+ if (!envId) { setHistory([]); setHistoryState(''); return undefined }
641
+ let cancelled = false
642
+ setHistoryState('loading')
643
+ rpc('history', { env: envId, limit: 20 }).then((list) => {
644
+ if (cancelled) return
645
+ setHistory(Array.isArray(list) ? list : [])
646
+ setHistoryState('')
647
+ }).catch(() => { if (!cancelled) setHistoryState('error') })
648
+ return () => { cancelled = true }
649
+ }, [envId, historyTick])
650
+
651
+ // frontEnv 为空的前端模块读取 .env 参考文本(用户已填写则不覆盖)
652
+ useEffect(() => {
653
+ if (!envId || !draft) return
654
+ for (const m of draft.modules) {
655
+ if (m.kind !== 'front' || m.frontEnv) continue
656
+ const key = envId + '::' + m.id
657
+ if (frontEnvFetched.current.has(key)) continue
658
+ frontEnvFetched.current.add(key)
659
+ rpc('frontEnv', { env: envId, module: m.id }).then((d) => {
660
+ const text = d && typeof d.text === 'string' ? d.text : ''
661
+ if (!text) return
662
+ setDraft((prev) => {
663
+ if (!prev || prev.id !== envId) return prev
664
+ let changed = false
665
+ const modules = prev.modules.map((x) => {
666
+ if (x.id === m.id && x.kind === 'front' && !x.frontEnv) {
667
+ changed = true
668
+ return { ...x, frontEnv: text }
669
+ }
670
+ return x
671
+ })
672
+ return changed ? { ...prev, modules } : prev
673
+ })
674
+ }).catch(() => { /* 读不到参考文本就留空 */ })
675
+ }
676
+ }, [envId, draft && draft.modules])
677
+
678
+ // 日志自动滚到底部
679
+ useEffect(() => {
680
+ const el = logRef.current
681
+ if (el) el.scrollTop = el.scrollHeight
682
+ }, [logText])
683
+
684
+ // JDK 下拉:点击空白处关闭
685
+ useEffect(() => {
686
+ if (!jdkOpen) return undefined
687
+ function onDocDown(ev) {
688
+ const el = jdkRef.current
689
+ if (el && !el.contains(ev.target)) setJdkOpen(false)
690
+ }
691
+ document.addEventListener('mousedown', onDocDown)
692
+ return () => document.removeEventListener('mousedown', onDocDown)
693
+ }, [jdkOpen])
694
+
695
+ // ---------- 环境操作 ----------
696
+
697
+ function selectEnv(env) {
698
+ setDraft(normalizeEnv(env))
699
+ setSelected([])
700
+ setFormMsg(null)
701
+ }
702
+ function addEnv() {
703
+ const env = {
704
+ id: newEnvId(envs), name: '新环境', note: '', group: '',
705
+ projectPath: defProject, jdkHome: '', expTime: '',
706
+ ssh: { host: '', port: 22, user: 'root', password: '', options: '' },
707
+ modules: [],
708
+ }
709
+ setDraft(env)
710
+ setSelected([])
711
+ setFormOpen(true)
712
+ setFormMsg({ text: '新环境尚未保存,填写后点击「保存环境」', kind: 'warn' })
713
+ }
714
+ function copyEnv(source) {
715
+ const copy = normalizeEnv(source)
716
+ let id = source.id + '-copy'
717
+ let n = 2
718
+ while (envs.some((e) => e.id === id)) id = source.id + '-copy' + (n++)
719
+ copy.id = id
720
+ copy.name = source.name + '(副本)'
721
+ setDraft(copy)
722
+ setSelected([])
723
+ setFormOpen(true)
724
+ setFormMsg({ text: '副本尚未保存,点击「保存环境」后写入配置', kind: 'warn' })
725
+ }
726
+ function askDeleteEnv(env) {
727
+ setConfirmState({
728
+ title: '删除环境',
729
+ body: '删除环境 ' + (env.name || env.id) + '(' + env.id + ')?此操作会写回配置。',
730
+ danger: true,
731
+ confirmLabel: '删除',
732
+ onConfirm: () => {
733
+ rpc('deleteEnv', { id: env.id }).then((d) => {
734
+ const list = Array.isArray(d && d.envs) ? d.envs : []
735
+ setEnvs(list)
736
+ setSelected([])
737
+ setFormMsg(null)
738
+ setDraft((prev) => {
739
+ if (prev && prev.id !== env.id) return prev
740
+ return list.length ? normalizeEnv(list[0]) : null
741
+ })
742
+ }).catch((error) => setFormMsg({ text: '删除失败:' + (error.message || error), kind: 'err' }))
743
+ },
744
+ })
745
+ }
746
+ function deleteCurrentEnv() {
747
+ if (!draft) return
748
+ if (isNew) {
749
+ setSelected([])
750
+ setFormMsg(null)
751
+ setDraft(envs.length ? normalizeEnv(envs[0]) : null)
752
+ return
753
+ }
754
+ askDeleteEnv(draft)
755
+ }
756
+
757
+ // ---------- 导入 / 导出配置 ----------
758
+
759
+ // 导出当前环境列表(与 envs.json 同构,可直接用于「导入」;含 SSH 密码)
760
+ function exportConfig() {
761
+ if (!envs.length) return
762
+ const d = new Date()
763
+ const date = d.getFullYear() + '-' + pad2(d.getMonth() + 1) + '-' + pad2(d.getDate())
764
+ const blob = new Blob([JSON.stringify(envs, null, 2)], { type: 'application/json' })
765
+ const url = URL.createObjectURL(blob)
766
+ const a = document.createElement('a')
767
+ a.href = url
768
+ a.download = 'envs-' + date + '.json'
769
+ document.body.appendChild(a)
770
+ a.click()
771
+ a.remove()
772
+ setTimeout(() => URL.revokeObjectURL(url), 1000)
773
+ setNotice({ text: '已导出 ' + envs.length + ' 个环境(含 SSH 密码,请妥善保管)', kind: 'ok' })
774
+ }
775
+
776
+ function openImport() {
777
+ if (importRef.current) importRef.current.click()
778
+ }
779
+ async function importFile(ev) {
780
+ const input = ev.target
781
+ const file = input.files && input.files[0]
782
+ input.value = '' // 允许重复选择同一文件
783
+ if (!file) return
784
+ let text
785
+ try { text = await file.text() } catch {
786
+ setNotice({ text: '读取文件失败', kind: 'err' })
787
+ return
788
+ }
789
+ // 客户端只预览要覆盖哪些 ID;解析/校验/合并以服务端为准
790
+ let ids = []
791
+ try {
792
+ const data = JSON.parse(text)
793
+ const list = Array.isArray(data) ? data : (data && Array.isArray(data.envs) ? data.envs : [])
794
+ ids = list.map((e) => String((e && e.id) || '')).filter(Boolean)
795
+ if (!ids.length) throw new Error('没有环境(需为环境数组,元素含 id)')
796
+ } catch (error) {
797
+ setNotice({ text: '导入失败:' + (error.message || error), kind: 'err' })
798
+ return
799
+ }
800
+ const overwritten = ids.filter((id) => envs.some((e) => e.id === id))
801
+ setConfirmState({
802
+ title: '导入配置',
803
+ body: '将导入 ' + ids.length + ' 个环境:' + ids.join('、') + '。'
804
+ + (overwritten.length ? '以下 ID 与本地相同,将被导入文件覆盖:' + overwritten.join('、') + '。' : ''),
805
+ danger: overwritten.length > 0,
806
+ confirmLabel: '导入',
807
+ onConfirm: () => doImport(text),
808
+ })
809
+ }
810
+ function doImport(text) {
811
+ rpc('importEnvs', { content: text }).then((d) => {
812
+ const next = Array.isArray(d && d.envs) ? d.envs : []
813
+ const added = Array.isArray(d && d.added) ? d.added : []
814
+ const overwritten = Array.isArray(d && d.overwritten) ? d.overwritten : []
815
+ const warning = (d && d.warning) || ''
816
+ setEnvs(next)
817
+ setSelected([])
818
+ const targetId = added[0] || overwritten[0] || ''
819
+ setDraft((prev) => {
820
+ const target = next.find((e) => e.id === targetId)
821
+ || (prev && next.find((e) => e.id === prev.id))
822
+ || next[0]
823
+ return target ? normalizeEnv(target) : null
824
+ })
825
+ setNotice({
826
+ text: '已导入 ' + (added.length + overwritten.length) + ' 个环境'
827
+ + (overwritten.length ? ',覆盖:' + overwritten.join('、') : '')
828
+ + (warning ? ';' + warning : ''),
829
+ kind: warning ? 'warn' : 'ok',
830
+ })
831
+ }).catch((error) => {
832
+ setNotice({ text: '导入失败:' + (error.message || error), kind: 'err' })
833
+ })
834
+ }
835
+
836
+ function patchEnv(patch) {
837
+ setDraft((prev) => (prev ? { ...prev, ...patch } : prev))
838
+ }
839
+ function patchSsh(patch) {
840
+ setDraft((prev) => (prev ? { ...prev, ssh: { ...prev.ssh, ...patch } } : prev))
841
+ }
842
+ function patchModule(index, field, value) {
843
+ setDraft((prev) => {
844
+ if (!prev) return prev
845
+ const modules = prev.modules.map((m, i) => (i === index ? { ...m, [field]: value } : m))
846
+ return { ...prev, modules }
847
+ })
848
+ }
849
+ function addModule() {
850
+ setDraft((prev) => (prev ? { ...prev, modules: prev.modules.concat([makeModule(prev.modules)]) } : prev))
851
+ }
852
+ function askDeleteModule(index) {
853
+ if (!draft || !draft.modules[index]) return
854
+ const mod = draft.modules[index]
855
+ setConfirmState({
856
+ title: '删除模块',
857
+ body: '删除模块「' + (mod.name || mod.id) + '」?保存环境后生效。',
858
+ danger: true,
859
+ confirmLabel: '删除',
860
+ onConfirm: () => setDraft((prev) => {
861
+ if (!prev) return prev
862
+ return { ...prev, modules: prev.modules.filter((_m, i) => i !== index) }
863
+ }),
864
+ })
865
+ }
866
+
867
+ function saveEnv() {
868
+ if (!draft) return
869
+ const envIdValue = (draft.id || '').trim()
870
+ if (!envIdValue) { setFormMsg({ text: 'ID 不能为空', kind: 'err' }); return }
871
+ if (isNew && envs.some((e) => e.id === envIdValue)) {
872
+ setFormMsg({ text: 'ID「' + envIdValue + '」已被其他环境使用', kind: 'err' })
873
+ return
874
+ }
875
+ const env = {
876
+ id: envIdValue,
877
+ name: (draft.name || '').trim() || '未命名',
878
+ note: (draft.note || '').trim(),
879
+ group: (draft.group || '').trim(),
880
+ projectPath: (draft.projectPath || '').trim() || defProject,
881
+ jdkHome: (draft.jdkHome || '').trim(),
882
+ expTime: draft.expTime || '',
883
+ ssh: {
884
+ host: (draft.ssh.host || '').trim(),
885
+ port: Number(draft.ssh.port) || 22,
886
+ user: (draft.ssh.user || '').trim() || 'root',
887
+ password: draft.ssh.password || '',
888
+ options: (draft.ssh.options || '').trim(),
889
+ },
890
+ modules: draft.modules.map((m) => ({
891
+ id: (m.id || '').trim(),
892
+ name: (m.name || '').trim() || '新模块',
893
+ kind: m.kind === 'server' ? 'server' : 'front',
894
+ enabled: !!m.enabled,
895
+ gradleTask: (m.gradleTask || '').trim(),
896
+ key: (m.key || '').trim(),
897
+ extraArgs: (m.extraArgs || '').trim(),
898
+ localPath: (m.localPath || '').trim(),
899
+ remotePath: (m.remotePath || '').trim(),
900
+ postCommand: (m.postCommand || '').trim(),
901
+ healthCheckUrl: (m.healthCheckUrl || '').trim(),
902
+ frontEnv: m.frontEnv || '',
903
+ })),
904
+ }
905
+ rpc('saveEnv', env).then((d) => {
906
+ const list = Array.isArray(d && d.envs) ? d.envs : []
907
+ setEnvs(list)
908
+ const saved = list.find((x) => x.id === env.id) || env
909
+ setDraft(normalizeEnv(saved))
910
+ setFormOpen(false)
911
+ setFormMsg({ text: (d && d.warning) || '已保存', kind: (d && d.warning) ? 'warn' : 'ok' })
912
+ }).catch((error) => {
913
+ setFormMsg({ text: (error.message || String(error)), kind: 'err' })
914
+ })
915
+ }
916
+
917
+ // ---------- 路径浏览 ----------
918
+
919
+ function loadBrowse(path, hidden) {
920
+ setBrowse((prev) => ({
921
+ path: prev ? prev.path : '',
922
+ parent: prev ? prev.parent : '',
923
+ dirs: prev ? prev.dirs : [],
924
+ worktrees: prev ? prev.worktrees : [],
925
+ hidden: !!hidden, loading: true, error: '',
926
+ }))
927
+ const args = {}
928
+ if (path) args.path = path
929
+ if (hidden) args.hidden = true
930
+ rpc('browse', args).then((d) => {
931
+ setBrowse({
932
+ path: (d && d.path) || '',
933
+ parent: (d && d.parent) || parentPath((d && d.path) || ''),
934
+ dirs: Array.isArray(d && d.dirs) ? d.dirs : [],
935
+ worktrees: Array.isArray(d && d.worktrees) ? d.worktrees : [],
936
+ hidden: !!hidden, loading: false, error: '',
937
+ })
938
+ }).catch((error) => {
939
+ setBrowse((prev) => ({
940
+ path: prev ? prev.path : '',
941
+ parent: prev ? prev.parent : '',
942
+ dirs: [], worktrees: [],
943
+ hidden: !!hidden, loading: false,
944
+ error: error.message || String(error),
945
+ }))
946
+ })
947
+ }
948
+ function openBrowse() {
949
+ if (!draft) return
950
+ loadBrowse((draft.projectPath || '').trim(), false)
951
+ }
952
+ function navigateBrowse(target) {
953
+ if (!browse) return
954
+ loadBrowse(joinPath(browse.path, target), browse.hidden)
955
+ }
956
+ function browseUp() {
957
+ if (!browse) return
958
+ loadBrowse(browse.parent || parentPath(browse.path), browse.hidden)
959
+ }
960
+ function browsePick() {
961
+ if (!browse || !browse.path) return
962
+ patchEnv({ projectPath: browse.path })
963
+ setBrowse(null)
964
+ }
965
+ function browseToggleHidden(hidden) {
966
+ if (!browse) return
967
+ loadBrowse(browse.path, hidden)
968
+ }
969
+
970
+ // ---------- JDK ----------
971
+
972
+ function openJdk() {
973
+ setJdkOpen(true)
974
+ if (!jdks.length) {
975
+ rpc('jdks').then((list) => setJdks(Array.isArray(list) ? list : [])).catch(() => setJdks([]))
976
+ }
977
+ }
978
+ function pickJdk(path) {
979
+ patchEnv({ jdkHome: path })
980
+ setJdkOpen(false)
981
+ }
982
+
983
+ // ---------- 部署 ----------
984
+
985
+ function toggleSel(id) {
986
+ setSelected((prev) => (prev.includes(id) ? prev.filter((x) => x !== id) : prev.concat([id])))
987
+ }
988
+ function toggleSelAll() {
989
+ const all = enabledModules.map((m) => m.id)
990
+ setSelected((prev) => (all.length > 0 && prev.length === all.length ? [] : all))
991
+ }
992
+ function orderDrop(targetId) {
993
+ const dragId = dragIdRef.current
994
+ dragIdRef.current = null
995
+ setDraggingId('')
996
+ setDragOverId('')
997
+ if (!dragId) return
998
+ setSelected((prev) => {
999
+ const from = prev.indexOf(dragId)
1000
+ if (from < 0) return prev
1001
+ const to = targetId ? prev.indexOf(targetId) : -1
1002
+ const arr = prev.slice()
1003
+ arr.splice(from, 1)
1004
+ if (to >= 0) arr.splice(Math.min(to, arr.length), 0, dragId)
1005
+ else arr.push(dragId)
1006
+ return arr
1007
+ })
1008
+ }
1009
+
1010
+ function buildPlan(env, mods) {
1011
+ const lines = []
1012
+ lines.push('环境: ' + env.name + ' (' + env.id + ')')
1013
+ lines.push('模块(按执行顺序): ' + mods.map((m) => m.name || m.id).join(' + '))
1014
+ if (buildPath) lines.push('构建路径: ' + buildPath + '(临时 worktree,不写回配置)')
1015
+ lines.push('')
1016
+ lines.push('将依次执行:')
1017
+ for (const m of mods) {
1018
+ const exp = m.kind === 'server' && env.expTime ? ' -PexpTime=' + env.expTime : ''
1019
+ lines.push('')
1020
+ lines.push('[' + (m.name || m.id) + ']')
1021
+ lines.push(' 构建: gradlew ' + m.gradleTask + (m.key ? ' -Pkey=' + m.key : '') + (m.extraArgs ? ' ' + m.extraArgs : '') + exp)
1022
+ lines.push(' rsync: ' + m.localPath + ' → ' + env.ssh.user + '@' + env.ssh.host + ':' + m.remotePath)
1023
+ if (m.postCommand) lines.push(' ssh: ' + m.postCommand)
1024
+ }
1025
+ return lines.join('\n')
1026
+ }
1027
+
1028
+ function requestDeploy() {
1029
+ if (!draft || !selected.length) return
1030
+ if (isNew) { setDeployState({ running: false, text: '请先保存环境再执行部署', kind: 'err' }); return }
1031
+ if (!draft.ssh.host || !draft.ssh.user) {
1032
+ setDeployState({ running: false, text: '该环境 SSH 配置不完整,请先保存 SSH 配置', kind: 'err' })
1033
+ return
1034
+ }
1035
+ const mods = selected.map((id) => moduleById[id]).filter((m) => m && m.enabled)
1036
+ if (!mods.length) { setDeployState({ running: false, text: '所选模块均已禁用', kind: 'err' }); return }
1037
+ const ids = mods.map((m) => m.id)
1038
+ // 部署读的是已保存配置:未保存的模块改动会让服务端按旧的启用状态执行,先拦住
1039
+ const savedEnv = envs.find((e) => e.id === draft.id)
1040
+ const savedEnabled = new Set(((savedEnv && savedEnv.modules) || []).filter((m) => m.enabled).map((m) => m.id))
1041
+ const unsaved = ids.filter((id) => !savedEnabled.has(id))
1042
+ if (unsaved.length) {
1043
+ setDeployState({ running: false, text: '有模块改动尚未保存(' + unsaved.join('、') + '),请先保存环境', kind: 'err' })
1044
+ return
1045
+ }
1046
+ const envIdValue = draft.id
1047
+ setConfirmState({
1048
+ title: '确认部署',
1049
+ body: buildPlan(draft, mods),
1050
+ pre: true,
1051
+ danger: true,
1052
+ confirmLabel: '执行部署',
1053
+ onConfirm: () => startDeploy(envIdValue, ids),
1054
+ })
1055
+ }
1056
+
1057
+ function startDeploy(envIdValue, ids) {
1058
+ if (esRef.current) { try { esRef.current.close() } catch { /* 已关闭 */ } esRef.current = null }
1059
+ setLogText('')
1060
+ setDeployState({ running: true, text: '部署中...', kind: '' })
1061
+ setLastBuild((prev) => ({ ...prev, [envIdValue]: { time: Date.now(), status: 'running' } }))
1062
+ let url = SSE + '?env=' + encodeURIComponent(envIdValue) + '&modules=' + ids.map((id) => encodeURIComponent(id)).join(',')
1063
+ if (buildPath) url += '&path=' + encodeURIComponent(buildPath)
1064
+ let es
1065
+ try { es = new EventSource(url) } catch (error) {
1066
+ setDeployState({ running: false, text: '无法建立日志连接:' + (error.message || error), kind: 'err' })
1067
+ return
1068
+ }
1069
+ esRef.current = es
1070
+ let finished = false
1071
+ es.onmessage = (ev) => {
1072
+ let chunk
1073
+ try { chunk = JSON.parse(ev.data) } catch { return }
1074
+ if (typeof chunk !== 'string') return
1075
+ setLogText((prev) => prev + chunk)
1076
+ }
1077
+ es.addEventListener('replay', (ev) => {
1078
+ let text
1079
+ try { text = JSON.parse(ev.data) } catch { return }
1080
+ if (typeof text !== 'string') return
1081
+ setLogText(text)
1082
+ })
1083
+ es.addEventListener('done', (ev) => {
1084
+ finished = true
1085
+ let data = {}
1086
+ try { data = JSON.parse(ev.data) || {} } catch { data = {} }
1087
+ try { es.close() } catch { /* 已关闭 */ }
1088
+ if (esRef.current === es) esRef.current = null
1089
+ const code = data.code
1090
+ setDeployState(code === 0
1091
+ ? { running: false, text: '部署完成', kind: 'ok' }
1092
+ : { running: false, text: '部署失败 (exit ' + code + ')', kind: 'err' })
1093
+ setHistoryTick((t) => t + 1)
1094
+ refreshLastBuild()
1095
+ })
1096
+ es.onerror = () => {
1097
+ if (finished) return
1098
+ if (es.readyState === 2) {
1099
+ // 服务端 204(记录已不存在)或彻底失败:停止重连,结果看执行记录
1100
+ try { es.close() } catch { /* 已关闭 */ }
1101
+ if (esRef.current === es) esRef.current = null
1102
+ setDeployState({ running: false, text: '连接已断开,部署结果请查看下方执行记录', kind: 'err' })
1103
+ setHistoryTick((t) => t + 1)
1104
+ refreshLastBuild()
1105
+ } else {
1106
+ // EventSource 自动重连中:服务端会加入原部署并回放日志
1107
+ setDeployState({ running: true, text: '连接中断,重连中…', kind: 'warn' })
1108
+ }
1109
+ }
1110
+ }
1111
+
1112
+ function askClearHistory() {
1113
+ setConfirmState({
1114
+ title: '清空执行记录',
1115
+ body: '清空全部执行记录?(进行中的部署记录会保留)',
1116
+ danger: true,
1117
+ confirmLabel: '清空',
1118
+ onConfirm: () => {
1119
+ rpc('clearHistory').then(() => setHistoryTick((t) => t + 1))
1120
+ .catch((error) => setDeployState({ running: false, text: '清空失败:' + (error.message || error), kind: 'err' }))
1121
+ },
1122
+ })
1123
+ }
1124
+
1125
+ const allSelected = enabledModules.length > 0 && selected.length === enabledModules.length
1126
+ const canRun = !!draft && !isNew && selected.length > 0 && !deployState.running
1127
+
1128
+ // 标题行:对象名 + 一句「现状」(ID / 最近部署结果 / SSH 目标),全部来自已有数据
1129
+ const selSsh = draft && draft.ssh ? draft.ssh : {}
1130
+ const selSshText = selSsh.host ? (selSsh.user || 'root') + '@' + selSsh.host + ':' + (selSsh.port || 22) : ''
1131
+ const selBuild = draft ? lastBuild[draft.id] : null
1132
+ let titleSub = '选择左侧环境,或新增一个环境'
1133
+ let titleDot = ''
1134
+ if (draft) {
1135
+ if (isNew) titleSub = draft.id + ' · 新环境尚未保存,保存后才能部署'
1136
+ else if (selBuild) {
1137
+ titleDot = selBuild.status
1138
+ titleSub = draft.id + ' · 上次部署 ' + (HSTATUS[selBuild.status] || selBuild.status) + ' · ' + fmtTime(selBuild.time)
1139
+ + '(' + fmtAgo(selBuild.time) + ')' + (selSshText ? ' · ' + selSshText : '')
1140
+ } else titleSub = draft.id + ' · ' + (selSshText ? selSshText + ' · ' : '') + '从未部署'
1141
+ }
1142
+
1143
+ return h('div', { className: 'dad-root' },
1144
+ h('style', null, CSS),
1145
+ h('input', {
1146
+ type: 'file', accept: '.json,application/json', ref: importRef,
1147
+ style: { display: 'none' }, onChange: importFile,
1148
+ }),
1149
+ loadError ? h('div', { className: 'dad-loaderr' }, '配置读取失败:' + loadError) : null,
1150
+ notice ? h('div', { className: 'dad-notice ' + (notice.kind || ''), role: 'status' },
1151
+ h('span', { className: 'dad-notice-text' }, notice.text),
1152
+ h('button', {
1153
+ type: 'button', className: 'dad-notice-close', 'aria-label': '关闭提示',
1154
+ onClick: () => setNotice(null),
1155
+ }, '×')) : null,
1156
+ h(EnvList, {
1157
+ envs, lastBuild, draft,
1158
+ onSelect: selectEnv, onCopy: copyEnv, onDelete: askDeleteEnv, onAdd: addEnv,
1159
+ onImport: openImport,
1160
+ onExport: exportConfig,
1161
+ }),
1162
+ h('div', { className: 'dad-right' },
1163
+ h('div', { className: 'dad-title' },
1164
+ h('h1', { className: 'dad-h1' }, draft ? (draft.name || '未命名环境') : '部署'),
1165
+ h('p', { className: 'dad-title-sub' },
1166
+ titleDot ? h('i', { className: 'dad-dot st-' + titleDot, 'aria-hidden': true }) : null,
1167
+ titleSub)),
1168
+ h('div', { className: 'dad-card' },
1169
+ h('div', { className: 'dad-card-head' },
1170
+ h('span', { className: 'dad-card-title' }, '部署')),
1171
+ h(GitBar, {
1172
+ env: draft, git: gitInfo, buildPath,
1173
+ onPickPath: (value) => {
1174
+ setBuildPath(value === normPath(draft && draft.projectPath) ? '' : value)
1175
+ },
1176
+ }),
1177
+ h('div', { className: 'dad-modbar' },
1178
+ h('span', { className: 'dad-hint' }, '勾选要部署的模块:'),
1179
+ h('button', {
1180
+ type: 'button', className: 'dad-btn', onClick: toggleSelAll,
1181
+ disabled: enabledModules.length === 0,
1182
+ }, allSelected ? '清空' : '全选')),
1183
+ enabledModules.length === 0
1184
+ ? h('div', { className: 'dad-hint' }, '该环境没有启用的模块,请先在编辑区添加并启用模块。')
1185
+ : h('div', { className: 'dad-mods' },
1186
+ enabledModules.map((m) => h('label', {
1187
+ key: m.id, className: 'dad-opt',
1188
+ 'data-sel': selected.includes(m.id) ? '1' : '0',
1189
+ },
1190
+ h('input', {
1191
+ type: 'checkbox', checked: selected.includes(m.id),
1192
+ onChange: () => toggleSel(m.id),
1193
+ }),
1194
+ h('span', { className: 'dad-opt-main' },
1195
+ h('b', { className: 'dad-opt-name' }, m.name || m.id),
1196
+ h('span', { className: 'dad-sub' }, m.id + ' · ' + (KIND_LABEL[m.kind] || m.kind)))))),
1197
+ h('div', { className: 'dad-otitle' },
1198
+ '执行顺序(', h('b', null, String(selected.length)), ' 个模块,拖动调整顺序)'),
1199
+ h('div', {
1200
+ className: 'dad-order',
1201
+ onDragOver: (ev) => { ev.preventDefault(); if (ev.dataTransfer) ev.dataTransfer.dropEffect = 'move' },
1202
+ onDrop: (ev) => { ev.preventDefault(); orderDrop(null) },
1203
+ },
1204
+ selected.length === 0
1205
+ ? h('span', { className: 'dad-order-emptytext' }, '(空白)勾选上方模块添加,可拖动调整顺序')
1206
+ : selected.map((id, idx) => {
1207
+ const m = moduleById[id] || { id, name: id, kind: '' }
1208
+ return h('div', {
1209
+ key: id,
1210
+ className: 'dad-orderitem',
1211
+ draggable: true,
1212
+ 'data-dragging': draggingId === id ? '1' : '0',
1213
+ 'data-over': dragOverId === id ? '1' : '0',
1214
+ onDragStart: (ev) => {
1215
+ dragIdRef.current = id
1216
+ setDraggingId(id)
1217
+ if (ev.dataTransfer) {
1218
+ ev.dataTransfer.effectAllowed = 'move'
1219
+ try { ev.dataTransfer.setData('text/plain', id) } catch { /* 忽略 */ }
1220
+ }
1221
+ },
1222
+ onDragEnd: () => { dragIdRef.current = null; setDraggingId(''); setDragOverId('') },
1223
+ onDragOver: (ev) => { ev.preventDefault(); ev.stopPropagation(); setDragOverId(id) },
1224
+ onDrop: (ev) => { ev.preventDefault(); ev.stopPropagation(); orderDrop(id) },
1225
+ },
1226
+ h('span', { className: 'dad-onum' }, String(idx + 1)),
1227
+ h('span', { className: 'dad-oname' }, m.name || m.id),
1228
+ h('span', { className: 'dad-kindtag' }, KIND_LABEL[m.kind] || '模块'),
1229
+ h('button', {
1230
+ type: 'button', className: 'dad-ordel', title: '移除', 'aria-label': '从执行顺序移除',
1231
+ onClick: (ev) => { ev.stopPropagation(); toggleSel(id) },
1232
+ }, '×'))
1233
+ })),
1234
+ h('div', { className: 'dad-bar' },
1235
+ h('span', { className: 'dad-summary' },
1236
+ draft && selected.length
1237
+ ? ['环境 ', h('b', { key: 'n' }, draft.name), ' · 已选 ', h('b', { key: 'c' }, String(selected.length)), ' 个模块']
1238
+ : '未选择'),
1239
+ h('button', {
1240
+ type: 'button', className: 'dad-btn dad-btn-primary dad-run',
1241
+ disabled: !canRun, onClick: requestDeploy,
1242
+ }, deployState.running ? '部署中…' : '执行部署'),
1243
+ deployState.text && deployState.text !== '部署中...'
1244
+ ? h('span', { className: 'dad-msg ' + (deployState.kind || '') }, deployState.text)
1245
+ : null),
1246
+ isNew ? h('div', { className: 'dad-hint' }, '当前环境尚未保存,保存环境后才能执行部署。') : null),
1247
+ h('div', { className: 'dad-card' },
1248
+ h('div', { className: 'dad-card-head' },
1249
+ h('span', { className: 'dad-card-title' }, '部署日志'),
1250
+ !logText && !deployState.running && history[0]
1251
+ ? h('span', { className: 'dad-head-note' },
1252
+ '上次部署 ' + fmtTime(history[0].startTime)
1253
+ + (history[0].durationMs != null ? ' · 耗时 ' + fmtDur(history[0].durationMs) : ''))
1254
+ : null),
1255
+ h('pre', { className: 'dad-log', ref: logRef },
1256
+ logText || (deployState.running ? '等待日志…' : (history[0] && history[0].logTail) || '(尚未执行)'))),
1257
+ h('div', { className: 'dad-card' },
1258
+ h('div', { className: 'dad-card-head' },
1259
+ h('span', { className: 'dad-card-title' }, '执行记录'),
1260
+ h('button', {
1261
+ type: 'button', className: 'dad-btn', onClick: askClearHistory,
1262
+ }, '清空')),
1263
+ h('div', { className: 'dad-hist' },
1264
+ !envId
1265
+ ? h('div', { className: 'dad-hint' }, '(未选择已保存的环境)')
1266
+ : historyState === 'error'
1267
+ ? h('div', { className: 'dad-hint' }, '执行记录读取失败')
1268
+ : history.length === 0
1269
+ ? h('div', { className: 'dad-hint' }, historyState === 'loading' ? '读取中…' : '(暂无记录)')
1270
+ : history.map((r) => h('details', { key: r.id, className: 'dad-hrow' },
1271
+ h('summary', { className: 'dad-hsum' },
1272
+ h('span', { className: 'dad-badge ' + r.status }, HSTATUS[r.status] || r.status),
1273
+ h('span', { className: 'dad-mono', title: fmtTime(r.startTime) }, fmtAgo(r.startTime)),
1274
+ r.durationMs != null ? h('span', { className: 'dad-muted dad-mono' }, '耗时 ' + fmtDur(r.durationMs)) : null,
1275
+ h('span', { className: 'dad-hmods' }, (r.modules || []).map((m) => m.name || m.id).join(' + ')),
1276
+ r.status === 'failed'
1277
+ ? h('span', { className: 'dad-err dad-hfail' },
1278
+ '失败: ' + (r.failedModule || '部署后')
1279
+ + (r.failedStep ? ' / ' + r.failedStep : '')
1280
+ + ' (exit ' + r.exitCode + ')')
1281
+ : null),
1282
+ r.logTail ? h('pre', { className: 'dad-hlog' }, r.logTail) : null)))),
1283
+ h(EnvForm, {
1284
+ env: draft, isNew, groups, message: formMsg, open: formOpen,
1285
+ onToggle: () => setFormOpen((v) => !v),
1286
+ onPatch: patchEnv, onPatchSsh: patchSsh, onPatchModule: patchModule,
1287
+ onAddModule: addModule, onDeleteModule: askDeleteModule,
1288
+ onSave: saveEnv, onDelete: deleteCurrentEnv, onBrowse: openBrowse,
1289
+ jdks, jdkOpen, jdkRef, onJdkFocus: openJdk, onJdkPick: pickJdk,
1290
+ })),
1291
+ confirmState ? h(ConfirmDialog, { state: confirmState, onClose: () => setConfirmState(null) }) : null,
1292
+ browse ? h(BrowseModal, {
1293
+ state: browse, onClose: () => setBrowse(null),
1294
+ onNavigate: navigateBrowse, onUp: browseUp, onPick: browsePick,
1295
+ onToggleHidden: browseToggleHidden,
1296
+ }) : null)
1297
+ }
1298
+
1299
+ // ---------- 样式(颜色/背景/边框只用主题变量) ----------
1300
+
1301
+ const CSS = [
1302
+ '.dad-root{height:100%;box-sizing:border-box;display:flex;overflow:hidden;background:var(--dsw-alias-bg-base);color:var(--dsw-alias-label-primary);font-size:13px;line-height:1.5}',
1303
+ '.dad-root *{box-sizing:border-box}',
1304
+ '.dad-root :focus-visible{outline:2px solid var(--dsw-focus-ring-color,var(--dsw-alias-state-business-primary));outline-offset:2px}',
1305
+ '.dad-left{width:264px;flex:0 0 264px;min-width:0;min-height:0;display:flex;flex-direction:column;background:var(--dsw-alias-bg-module-platform);border-right:.5px solid var(--dsw-alias-border-l3)}',
1306
+ '.dad-left-head{flex:none;display:flex;align-items:center;gap:8px;padding:14px 12px 10px}',
1307
+ '.dad-left-title{flex:1;min-width:0;font-size:13px;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}',
1308
+ '.dad-envs{flex:1;min-height:0;overflow:auto;padding:0 10px 12px;display:flex;flex-direction:column;gap:6px}',
1309
+ '.dad-grp{margin:10px 2px 0;padding-left:10px;font-size:12px;color:var(--dsw-alias-label-tertiary)}',
1310
+ '.dad-grp:first-child{margin-top:0}',
1311
+ '.dad-env{position:relative;display:block;width:100%;min-width:0;padding:8px 10px;border:.5px solid var(--dsw-alias-border-l3);border-radius:var(--dsw-radius-lg);background:var(--dsw-alias-bg-layer-1);color:inherit;font:inherit;text-align:left}',
1312
+ '.dad-env-main{display:block;width:100%;min-width:0;margin:0;padding:0;border:0;background:none;color:inherit;font:inherit;text-align:left;cursor:pointer}',
1313
+ '.dad-env:hover{background:var(--dsw-alias-interactive-bg-hover);border-color:var(--dsw-alias-border-l4)}',
1314
+ '.dad-env[data-sel="1"]{border-color:var(--dsw-alias-border-l3);background:var(--dsw-alias-state-business-tertiary)}',
1315
+ '.dad-env-name{display:block;font-size:13.5px;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;padding-right:58px}',
1316
+ '.dad-envops{position:absolute;top:6px;right:8px;display:none;gap:8px;align-items:center}',
1317
+ '.dad-env:hover .dad-envops,.dad-env:focus-within .dad-envops{display:flex}',
1318
+ '.dad-linkbtn{border:0;background:none;padding:0;font:inherit;font-size:12px;cursor:pointer;color:var(--dsw-alias-state-business-primary)}',
1319
+ '.dad-linkbtn-danger{color:var(--dsw-alias-state-error-primary)}',
1320
+ '.dad-right{flex:1 1 auto;min-width:0;min-height:0;overflow:auto;padding:16px 20px 32px;display:flex;flex-direction:column;gap:16px}',
1321
+ '.dad-title{margin:2px 0 0;min-width:0}',
1322
+ '.dad-h1{margin:0;font-size:20px;font-weight:600;line-height:1.3;letter-spacing:-0.01em;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}',
1323
+ '.dad-title-sub{margin:5px 0 0;font-size:13px;color:var(--dsw-alias-label-secondary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}',
1324
+ '.dad-loaderr{flex:none;margin:12px 16px 0;padding:8px 12px;border-radius:var(--dsw-radius-md);border:.5px solid var(--dsw-alias-state-error-primary);color:var(--dsw-alias-state-error-primary);font-size:12px}',
1325
+ '.dad-notice{position:fixed;top:12px;left:50%;transform:translateX(-50%);z-index:70;display:flex;align-items:center;gap:10px;max-width:min(640px,80vw);padding:7px 12px;border-radius:var(--dsw-radius-md);border:.5px solid var(--dsw-alias-border-l3);background:var(--dsw-alias-bg-layer-2);box-shadow:var(--dsw-elevation-prominent);font-size:12px;color:var(--dsw-alias-label-primary)}',
1326
+ '.dad-notice.ok{border-color:var(--dsw-alias-state-success-primary);color:var(--dsw-alias-state-success-primary)}',
1327
+ '.dad-notice.err{border-color:var(--dsw-alias-state-error-primary);color:var(--dsw-alias-state-error-primary)}',
1328
+ '.dad-notice.warn{border-color:var(--dsw-alias-state-warn-primary);color:var(--dsw-alias-state-warn-label)}',
1329
+ '.dad-notice-text{min-width:0;overflow-wrap:anywhere}',
1330
+ '.dad-notice-close{border:0;background:none;color:inherit;font-size:14px;line-height:1;padding:0 2px;cursor:pointer;opacity:.7}',
1331
+ '.dad-notice-close:hover{opacity:1}',
1332
+ '.dad-card{min-width:0;padding:14px 16px;border:.5px solid var(--dsw-alias-border-l3);border-radius:var(--dsw-radius-lg);background:var(--dsw-alias-bg-layer-1)}',
1333
+ '.dad-card-head{display:flex;align-items:center;gap:8px;min-width:0;margin-bottom:12px}',
1334
+ '.dad-card-title{flex:1;min-width:0;font-size:13px;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}',
1335
+ '.dad-sec{padding:12px 14px;margin-bottom:10px;border-radius:var(--dsw-radius-lg);min-width:0;background:var(--dsw-alias-bg-module-platform)}',
1336
+ '.dad-sec-title{margin-bottom:10px;font-size:12.5px;font-weight:600;color:var(--dsw-alias-label-secondary)}',
1337
+ '.dad-row{display:flex;flex-wrap:wrap;gap:8px;margin-bottom:8px;align-items:flex-end;min-width:0}',
1338
+ '.dad-row:last-child{margin-bottom:0}',
1339
+ '.dad-field{display:flex;flex-direction:column;gap:3px;flex:1 1 170px;min-width:0}',
1340
+ '.dad-field-wide{flex:2 1 300px}',
1341
+ '.dad-label{font-size:12px;font-weight:500;color:var(--dsw-alias-label-secondary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}',
1342
+ '.dad-input,.dad-select,.dad-textarea{width:100%;min-width:0;border:.5px solid var(--dsw-alias-border-l4);border-radius:var(--dsw-radius-md);background:var(--dsw-alias-bg-layer-3);color:var(--dsw-alias-label-primary);font:inherit;font-size:13px;padding:5px 9px;outline:none}',
1343
+ '.dad-input:focus,.dad-select:focus,.dad-textarea:focus{border-color:var(--dsw-alias-state-business-primary)}',
1344
+ '.dad-input:disabled{color:var(--dsw-alias-label-tertiary);cursor:not-allowed}',
1345
+ '.dad-textarea{resize:vertical;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:11.5px;line-height:1.5}',
1346
+ '.dad-inline{display:flex;gap:6px;align-items:center;min-width:0}',
1347
+ '.dad-inline .dad-input{flex:1 1 auto}',
1348
+ '.dad-chk{display:inline-flex;align-items:center;gap:6px;flex:0 0 auto;align-self:flex-end;padding-bottom:5px;font-size:12px;color:var(--dsw-alias-label-secondary);cursor:pointer}',
1349
+ '.dad-chk input[type=checkbox]{accent-color:var(--dsw-alias-state-business-primary);width:14px;height:14px;margin:0}',
1350
+ '.dad-autoflex{align-self:center;padding-bottom:0}',
1351
+ '.dad-hint{margin-top:4px;font-size:12px;color:var(--dsw-alias-label-tertiary)}',
1352
+ '.dad-muted{color:var(--dsw-alias-label-tertiary)}',
1353
+ '.dad-err{color:var(--dsw-alias-state-error-primary)}',
1354
+ '.dad-msg{font-size:12.5px;font-weight:600;min-width:0;overflow-wrap:anywhere;color:var(--dsw-alias-label-primary)}',
1355
+ '.dad-msg.ok::before,.dad-msg.err::before,.dad-msg.warn::before{content:"";display:inline-block;width:7px;height:7px;border-radius:50%;margin-right:6px;vertical-align:1px}',
1356
+ '.dad-msg.ok::before{background:var(--dsw-alias-state-success-primary)}',
1357
+ '.dad-msg.err::before{background:var(--dsw-alias-state-error-primary)}',
1358
+ '.dad-msg.warn::before{background:var(--dsw-alias-state-warn-primary)}',
1359
+ '.dad-mono{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:11.5px}',
1360
+ '.dad-sub{display:block;margin-top:2px;font-size:12px;color:var(--dsw-alias-label-secondary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}',
1361
+ '.dad-dot{display:inline-block;width:7px;height:7px;border-radius:50%;background:var(--dsw-alias-label-caption);margin-right:5px;vertical-align:1px}',
1362
+ '.dad-dot.st-success{background:var(--dsw-alias-state-success-primary)}',
1363
+ '.dad-dot.st-failed{background:var(--dsw-alias-state-error-primary)}',
1364
+ '.dad-dot.st-running,.dad-dot.st-aborted{background:var(--dsw-alias-state-warn-primary)}',
1365
+ '.dad-env-name .dad-dot,.dad-title-sub .dad-dot{margin-right:6px;vertical-align:1px}',
1366
+ '.dad-head-note{flex:0 0 auto;font-size:12px;color:var(--dsw-alias-label-tertiary);font-variant-numeric:tabular-nums}',
1367
+ '.dad-build.success .dad-dot{background:var(--dsw-alias-state-success-primary)}',
1368
+ '.dad-build.failed .dad-dot{background:var(--dsw-alias-state-error-primary)}',
1369
+ '.dad-build.running .dad-dot,.dad-build.aborted .dad-dot{background:var(--dsw-alias-state-warn-primary)}',
1370
+ '.dad-btn{flex:0 0 auto;border:.5px solid var(--dsw-alias-border-l3);border-radius:var(--dsw-radius-md);background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);font:inherit;font-size:12.5px;padding:5px 11px;cursor:pointer;white-space:nowrap}',
1371
+ '.dad-btn:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}',
1372
+ '.dad-btn:disabled{opacity:.5;cursor:default}',
1373
+ '.dad-btn-primary{background:var(--dsw-alias-button-primary-fill);border-color:transparent;color:var(--dsw-alias-label-primary-foreground)}',
1374
+ '.dad-btn-primary:hover:not(:disabled){background:var(--dsw-alias-button-primary-hover)}',
1375
+ '.dad-btn-danger{color:var(--dsw-alias-state-error-primary)}',
1376
+ '.dad-btn-danger:hover:not(:disabled){border-color:var(--dsw-alias-state-error-primary)}',
1377
+ '.dad-btn-danger-solid{background:var(--dsw-alias-state-error-primary);border-color:transparent;color:var(--dsw-alias-label-primary-foreground)}',
1378
+ '.dad-btn-danger-solid:hover:not(:disabled){background:var(--dsw-alias-state-error-primary)}',
1379
+ '.dad-formfoot{display:flex;flex-wrap:wrap;align-items:center;gap:8px;margin-top:4px}',
1380
+ '.dad-mcard{position:relative;min-width:0;padding:10px 12px;margin-bottom:10px;border:.5px solid var(--dsw-alias-border-l3);border-radius:var(--dsw-radius-lg);background:var(--dsw-alias-bg-layer-2)}',
1381
+ '.dad-mcard[data-off="1"]{opacity:.6}',
1382
+ '.dad-btndel{position:absolute;top:4px;right:6px;border:0;background:none;color:var(--dsw-alias-label-tertiary);font-size:14px;line-height:1;padding:2px 4px;cursor:pointer}',
1383
+ '.dad-btndel:hover{color:var(--dsw-alias-state-error-primary)}',
1384
+ '.dad-jdkwrap{position:relative;display:block;min-width:0}',
1385
+ '.dad-jdkdd{position:absolute;top:100%;left:0;z-index:40;margin-top:3px;min-width:280px;max-width:min(460px,80vw);max-height:240px;overflow:auto;background:var(--dsw-alias-bg-layer-2);border:.5px solid var(--dsw-alias-border-l3);border-radius:var(--dsw-radius-md);box-shadow:var(--dsw-elevation-prominent)}',
1386
+ '.dad-jitem{display:flex;flex-direction:column;align-items:flex-start;gap:1px;width:100%;border:0;border-bottom:.5px solid var(--dsw-alias-border-l1);background:none;color:inherit;font:inherit;text-align:left;padding:6px 10px;cursor:pointer}',
1387
+ '.dad-jitem:hover{background:var(--dsw-alias-interactive-bg-hover)}',
1388
+ '.dad-jlbl{font-size:12px;font-weight:600}',
1389
+ '.dad-jfull{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:10.5px;color:var(--dsw-alias-label-tertiary);word-break:break-all}',
1390
+ '.dad-jempty{padding:8px 10px;font-size:11.5px;color:var(--dsw-alias-label-tertiary)}',
1391
+ '.dad-gitbar{display:flex;align-items:center;gap:8px;flex-wrap:wrap;min-width:0;margin-bottom:12px;padding:8px 12px;font-size:12.5px;border:.5px solid var(--dsw-alias-border-l1);border-radius:var(--dsw-radius-md);background:var(--dsw-alias-bg-module-platform)}',
1392
+ '.dad-ghead{display:inline-flex;align-items:center;gap:6px;flex:0 0 auto}',
1393
+ '.dad-branch{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:11.5px;font-weight:600;color:var(--dsw-alias-state-business-primary);border:.5px solid var(--dsw-alias-border-l3);border-radius:var(--dsw-radius-sm);padding:2px 7px;background:var(--dsw-alias-bg-layer-1)}',
1394
+ '.dad-gpath{flex:1 1 160px;min-width:0;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:11.5px;color:var(--dsw-alias-label-secondary);word-break:break-all}',
1395
+ '.dad-gtmp{flex:0 0 auto;font-size:11.5px;color:var(--dsw-alias-state-warn-label);border:.5px solid var(--dsw-alias-state-warn-primary);border-radius:var(--dsw-radius-sm);padding:1px 6px}',
1396
+ '.dad-gsel{flex:0 0 auto;width:auto;max-width:100%;font-size:12.5px}',
1397
+ '.dad-modbar{display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin-bottom:8px}',
1398
+ '.dad-mods{display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:8px;min-width:0}',
1399
+ '.dad-opt{display:flex;align-items:center;gap:8px;min-width:0;padding:8px 10px;border:.5px solid var(--dsw-alias-border-l3);border-radius:var(--dsw-radius-md);background:var(--dsw-alias-bg-layer-1);cursor:pointer}',
1400
+ '.dad-opt:hover{border-color:var(--dsw-alias-border-l4);background:var(--dsw-alias-interactive-bg-hover)}',
1401
+ '.dad-opt[data-sel="1"]{border-color:var(--dsw-alias-state-business-primary);background:var(--dsw-alias-state-business-tertiary)}',
1402
+ '.dad-opt input[type=checkbox]{flex:0 0 auto;width:14px;height:14px;margin:0;accent-color:var(--dsw-alias-state-business-primary)}',
1403
+ '.dad-opt-main{flex:1 1 auto;min-width:0;display:flex;flex-direction:column}',
1404
+ '.dad-opt-name{font-size:12.5px;font-weight:500;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}',
1405
+ '.dad-otitle{margin:10px 0 6px;font-size:12px;color:var(--dsw-alias-label-secondary)}',
1406
+ '.dad-otitle b{color:var(--dsw-alias-state-business-primary)}',
1407
+ '.dad-order{display:flex;flex-wrap:wrap;align-items:center;gap:6px;min-height:42px;padding:6px;border:1px dashed var(--dsw-alias-border-l3);border-radius:var(--dsw-radius-md);background:var(--dsw-alias-bg-module-platform)}',
1408
+ '.dad-order-emptytext{font-size:12px;color:var(--dsw-alias-label-secondary);padding:2px 4px}',
1409
+ '.dad-orderitem{display:inline-flex;align-items:center;gap:6px;max-width:220px;min-width:0;padding:4px 8px;border:.5px solid var(--dsw-alias-border-l3);border-radius:var(--dsw-radius-md);background:var(--dsw-alias-bg-layer-1);font-size:12px;cursor:grab;user-select:none}',
1410
+ '.dad-orderitem:hover{border-color:var(--dsw-alias-border-l4)}',
1411
+ '.dad-orderitem[data-over="1"]{border-color:var(--dsw-alias-state-business-primary);background:var(--dsw-alias-interactive-bg-hover)}',
1412
+ '.dad-orderitem[data-dragging="1"]{opacity:.45}',
1413
+ '.dad-onum{flex:0 0 auto;width:17px;height:17px;border-radius:50%;background:var(--dsw-alias-state-business-primary);color:var(--dsw-alias-label-primary-foreground);font-size:10px;display:inline-flex;align-items:center;justify-content:center}',
1414
+ '.dad-oname{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-weight:500}',
1415
+ '.dad-kindtag{flex:0 0 auto;font-size:10px;color:var(--dsw-alias-label-tertiary);border:.5px solid var(--dsw-alias-border-l3);border-radius:var(--dsw-radius-sm);padding:0 5px}',
1416
+ '.dad-ordel{flex:0 0 auto;border:0;background:none;color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:1;padding:0 2px;cursor:pointer}',
1417
+ '.dad-ordel:hover{color:var(--dsw-alias-state-error-primary)}',
1418
+ '.dad-bar{display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin:14px -16px -14px;padding:12px 16px;background:var(--dsw-alias-bg-module-platform);border-radius:0 0 var(--dsw-radius-lg) var(--dsw-radius-lg)}',
1419
+ '.dad-summary{font-size:12.5px}',
1420
+ '.dad-summary b{color:var(--dsw-alias-label-primary)}',
1421
+ '.dad-run{padding:8px 26px;font-size:13.5px;font-weight:600}',
1422
+ '.dad-run:disabled{opacity:.4;cursor:default}',
1423
+ '.dad-log{margin:0;height:200px;overflow:auto;padding:10px 12px;border:.5px solid var(--dsw-alias-border-l1);border-radius:var(--dsw-radius-lg);background:var(--dsw-alias-markdown-code-block);color:var(--dsw-alias-label-primary);font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:11.5px;line-height:1.5;white-space:pre-wrap;word-break:break-all}',
1424
+ '.dad-hist{display:flex;flex-direction:column;gap:6px;min-width:0}',
1425
+ '.dad-hrow{border:.5px solid var(--dsw-alias-border-l3);border-radius:var(--dsw-radius-md);background:var(--dsw-alias-bg-layer-1);min-width:0}',
1426
+ '.dad-hrow:hover{background:var(--dsw-alias-interactive-bg-hover)}',
1427
+ '.dad-hsum{display:flex;align-items:center;gap:10px;flex-wrap:wrap;padding:8px 12px;font-size:12.5px;cursor:pointer;list-style:none;font-variant-numeric:tabular-nums}',
1428
+ '.dad-hsum::-webkit-details-marker{display:none}',
1429
+ '.dad-badge{flex:0 0 auto;display:inline-flex;align-items:center;gap:6px;font-size:12px;font-weight:500;color:var(--dsw-alias-label-secondary)}',
1430
+ '.dad-badge::before{content:"";flex:none;width:7px;height:7px;border-radius:50%;background:var(--dsw-alias-label-caption)}',
1431
+ '.dad-badge.success::before{background:var(--dsw-alias-state-success-primary)}',
1432
+ '.dad-badge.failed::before{background:var(--dsw-alias-state-error-primary)}',
1433
+ '.dad-badge.running::before{background:var(--dsw-alias-state-business-primary)}',
1434
+ '.dad-badge.aborted::before{background:var(--dsw-alias-state-warn-primary)}',
1435
+ '.dad-hmods{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}',
1436
+ '.dad-hfail{min-width:0;overflow-wrap:anywhere}',
1437
+ '.dad-hlog{margin:0 10px 10px;max-height:220px;overflow:auto;padding:8px 10px;border-radius:var(--dsw-radius-md);background:var(--dsw-alias-markdown-code-block);color:var(--dsw-alias-label-primary);font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:11px;line-height:1.5;white-space:pre-wrap;word-break:break-all}',
1438
+ '.dad-mask{position:fixed;inset:0;z-index:80;display:flex;align-items:center;justify-content:center;padding:20px;background:var(--dsw-alias-bg-mask-2)}',
1439
+ '.dad-modal{display:flex;flex-direction:column;width:min(560px,100%);max-height:min(80vh,720px);min-width:0;border:.5px solid var(--dsw-alias-border-l3);border-radius:var(--dsw-radius-panel);background:var(--dsw-alias-bg-layer-2);box-shadow:var(--dsw-elevation-prominent);overflow:hidden}',
1440
+ '.dad-modal-head{flex:none;display:flex;align-items:center;gap:10px;padding:10px 14px;border-bottom:.5px solid var(--dsw-alias-border-l3);min-width:0}',
1441
+ '.dad-modal-body{flex:1 1 auto;min-height:0;overflow:auto;padding:10px 14px}',
1442
+ '.dad-modal-foot{flex:none;display:flex;align-items:center;justify-content:flex-end;gap:8px;padding:10px 14px;border-top:.5px solid var(--dsw-alias-border-l3)}',
1443
+ '.dad-spacer{flex:1 1 auto}',
1444
+ '.dad-plan{margin:0;padding:10px 12px;max-height:46vh;overflow:auto;border-radius:var(--dsw-radius-md);background:var(--dsw-alias-markdown-code-block);color:var(--dsw-alias-label-primary);font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:11.5px;line-height:1.55;white-space:pre-wrap;word-break:break-all}',
1445
+ '.dad-plan-text{font-size:12.5px;color:var(--dsw-alias-label-secondary);overflow-wrap:anywhere}',
1446
+ '.dad-browse-path{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:11px;color:var(--dsw-alias-label-secondary);word-break:break-all;padding:6px 10px;margin-bottom:8px;border-radius:var(--dsw-radius-md);background:var(--dsw-alias-bg-module-platform)}',
1447
+ '.dad-wts{display:flex;flex-wrap:wrap;align-items:center;gap:6px;margin-bottom:8px;min-width:0}',
1448
+ '.dad-wt{display:inline-flex;align-items:center;gap:5px;max-width:100%;min-width:0;border:.5px solid var(--dsw-alias-border-l3);border-radius:var(--dsw-radius-md);background:none;color:inherit;font:inherit;font-size:11px;padding:2px 8px;cursor:pointer}',
1449
+ '.dad-wt:hover{border-color:var(--dsw-alias-state-business-primary);color:var(--dsw-alias-state-business-primary)}',
1450
+ '.dad-wt[data-cur="1"]{border-color:var(--dsw-alias-state-business-primary);color:var(--dsw-alias-state-business-primary);cursor:default}',
1451
+ '.dad-wic{flex:0 0 auto;color:var(--dsw-alias-label-tertiary)}',
1452
+ '.dad-wbr{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}',
1453
+ '.dad-dirs{display:flex;flex-direction:column;gap:1px;max-height:46vh;overflow:auto;padding:4px;border:.5px solid var(--dsw-alias-border-l1);border-radius:var(--dsw-radius-md)}',
1454
+ '.dad-dir{border:0;background:none;color:var(--dsw-alias-label-primary);font:inherit;font-size:12px;text-align:left;padding:5px 8px;border-radius:var(--dsw-radius-sm);cursor:pointer;word-break:break-all;min-width:0}',
1455
+ '.dad-dir:hover{background:var(--dsw-alias-interactive-bg-hover)}',
1456
+ '.dad-envs,.dad-right,.dad-log,.dad-dirs,.dad-modal-body,.dad-jdkdd,.dad-hlog,.dad-plan{--dsh-scrollbar-thumb:var(--dsw-alias-scrollbar-bg-l2);--dsh-scrollbar-thumb-hover:var(--dsw-alias-scrollbar-hover-l2)}',
1457
+ '@media (max-width:900px){.dad-root{flex-direction:column}.dad-left{width:auto;flex:0 0 auto;max-height:38vh;border-right:0;border-bottom:.5px solid var(--dsw-alias-border-l3)}.dad-right{flex:1 1 auto}.dad-h1{font-size:18px}.dad-title-sub{font-size:12px}}',
1458
+ ].join('\n')
1459
+
1460
+ return {
1461
+ inject: ['slots'],
1462
+ apply(ctx) {
1463
+ ctx.slots.inject('sidebar.panellist', () => ctx.slots.register({
1464
+ name: 'sidebar.panellist', id: 'deploy', label: '部署', order: 45,
1465
+ }, Icon))
1466
+ ctx.slots.inject('main', () => ctx.slots.register({
1467
+ name: 'main', key: 'deploy',
1468
+ }, Panel))
1469
+ },
1470
+ }
1471
+ },
1472
+ })