dsh-app-deploy 0.0.0-stage → 0.1.0

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