diff --git a/frontend/components.d.ts b/frontend/components.d.ts index d9fb1f7..95b7471 100644 --- a/frontend/components.d.ts +++ b/frontend/components.d.ts @@ -14,6 +14,9 @@ declare module 'vue' { BusinessLineSwitcher: typeof import('./src/components/BusinessLineSwitcher.vue')['default'] ElButton: typeof import('element-plus/es')['ElButton'] ElDialog: typeof import('element-plus/es')['ElDialog'] + ElDropdown: typeof import('element-plus/es')['ElDropdown'] + ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem'] + ElDropdownMenu: typeof import('element-plus/es')['ElDropdownMenu'] ElForm: typeof import('element-plus/es')['ElForm'] ElFormItem: typeof import('element-plus/es')['ElFormItem'] ElIcon: typeof import('element-plus/es')['ElIcon'] @@ -26,8 +29,6 @@ declare module 'vue' { ElSwitch: typeof import('element-plus/es')['ElSwitch'] ElTable: typeof import('element-plus/es')['ElTable'] ElTableColumn: typeof import('element-plus/es')['ElTableColumn'] - ElTabPane: typeof import('element-plus/es')['ElTabPane'] - ElTabs: typeof import('element-plus/es')['ElTabs'] RouterLink: typeof import('vue-router')['RouterLink'] RouterView: typeof import('vue-router')['RouterView'] SubsystemCard: typeof import('./src/components/SubsystemCard.vue')['default'] diff --git a/frontend/src/views/service/Catalog.vue b/frontend/src/views/service/Catalog.vue index 8e08636..e1e9b66 100644 --- a/frontend/src/views/service/Catalog.vue +++ b/frontend/src/views/service/Catalog.vue @@ -56,8 +56,9 @@ :key="tab.key" type="button" class="flow-tab" - :class="{ active: activeView === tab.key }" - @click="activeView = tab.key" + :class="{ active: activeView === tab.key && (tab.key !== 'result' || resultReady), disabled: tab.key === 'result' && !resultReady }" + :disabled="tab.key === 'result' && !resultReady" + @click="selectFlowTab(tab.key)" > {{ tab.title }} {{ tab.subtitle }} @@ -82,11 +83,14 @@
@@ -325,7 +329,7 @@
-
+

AWX 自动交付

@@ -409,11 +413,11 @@ 仅展示一次

请立即复制或下载凭证文件;关闭后页面不再显示明文密码。

- 查看一次性密码 + 查看一次性密码
- {{ deliveryForm.rootPassword }} + {{ revealedRootCredential?.password }}
- 复制密码 + 复制密码 下载凭证文件 已保存,关闭
@@ -491,7 +495,7 @@ import { computed, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue' import { ElMessage, ElMessageBox } from 'element-plus' import { ArrowDown, Back, CircleCheck, Connection, CopyDocument, DocumentCopy, Download, Monitor, Promotion, Refresh, Setting, Tickets } from '@element-plus/icons-vue' import { useRoute, useRouter } from 'vue-router' -import { deliveryApi, type DeliveryTarget, type TaskEvent } from '@/api/delivery' +import { deliveryApi, type DeliveryTarget, type DeploymentCredential, type TaskEvent } from '@/api/delivery' import { useAuthStore } from '@/stores/auth' import { useBusinessLineStore } from '@/stores/businessLine' import { useBusinessLineMockProfile } from '@/utils/businessLineMock' @@ -560,8 +564,45 @@ interface DeliveryHistoryItem { errorMessage: string createdAt: string updatedAt: string - credential?: string - credentialConsumed?: boolean +} + +interface WorkbenchSnapshot { + version: 1 + serviceKey: string + activeView: FlowView + precheckPassed: boolean + running: boolean + deliveryDone: boolean + deliveryFailed: boolean + deploymentId: string + deliveryError: string + deliveredHost: string + deliveredPort?: number + lastDeliveryStatus: string + deliveryLog: string + steps: DeliveryStep[] + form: { + instanceName: string + version: string + mode: string + spec: string + storageGi: number + port?: number + charset: string + collation: string + lowerCaseTableNames: number + timezone: string + dataDisk: string + targetHost: string + maxConnections: string + redoCapacity: string + flushLogAtCommit: number + syncBinlog: number + ioCapacity: number + longQueryTime: number + binlogExpireSeconds: number + maxBinlogSize: string + } } const { currentName } = useBusinessLineMockProfile() @@ -587,7 +628,7 @@ const basicServices = ref([ modes: [ { value: 'single', label: '单实例' }, ], - specs: ['0.5C / 2G', '1C / 2G', '2C / 4G', '4C / 8G', '8C / 16G', '16C / 32G'], + specs: ['1C / 2G', '2C / 4G', '4C / 8G', '8C / 16G', '16C / 32G'], disks: ['20 GB', '50 GB', '100 GB', '500 GB', '1000 GB'], charsets: ['utf8mb4', 'utf8', 'gbk', 'latin1'], defaultPort: 0, @@ -720,17 +761,14 @@ const precheckPassed = ref(false) const running = ref(false) const deliveryDone = ref(false) const deliveryFailed = ref(false) +const taskStateLoading = ref(false) const credentialRevealed = ref(false) const credentialDismissed = ref(false) +const credentialRevealing = ref(false) +const revealedCredentials = ref([]) const deliveryHistory = ref(loadDeliveryHistory()) -const deliveryCredentials = new Map() -const deliveryCredentialStates = new Map() -deliveryHistory.value.forEach((item) => { - if (item.credential && !item.credentialConsumed) { - deliveryCredentials.set(item.id, item.credential) - deliveryCredentialStates.set(item.id, { revealed: false, dismissed: false }) - } -}) +let workbenchReady = false +let restoringWorkbench = false const canceling = ref(false) const deploymentId = ref('') const deliveryError = ref('') @@ -758,7 +796,7 @@ const deliveryForm = reactive({ version: 'MySQL 8.0', mode: 'single', spec: '1C / 2G', - storageGi: 50, + storageGi: 20, port: undefined as number | undefined, rootPassword: generateRootPassword(), charset: 'utf8mb4', @@ -850,6 +888,8 @@ const runnerPreview = computed(() => { }) const connectionReady = computed(() => Boolean(deliveredHost.value && deliveredPort.value)) const credentialEligible = computed(() => connectionReady.value && ['finished', 'register_failed'].includes(lastDeliveryStatus.value)) +const revealedRootCredential = computed(() => revealedCredentials.value.find((item) => item.username === 'root')) +const resultReady = computed(() => !taskStateLoading.value && !running.value && isTerminalDeliveryStatus(lastDeliveryStatus.value)) const previewAddress = computed(() => `${selectedHost.value?.ip || 'AWX 自动分配'}:${deliveryForm.port || '自动端口'}`) const resultAddress = computed(() => connectionReady.value ? `${deliveredHost.value}:${deliveredPort.value}` : '交付接口未返回地址') const connectionUri = computed(() => connectionReady.value ? `mysql://root@${deliveredHost.value}:${deliveredPort.value}` : '') @@ -918,16 +958,24 @@ watch( watch(deliveryForm, () => { precheckPassed.value = false + if (workbenchReady && !restoringWorkbench) persistWorkbench() }, { deep: true }) watch(activeServiceKey, () => { hydrateServiceDefaults() + if (activeServiceKey.value) { + workbenchReady = false + restoreWorkbench() + } else { + workbenchReady = true + } }) watch(activeView, (view, previousView) => { if (previousView === 'result' && view !== 'result' && credentialRevealed.value) { dismissCredential() } + if (workbenchReady && !restoringWorkbench) persistWorkbench() }) watch( @@ -942,9 +990,11 @@ hydrateServiceDefaults() onMounted(() => { if (route.query.preview === 'credential') { setupCredentialPreview() + workbenchReady = true return } loadDeliveryTargets() + restoreWorkbench() }) onBeforeUnmount(stopPolling) @@ -965,10 +1015,130 @@ function backToCatalog() { router.push({ name: 'ServiceCatalog' }) } +function selectFlowTab(view: FlowView) { + if (view === 'result' && !resultReady.value) return + activeView.value = view +} + function openDeliveryRecords() { router.push({ name: 'ServiceManagement' }) } +function workbenchStorageKey() { + return `xinfra:delivery-workbench:${currentName.value}:${activeServiceKey.value || 'catalog'}` +} + +function persistWorkbench() { + if (!workbenchReady || !activeServiceKey.value) return + const snapshot: WorkbenchSnapshot = { + version: 1, + serviceKey: activeServiceKey.value, + activeView: activeView.value, + precheckPassed: precheckPassed.value, + running: running.value, + deliveryDone: deliveryDone.value, + deliveryFailed: deliveryFailed.value, + deploymentId: deploymentId.value, + deliveryError: deliveryError.value, + deliveredHost: deliveredHost.value, + deliveredPort: deliveredPort.value, + lastDeliveryStatus: lastDeliveryStatus.value, + deliveryLog: deliveryLog.value, + steps: steps.value, + form: { + instanceName: deliveryForm.instanceName, + version: deliveryForm.version, + mode: deliveryForm.mode, + spec: deliveryForm.spec, + storageGi: deliveryForm.storageGi, + port: deliveryForm.port, + charset: deliveryForm.charset, + collation: deliveryForm.collation, + lowerCaseTableNames: deliveryForm.lowerCaseTableNames, + timezone: deliveryForm.timezone, + dataDisk: deliveryForm.dataDisk, + targetHost: deliveryForm.targetHost, + maxConnections: deliveryForm.maxConnections, + redoCapacity: deliveryForm.redoCapacity, + flushLogAtCommit: deliveryForm.flushLogAtCommit, + syncBinlog: deliveryForm.syncBinlog, + ioCapacity: deliveryForm.ioCapacity, + longQueryTime: deliveryForm.longQueryTime, + binlogExpireSeconds: deliveryForm.binlogExpireSeconds, + maxBinlogSize: deliveryForm.maxBinlogSize, + }, + } + try { + sessionStorage.setItem(workbenchStorageKey(), JSON.stringify(snapshot)) + } catch { + // Session storage may be disabled; the backend task history remains available. + } +} + +async function restoreWorkbench() { + let snapshot: WorkbenchSnapshot | undefined + try { + const raw = sessionStorage.getItem(workbenchStorageKey()) + snapshot = raw ? JSON.parse(raw) as WorkbenchSnapshot : undefined + } catch { + snapshot = undefined + } + + if (!snapshot || snapshot.version !== 1 || snapshot.serviceKey !== activeServiceKey.value) { + workbenchReady = true + return + } + + restoringWorkbench = true + Object.assign(deliveryForm, snapshot.form) + deliveryForm.rootPassword = generateRootPassword() + precheckPassed.value = snapshot.precheckPassed + running.value = snapshot.running + deliveryDone.value = snapshot.deliveryDone + deliveryFailed.value = snapshot.deliveryFailed + deploymentId.value = snapshot.deploymentId + deliveryError.value = snapshot.deliveryError + deliveredHost.value = snapshot.deliveredHost + deliveredPort.value = snapshot.deliveredPort + lastDeliveryStatus.value = snapshot.lastDeliveryStatus + deliveryLog.value = snapshot.deliveryLog + steps.value = snapshot.steps?.length ? snapshot.steps : defaultSteps() + credentialRevealed.value = false + credentialDismissed.value = Boolean(snapshot.deploymentId) + + if (snapshot.deploymentId) { + taskStateLoading.value = true + try { + const data = await deliveryApi.getTask(snapshot.deploymentId) + deliveredHost.value = data.task.target_host_ip || deliveredHost.value + deliveredPort.value = data.task.mysql_port || deliveredPort.value + lastDeliveryStatus.value = data.task.status + deliveryError.value = data.task.error_message || deliveryError.value + seenEventIds.value = new Set(data.events.map((event) => event.id)) + applyDeliveryStatus(data.task.status, data.task.error_message || '') + if (isTerminalDeliveryStatus(data.task.status)) { + activeView.value = 'result' + stopPolling() + } else { + activeView.value = 'execution' + startTaskPolling(snapshot.deploymentId) + } + } catch (error) { + appendLog(`[restore] ${error instanceof Error ? error.message : '获取任务状态失败'}`) + running.value = true + activeView.value = 'execution' + } finally { + taskStateLoading.value = false + } + } else { + activeView.value = snapshot.activeView + } + + restoringWorkbench = false + workbenchReady = true + persistWorkbench() +} + function hydrateServiceDefaults() { const service = activeService.value if (!service) return @@ -976,8 +1146,12 @@ function hydrateServiceDefaults() { deliveryForm.instanceName = `${prefix}-${currentName.value}-billing-02` deliveryForm.version = service.versions[0] || '' deliveryForm.mode = service.modes[1]?.value || service.modes[0]?.value || 'single' - deliveryForm.spec = service.specs[1] || service.specs[0] || '' - deliveryForm.storageGi = parseStorageGi(service.disks[1] || service.disks[0] || '50 GB') + deliveryForm.spec = service.key === 'mysql' + ? (service.specs[0] || '') + : (service.specs[1] || service.specs[0] || '') + deliveryForm.storageGi = service.key === 'mysql' + ? parseStorageGi(service.disks[0] || '20 GB') + : parseStorageGi(service.disks[1] || service.disks[0] || '50 GB') deliveryForm.port = service.defaultPort || undefined deliveryForm.charset = service.charsets[0] || '' deliveryForm.collation = collationMap[deliveryForm.charset]?.[0] || '' @@ -998,19 +1172,30 @@ function hydrateServiceDefaults() { } function resetWorkbench() { + clearWorkbenchSnapshot() hydrateServiceDefaults() activeView.value = 'config' ElMessage.success('交付工作台已重置') } +function clearWorkbenchSnapshot() { + try { + sessionStorage.removeItem(workbenchStorageKey()) + } catch { + // Session storage may be disabled. + } +} + function resetExecutionState() { stopPolling() + taskStateLoading.value = false precheckPassed.value = false running.value = false deliveryDone.value = false deliveryFailed.value = false credentialRevealed.value = false credentialDismissed.value = false + revealedCredentials.value = [] deliveryError.value = '' lastDeliveryStatus.value = '' canceling.value = false @@ -1042,8 +1227,13 @@ function defaultSteps(): DeliveryStep[] { return copy.map(([name, desc]) => ({ name, desc, state: 'pending' as StepState })) } -function precheck() { +async function precheck() { if (!validateDeliveryForm()) return + const businessLineId = businessLineStore.current?.id + if (!businessLineId) { + ElMessage.warning('请先选择业务线') + return + } if (!selectedTargetId.value) { ElMessage.warning('当前没有可用的 AWX 部署模板,请联系平台管理员配置') return @@ -1052,6 +1242,18 @@ function precheck() { ElMessage.warning('手动端口必须在 13306–13999 范围内') return } + try { + const conflict = await findInstanceNameConflict(businessLineId, normalizeDNSLabel(deliveryForm.instanceName)) + if (conflict) { + precheckPassed.value = false + ElMessage.error(conflict) + return + } + } catch (error) { + precheckPassed.value = false + ElMessage.error(error instanceof Error ? `实例名称可用性检查失败:${error.message}` : '实例名称可用性检查失败') + return + } precheckPassed.value = true deliveryFailed.value = false deliveryLog.value = [ @@ -1063,6 +1265,33 @@ function precheck() { ElMessage.success('预检查通过,可以创建交付任务') } +async function findInstanceNameConflict(businessLineId: number, instanceName: string) { + const occupiedStatuses = new Set([ + 'pending', + 'validating', + 'dispatching', + 'running', + 'registering', + 'finished', + 'register_failed', + 'canceling', + 'rollback_pending', + 'rolling_back', + 'rollback_failed', + ]) + const [tasks, services] = await Promise.all([ + deliveryApi.listTasks({ businessLineId, component: 'mysql' }), + deliveryApi.listMySQLServices(businessLineId), + ]) + if (tasks.some((task) => task.instance_name === instanceName && occupiedStatuses.has(task.status))) { + return `实例名称 ${instanceName} 已被运行中或尚未释放的交付任务占用` + } + if (services.some((service) => service.name === instanceName)) { + return `实例名称 ${instanceName} 已被现有 MySQL 实例占用` + } + return '' +} + async function createTask() { if (!precheckPassed.value) { ElMessage.warning('请先执行预检查') @@ -1091,18 +1320,20 @@ async function createTask() { try { const task = await deliveryApi.createMySQL(mysqlDeliveryPayload(businessLineId)) deploymentId.value = task.id - deliveryCredentials.set(task.id, deliveryForm.rootPassword) - deliveryCredentialStates.set(task.id, { revealed: false, dismissed: false }) - upsertDeliveryHistory(task.id, task.status, task.error_message || '', task.created_at, deliveryForm.rootPassword) + deliveryForm.rootPassword = '' + upsertDeliveryHistory(task.id, task.status, task.error_message || '', task.created_at) deliveryLog.value += `\n[task] ${task.id} created by ${currentName.value}` applyDeliveryStatus(task.status, '') startTaskPolling(task.id) + persistWorkbench() ElMessage.success('交付任务已创建') } catch (error) { running.value = false deliveryFailed.value = true + lastDeliveryStatus.value = 'execution_failed' + deliveryError.value = error instanceof Error ? error.message : '创建交付任务失败' activeView.value = 'result' - ElMessage.error(error instanceof Error ? error.message : '创建交付任务失败') + ElMessage.error(deliveryError.value) } } @@ -1191,6 +1422,7 @@ async function pollTask(id: string) { stopPolling() activeView.value = 'result' } + persistWorkbench() } catch (error) { appendLog(`[poll] ${error instanceof Error ? error.message : '获取任务状态失败'}`) } @@ -1276,9 +1508,8 @@ function setupCredentialPreview() { deliveredPort.value = 13306 credentialRevealed.value = false credentialDismissed.value = false - deliveryCredentials.set(deploymentId.value, deliveryForm.rootPassword) - deliveryCredentialStates.set(deploymentId.value, { revealed: false, dismissed: false }) - upsertDeliveryHistory(deploymentId.value, 'finished', '', new Date().toISOString(), deliveryForm.rootPassword) + revealedCredentials.value = [] + upsertDeliveryHistory(deploymentId.value, 'finished', '', new Date().toISOString()) if (deliveryHistory.value.length < 3) { addPreviewHistoryItems() } @@ -1317,10 +1548,29 @@ function normalizeDNSLabel(value: string) { } function generateRootPassword() { - const alphabet = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz23456789!@#$%^&*_-+=' - const values = new Uint32Array(24) - crypto.getRandomValues(values) - return Array.from(values, (value) => alphabet[value % alphabet.length]).join('') + const groups = [ + 'ABCDEFGHJKLMNPQRSTUVWXYZ', + 'abcdefghijkmnopqrstuvwxyz', + '23456789', + '!@#$%^&*_-+=', + ] + const alphabet = groups.join('') + const pick = (characters: string) => { + const value = new Uint32Array(1) + crypto.getRandomValues(value) + return characters[value[0] % characters.length] + } + const password = groups.map(pick) + while (password.length < 24) password.push(pick(alphabet)) + for (let index = password.length - 1; index > 0; index--) { + const value = new Uint32Array(1) + crypto.getRandomValues(value) + const swapIndex = value[0] % (index + 1) + const current = password[index] + password[index] = password[swapIndex] + password[swapIndex] = current + } + return password.join('') } function rootPasswordValidationError(value: string) { @@ -1339,33 +1589,55 @@ function regenerateRootPassword() { ElMessage.success('已生成新的 root 密码') } -function revealCredential() { +function regenerateInstanceName() { + const prefix = activeServiceKey.value === 'mysql' ? 'mysql' : 'service' + const businessLine = normalizeDNSLabel(currentName.value) + const alphabet = 'abcdefghijklmnopqrstuvwxyz0123456789' + const values = new Uint32Array(6) + crypto.getRandomValues(values) + const suffix = Array.from(values, (value) => alphabet[value % alphabet.length]).join('') + deliveryForm.instanceName = normalizeDNSLabel(`${prefix}-${businessLine}-${suffix}`) + precheckPassed.value = false + ElMessage.success('已生成新的实例名称,请执行预检查确认可用') +} + +async function revealCredential() { if (!credentialEligible.value) return - credentialRevealed.value = true - if (deploymentId.value) { - deliveryCredentialStates.set(deploymentId.value, { revealed: true, dismissed: false }) + credentialRevealing.value = true + try { + if (deploymentId.value === 'PREVIEW-CREDENTIAL') { + revealedCredentials.value = [{ username: 'root', host: 'localhost', password: deliveryForm.rootPassword }] + } else { + revealedCredentials.value = await deliveryApi.revealCredentials(deploymentId.value) + } + if (!revealedRootCredential.value) { + throw new Error('后端未返回 root 凭证') + } + credentialRevealed.value = true + } catch (error) { + credentialDismissed.value = true + ElMessage.error(error instanceof Error ? error.message : '一次性凭证领取失败') + } finally { + credentialRevealing.value = false } } function dismissCredential() { credentialRevealed.value = false credentialDismissed.value = true - if (deploymentId.value) { - deliveryCredentialStates.set(deploymentId.value, { revealed: false, dismissed: true }) - deliveryCredentials.delete(deploymentId.value) - markHistoryCredentialConsumed(deploymentId.value) - } + revealedCredentials.value = [] } function downloadCredentialFile() { - if (!credentialRevealed.value || !deliveryForm.rootPassword) return + const password = revealedRootCredential.value?.password + if (!credentialRevealed.value || !password) return const content = [ '# XInfra MySQL delivery credential', `instance=${deliveryForm.instanceName}`, `host=${deliveredHost.value}`, `port=${deliveredPort.value || ''}`, 'username=root', - `password=${deliveryForm.rootPassword}`, + `password=${password}`, ].join('\n') + '\n' const url = URL.createObjectURL(new Blob([content], { type: 'text/plain;charset=utf-8' })) const link = document.createElement('a') @@ -1380,7 +1652,14 @@ function loadDeliveryHistory(): DeliveryHistoryItem[] { try { const raw = localStorage.getItem('xinfra:mysql-delivery-history') const parsed = raw ? JSON.parse(raw) : [] - return Array.isArray(parsed) ? parsed.slice(0, 12) : [] + if (!Array.isArray(parsed)) return [] + const sanitized = parsed.slice(0, 12).map((item) => { + if (!item || typeof item !== 'object') return item + const { credential: _credential, credentialConsumed: _credentialConsumed, ...history } = item + return history + }) + localStorage.setItem('xinfra:mysql-delivery-history', JSON.stringify(sanitized)) + return sanitized } catch { return [] } @@ -1390,7 +1669,7 @@ function persistDeliveryHistory() { localStorage.setItem('xinfra:mysql-delivery-history', JSON.stringify(deliveryHistory.value.slice(0, 12))) } -function upsertDeliveryHistory(id: string, status: string, errorMessage = '', createdAt?: string, credential?: string) { +function upsertDeliveryHistory(id: string, status: string, errorMessage = '', createdAt?: string) { if (!id) return const existing = deliveryHistory.value.find((item) => item.id === id) const item: DeliveryHistoryItem = { @@ -1406,8 +1685,6 @@ function upsertDeliveryHistory(id: string, status: string, errorMessage = '', cr errorMessage, createdAt: createdAt || existing?.createdAt || new Date().toISOString(), updatedAt: new Date().toISOString(), - credential: credential ?? existing?.credential, - credentialConsumed: existing?.credentialConsumed ?? false, } deliveryHistory.value = [item, ...deliveryHistory.value.filter((entry) => entry.id !== id)] .sort((a, b) => Date.parse(b.updatedAt) - Date.parse(a.updatedAt)) @@ -1415,16 +1692,6 @@ function upsertDeliveryHistory(id: string, status: string, errorMessage = '', cr persistDeliveryHistory() } -function markHistoryCredentialConsumed(id: string) { - const item = deliveryHistory.value.find((entry) => entry.id === id) - if (!item) return - if (!item.credential && !deliveryCredentials.has(id)) return - item.credentialConsumed = true - item.credential = undefined - deliveryHistory.value = [...deliveryHistory.value] - persistDeliveryHistory() -} - function addPreviewHistoryItems() { const now = Date.now() const previewItems: DeliveryHistoryItem[] = [ @@ -1441,8 +1708,6 @@ function addPreviewHistoryItems() { errorMessage: '', createdAt: new Date(now - 18 * 60 * 1000).toISOString(), updatedAt: new Date(now - 12 * 60 * 1000).toISOString(), - credential: 'preview-r0ot-Password!2026', - credentialConsumed: false, }, { id: 'PREVIEW-FAILED', @@ -1457,18 +1722,11 @@ function addPreviewHistoryItems() { errorMessage: '目标主机资源预检查未通过', createdAt: new Date(now - 95 * 60 * 1000).toISOString(), updatedAt: new Date(now - 92 * 60 * 1000).toISOString(), - credentialConsumed: true, }, ] deliveryHistory.value = [...deliveryHistory.value, ...previewItems] .filter((item, index, items) => items.findIndex((entry) => entry.id === item.id) === index) .slice(0, 12) - previewItems.forEach((item) => { - if (item.credential && !item.credentialConsumed) { - deliveryCredentials.set(item.id, item.credential) - deliveryCredentialStates.set(item.id, { revealed: false, dismissed: false }) - } - }) persistDeliveryHistory() } @@ -1763,6 +2021,17 @@ h4 { background: var(--accent-dim); } +.flow-tab:disabled, +.flow-tab.disabled { + cursor: not-allowed; + opacity: 0.52; +} + +.flow-tab:disabled:hover { + border-color: var(--line); + background: var(--bg-panel-2); +} + .workbench-grid { display: grid; grid-template-columns: minmax(0, 1.18fr) minmax(360px, 0.82fr); @@ -1832,6 +2101,19 @@ h4 { font-size: 11.5px; } +.instance-name-editor { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + gap: 6px; + align-items: center; +} + +.random-name-button { + height: 32px; + padding: 0 8px; + white-space: nowrap; +} + .form-field :deep(.el-select), .form-field :deep(.el-input-number) { width: 100%; diff --git a/frontend/src/views/service/Management.vue b/frontend/src/views/service/Management.vue index dada1ae..bcdee5d 100644 --- a/frontend/src/views/service/Management.vue +++ b/frontend/src/views/service/Management.vue @@ -5,15 +5,8 @@

服务管理

同步基础服务实例台账,统一查看服务名 / 服务 IP / 业务标签 / 实例数

- 同步 + 同步
- - - - - - - - -