feat(security-center): add reset container operation alongside update
- system.ts: extract streamOperation helper; add streamSystemReset export - SecurityCenterView: replace single update button with update/reset descriptions table; shared dialog driven by operationType ref Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
这个提交包含在:
父节点
d0b7a51c2c
当前提交
e965c012e6
@ -12,12 +12,13 @@ export async function getDeploymentStatus(): Promise<DeploymentStatus> {
|
||||
return json.data as DeploymentStatus
|
||||
}
|
||||
|
||||
export async function streamSystemUpdate(
|
||||
async function streamOperation(
|
||||
path: string,
|
||||
onLine: (line: string) => void,
|
||||
signal?: AbortSignal,
|
||||
): Promise<void> {
|
||||
const token = localStorage.getItem('token') ?? ''
|
||||
const res = await fetch(`${BASE}/system/update`, {
|
||||
const res = await fetch(`${BASE}${path}`, {
|
||||
method: 'POST',
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
signal,
|
||||
@ -41,3 +42,11 @@ export async function streamSystemUpdate(
|
||||
}
|
||||
if (buf) onLine(buf)
|
||||
}
|
||||
|
||||
export function streamSystemUpdate(onLine: (line: string) => void, signal?: AbortSignal) {
|
||||
return streamOperation('/system/update', onLine, signal)
|
||||
}
|
||||
|
||||
export function streamSystemReset(onLine: (line: string) => void, signal?: AbortSignal) {
|
||||
return streamOperation('/system/reset', onLine, signal)
|
||||
}
|
||||
|
||||
@ -47,18 +47,24 @@
|
||||
<el-button type="warning" plain @click="openMigrateDialog">生成迁移密钥</el-button>
|
||||
</el-card>
|
||||
|
||||
<!-- 一键更新:仅私有化平台显示 -->
|
||||
<!-- 系统运维:仅私有化平台显示 -->
|
||||
<el-card v-if="deploymentMode === 'PRIVATE'" style="margin-bottom: 16px">
|
||||
<template #header>
|
||||
<div style="display:flex;align-items:center;justify-content:space-between">
|
||||
<span>系统更新</span>
|
||||
<span>系统运维</span>
|
||||
<el-tag type="info" size="small">私有化部署</el-tag>
|
||||
</div>
|
||||
</template>
|
||||
<p style="color: #606266; margin: 0 0 16px;">
|
||||
拉取最新镜像并逐一重启各服务容器,全程无需手动操作。tenant-service 重启时页面连接会短暂中断,更新后自动恢复。
|
||||
</p>
|
||||
<el-button type="primary" @click="showUpdateDialog = true">立即更新</el-button>
|
||||
<el-descriptions :column="1" border>
|
||||
<el-descriptions-item label="一键更新">
|
||||
<span style="color:#606266;font-size:13px;margin-right:16px">拉取最新镜像并重建所有容器,用于升级到新版本。</span>
|
||||
<el-button type="primary" size="small" @click="openOperationDialog('update')">立即更新</el-button>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="重置容器">
|
||||
<span style="color:#606266;font-size:13px;margin-right:16px">用当前本地镜像重建所有容器,无需下载新镜像,适合修复异常服务。</span>
|
||||
<el-button type="warning" size="small" @click="openOperationDialog('reset')">重置容器</el-button>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-card>
|
||||
|
||||
<el-card>
|
||||
@ -158,20 +164,21 @@
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 一键更新 dialog -->
|
||||
<!-- 一键更新 / 重置容器 dialog -->
|
||||
<el-dialog
|
||||
v-model="showUpdateDialog"
|
||||
title="一键更新"
|
||||
:title="operationType === 'update' ? '一键更新' : '重置容器'"
|
||||
width="600px"
|
||||
:close-on-click-modal="!updating"
|
||||
:close-on-press-escape="!updating"
|
||||
@closed="resetUpdateDialog"
|
||||
>
|
||||
<div v-if="!updating && !updateDone && !updateError" style="color:#606266;">
|
||||
<p>将拉取最新镜像并重启所有运行中的服务容器。</p>
|
||||
<p v-if="operationType === 'update'">将拉取最新镜像并重建所有运行中的服务容器,用于升级到新版本。</p>
|
||||
<p v-else>将用当前本地镜像重建所有运行中的服务容器,无需下载新镜像,适合修复异常服务。</p>
|
||||
<el-alert type="warning" :closable="false" show-icon style="margin-top:12px">
|
||||
<template #title>tenant-service 重启时页面连接会短暂中断(约 10–30 秒)</template>
|
||||
<template #default>更新完成后请刷新页面。</template>
|
||||
<template #default>操作完成后请刷新页面。</template>
|
||||
</el-alert>
|
||||
</div>
|
||||
|
||||
@ -185,7 +192,7 @@
|
||||
</div>
|
||||
|
||||
<div v-if="updateDone" style="margin-top:12px">
|
||||
<el-alert type="success" :closable="false" show-icon title="更新完成!" />
|
||||
<el-alert type="success" :closable="false" show-icon :title="operationType === 'update' ? '更新完成!' : '重置完成!'" />
|
||||
</div>
|
||||
<div v-if="updateError" style="margin-top:12px">
|
||||
<el-alert type="error" :closable="false" show-icon :title="updateError" />
|
||||
@ -197,10 +204,10 @@
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="!updating && !updateDone && !selfRestarting"
|
||||
type="primary"
|
||||
@click="startUpdate"
|
||||
:type="operationType === 'update' ? 'primary' : 'warning'"
|
||||
@click="startOperation"
|
||||
>
|
||||
开始更新
|
||||
{{ operationType === 'update' ? '开始更新' : '开始重置' }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
@ -214,7 +221,7 @@ import { Loading } from '@element-plus/icons-vue'
|
||||
import { accountApi } from '@/api/account'
|
||||
import { appApi, type App } from '@/api/app'
|
||||
import { migrateApi } from '@/api/migrate'
|
||||
import { getDeploymentStatus, streamSystemUpdate } from '@/api/system'
|
||||
import { getDeploymentStatus, streamSystemUpdate, streamSystemReset } from '@/api/system'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { formatTime } from '@/utils/date'
|
||||
|
||||
@ -349,6 +356,7 @@ async function copyMigrateKey() {
|
||||
// ── System Update ──────────────────────────────────────────────────────────
|
||||
|
||||
const showUpdateDialog = ref(false)
|
||||
const operationType = ref<'update' | 'reset'>('update')
|
||||
const updating = ref(false)
|
||||
const updateDone = ref(false)
|
||||
const updateError = ref('')
|
||||
@ -356,6 +364,11 @@ const selfRestarting = ref(false)
|
||||
const updateLog = ref<string[]>([])
|
||||
const logEl = ref<HTMLPreElement | null>(null)
|
||||
|
||||
function openOperationDialog(type: 'update' | 'reset') {
|
||||
operationType.value = type
|
||||
showUpdateDialog.value = true
|
||||
}
|
||||
|
||||
function resetUpdateDialog() {
|
||||
if (updating.value) return
|
||||
updateLog.value = []
|
||||
@ -364,15 +377,18 @@ function resetUpdateDialog() {
|
||||
selfRestarting.value = false
|
||||
}
|
||||
|
||||
async function startUpdate() {
|
||||
async function startOperation() {
|
||||
updating.value = true
|
||||
updateLog.value = []
|
||||
updateDone.value = false
|
||||
updateError.value = ''
|
||||
selfRestarting.value = false
|
||||
|
||||
const streamFn = operationType.value === 'update' ? streamSystemUpdate : streamSystemReset
|
||||
const failMsg = operationType.value === 'update' ? '更新失败' : '重置失败'
|
||||
|
||||
try {
|
||||
await streamSystemUpdate((line) => {
|
||||
await streamFn((line) => {
|
||||
if (line === 'RESTART_SELF') {
|
||||
selfRestarting.value = true
|
||||
updating.value = false
|
||||
@ -391,7 +407,7 @@ async function startUpdate() {
|
||||
} catch (e: any) {
|
||||
if (!selfRestarting.value) {
|
||||
updating.value = false
|
||||
updateError.value = e?.message ?? '更新失败'
|
||||
updateError.value = e?.message ?? failMsg
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -404,7 +420,7 @@ async function pollForRecovery() {
|
||||
await getDeploymentStatus()
|
||||
selfRestarting.value = false
|
||||
updateDone.value = true
|
||||
updateLog.value.push('>>> tenant-service 已重启,更新完成 ✓')
|
||||
updateLog.value.push(operationType.value === 'update' ? '>>> tenant-service 已重启,更新完成 ✓' : '>>> tenant-service 已重启,重置完成 ✓')
|
||||
return
|
||||
} catch {
|
||||
// still restarting
|
||||
|
||||
正在加载...
在新工单中引用
屏蔽一个用户