-
{{ 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 / 业务标签 / 实例数