modify 修复前端展示配置与实际插件配置不一致的bug
This commit is contained in:
+58
-4
@@ -22,6 +22,12 @@ export interface AdminCmdReply {
|
||||
exit_request?: string
|
||||
}
|
||||
|
||||
/** 插件配置优先来源模式:toml=以 config.toml 为准(启动时覆盖缓存);cache=以缓存为准 */
|
||||
export type ConfigMode = 'toml' | 'cache'
|
||||
|
||||
/** 展示值的实际来源:cache=运行时缓存;toml=缓存缺失或副本未响应,回退读配置文件 */
|
||||
export type ConfigSource = 'cache' | 'toml'
|
||||
|
||||
export interface RestartAllResult {
|
||||
worker_id: string
|
||||
ok: boolean
|
||||
@@ -125,10 +131,16 @@ export function parseJwt(token: string): AuthUser | null {
|
||||
|
||||
/** 读取某插件的现有运行时配置([workers.config]) */
|
||||
export function getWorkerConfig(token: string, workerType: string) {
|
||||
return request<{ success: boolean; worker_type: string; config: Record<string, unknown> }>(
|
||||
`/api/v1/admin/workers/${encodeURIComponent(workerType)}/config`,
|
||||
token,
|
||||
)
|
||||
return request<{
|
||||
success: boolean
|
||||
worker_type: string
|
||||
worker_id?: string
|
||||
/** 展示值实际来自哪一侧(后端如实回报,勿混用缓存/文件值) */
|
||||
source?: ConfigSource
|
||||
/** 该类插件的配置优先来源模式 */
|
||||
mode?: ConfigMode
|
||||
config: Record<string, unknown>
|
||||
}>(`/api/v1/admin/workers/${encodeURIComponent(workerType)}/config`, token)
|
||||
}
|
||||
|
||||
/** 更新某插件的运行时配置([workers.config],路由配置除外),由 worker 写回并重启 */
|
||||
@@ -160,4 +172,46 @@ export function scaleWorker(token: string, workerType: string, count: number) {
|
||||
)
|
||||
}
|
||||
|
||||
export interface WorkerInstanceGroup {
|
||||
worker_type: string
|
||||
instance_id: string
|
||||
mode: 'inline' | 'external'
|
||||
replicas: number
|
||||
worker_ids: string[]
|
||||
}
|
||||
|
||||
export interface ScaleInstanceResult {
|
||||
success: boolean
|
||||
result: {
|
||||
worker_type: string
|
||||
instance_id: string
|
||||
target: number
|
||||
mode: 'inline' | 'external'
|
||||
sent_to?: string
|
||||
reply?: AdminCmdReply & { persist?: string }
|
||||
}
|
||||
}
|
||||
|
||||
/** 按「外部进程实例」分组列出某类型的 worker(精细扩缩寻址) */
|
||||
export function listWorkerInstances(token: string, workerType: string) {
|
||||
return request<{ success: boolean; result: WorkerInstanceGroup[] }>(
|
||||
`/api/v1/admin/workers/${encodeURIComponent(workerType)}/instances`,
|
||||
token,
|
||||
)
|
||||
}
|
||||
|
||||
/** 把某实例精确扩缩到指定副本数(绝对数,不影响其它实例) */
|
||||
export function scaleWorkerInstance(
|
||||
token: string,
|
||||
workerType: string,
|
||||
instanceId: string,
|
||||
count: number,
|
||||
) {
|
||||
return request<ScaleInstanceResult>(
|
||||
`/api/v1/admin/workers/${encodeURIComponent(workerType)}/scale_instance`,
|
||||
token,
|
||||
{ method: 'POST', body: JSON.stringify({ instance_id: instanceId, count }) },
|
||||
)
|
||||
}
|
||||
|
||||
export const TOKEN_KEY = 'cm_admin_token'
|
||||
+355
-16
@@ -65,6 +65,34 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="filter-bar">
|
||||
<el-input
|
||||
v-model="searchText"
|
||||
clearable
|
||||
:prefix-icon="Search"
|
||||
placeholder="搜索:插件类型 / 副本ID / 实例ID"
|
||||
style="width: 280px"
|
||||
/>
|
||||
<el-select v-model="modeFilter" style="width: 150px; margin-left: 12px">
|
||||
<el-option label="全部模式" value="all" />
|
||||
<el-option label="inline" value="inline" />
|
||||
<el-option label="external" value="external" />
|
||||
</el-select>
|
||||
<div class="replica-filter" style="margin-left: 12px; display: flex; align-items: center">
|
||||
<span class="scale-tip" style="margin-right: 6px">副本数 ≥</span>
|
||||
<el-input-number
|
||||
v-model="minReplicas"
|
||||
:min="0"
|
||||
:max="50"
|
||||
controls-position="right"
|
||||
style="width: 130px"
|
||||
/>
|
||||
</div>
|
||||
<span class="scale-tip" style="margin-left: 12px"
|
||||
>筛选同时作用于下方「逐实例精细扩缩」分组表(副本数仅对实例分组表生效)</span
|
||||
>
|
||||
</div>
|
||||
|
||||
<el-table
|
||||
:data="pagedWorkers"
|
||||
v-loading="loading"
|
||||
@@ -166,9 +194,52 @@
|
||||
调整数量
|
||||
</el-button>
|
||||
<span class="scale-tip"
|
||||
>调小会<strong>优雅排空</strong>:先隔离新请求,等当前请求处理完再关闭对应副本;数量将写入配置,重启网关后保持。</span
|
||||
>调小会<strong>优雅排空</strong>:先隔离新请求,等当前请求处理完再关闭对应副本;数量写入运行时缓存——cache
|
||||
模式下重启网关后保持,TOML 模式下重启会被 config.toml 的 count 覆盖。</span
|
||||
>
|
||||
</div>
|
||||
<el-divider style="margin: 14px 0">逐实例精细扩缩</el-divider>
|
||||
<div v-loading="instancesLoading">
|
||||
<div class="scale-tip" style="margin-bottom: 10px">
|
||||
把某个<strong>外部进程/实例</strong>精确调整到指定副本数(绝对数,不影响其它实例);「调整数量」全局缩放会清除这些定制、回到均匀分摊。
|
||||
</div>
|
||||
<el-table :data="filteredInstances" size="small" style="width: 100%">
|
||||
<el-table-column prop="instance_id" label="实例 (instance_id)" show-overflow-tooltip />
|
||||
<el-table-column prop="mode" label="模式" width="90">
|
||||
<template #default="{ row }">
|
||||
<el-tag size="small" :type="row.mode === 'inline' ? 'success' : 'warning'">
|
||||
{{ row.mode }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="replicas" label="当前副本数" width="100" />
|
||||
<el-table-column label="目标副本数" width="150">
|
||||
<template #default="{ row }">
|
||||
<el-input-number
|
||||
v-model="row._target"
|
||||
:min="0"
|
||||
:max="50"
|
||||
size="small"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-button
|
||||
type="primary"
|
||||
size="small"
|
||||
link
|
||||
:loading="scalingInstanceId === row.instance_id"
|
||||
:disabled="!row.instance_id"
|
||||
@click="applyInstanceScale(row as any)"
|
||||
>
|
||||
应用
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<template #empty>该类型暂无可用实例</template>
|
||||
</el-table>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
@@ -182,7 +253,7 @@
|
||||
type="info"
|
||||
:closable="false"
|
||||
show-icon
|
||||
title="写入插件 config.toml 的 [workers.config](路由配置除外)。由 worker 写回并重启所有对应副本。"
|
||||
:title="configAlertTitle"
|
||||
style="margin-bottom: 14px"
|
||||
/>
|
||||
<el-form label-position="top">
|
||||
@@ -190,6 +261,17 @@
|
||||
<el-tag effect="plain">{{ workerType || '-' }}</el-tag>
|
||||
</el-form-item>
|
||||
<el-form-item label="现有配置值">
|
||||
<div v-if="configMode || configSource" class="config-source">
|
||||
<el-tag size="small" :type="configMode === 'cache' ? 'success' : 'info'">
|
||||
模式:{{ configMode === 'cache' ? '缓存为准' : 'TOML 为准' }}
|
||||
</el-tag>
|
||||
<el-tag size="small" :type="configSource === 'cache' ? 'info' : 'warning'">
|
||||
来源:{{ configSource === 'cache' ? '运行时缓存' : 'config.toml(回退读取)' }}
|
||||
</el-tag>
|
||||
<span v-if="configSource === 'toml'" class="config-source-tip">
|
||||
副本未响应或缓存中无该键,以上取自配置文件,可能不是实际生效值
|
||||
</span>
|
||||
</div>
|
||||
<el-table
|
||||
:data="currentConfig"
|
||||
v-loading="configLoading"
|
||||
@@ -219,16 +301,38 @@
|
||||
<el-select
|
||||
v-model="configValueType"
|
||||
style="width: 120px; margin-right: 8px"
|
||||
@change="onConfigValueTypeChange"
|
||||
>
|
||||
<el-option label="字符串" value="string" />
|
||||
<el-option label="数字" value="number" />
|
||||
<el-option label="布尔" value="boolean" />
|
||||
<el-option label="对象 (JSON)" value="object" />
|
||||
<el-option label="数组 (JSON)" value="array" />
|
||||
</el-select>
|
||||
<el-input
|
||||
v-if="!isJsonType"
|
||||
v-model="configValue"
|
||||
:style="{ width: 'calc(100% - 128px)' }"
|
||||
placeholder="配置值"
|
||||
/>
|
||||
<div v-else class="json-editor">
|
||||
<el-input
|
||||
v-model="configValue"
|
||||
type="textarea"
|
||||
:rows="6"
|
||||
placeholder="请输入 JSON 文本(如对象 { } 或数组 [ ])"
|
||||
@input="validateConfigJson"
|
||||
/>
|
||||
<div class="json-editor-bar">
|
||||
<el-button size="small" text type="primary" @click="formatConfigJson">
|
||||
格式化
|
||||
</el-button>
|
||||
<span v-if="configJsonInvalid" class="json-err">
|
||||
JSON 不合法:{{ configJsonError }}
|
||||
</span>
|
||||
<span v-else-if="configValue" class="json-ok">JSON 合法</span>
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
@@ -249,7 +353,7 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Monitor } from '@element-plus/icons-vue'
|
||||
import { Monitor, Search } from '@element-plus/icons-vue'
|
||||
import {
|
||||
getWorkers,
|
||||
restartWorker,
|
||||
@@ -257,11 +361,16 @@ import {
|
||||
getWorkerConfig,
|
||||
updateWorkerConfig,
|
||||
scaleWorker,
|
||||
listWorkerInstances,
|
||||
scaleWorkerInstance,
|
||||
login,
|
||||
parseJwt,
|
||||
TOKEN_KEY,
|
||||
type WorkerInstance,
|
||||
type WorkerInstanceGroup,
|
||||
type AuthUser,
|
||||
type ConfigMode,
|
||||
type ConfigSource,
|
||||
} from '@/api/workerAdmin'
|
||||
|
||||
const token = ref(localStorage.getItem(TOKEN_KEY) || '')
|
||||
@@ -279,24 +388,74 @@ const workerType = ref('')
|
||||
// 分页
|
||||
const currentPage = ref(1)
|
||||
const pageSize = ref(10)
|
||||
const total = computed(() => workers.value.length)
|
||||
|
||||
// 查询与筛选(作用于总副本列表与逐实例分组表两份数据)
|
||||
const searchText = ref('')
|
||||
const modeFilter = ref<'all' | 'inline' | 'external'>('all')
|
||||
const minReplicas = ref(0)
|
||||
|
||||
const filteredWorkers = computed(() => {
|
||||
const kw = searchText.value.trim().toLowerCase()
|
||||
return workers.value.filter((w) => {
|
||||
if (modeFilter.value !== 'all' && w.mode !== modeFilter.value) return false
|
||||
if (!kw) return true
|
||||
return (
|
||||
w.worker_type.toLowerCase().includes(kw) ||
|
||||
w.worker_id.toLowerCase().includes(kw)
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
const filteredInstances = computed(() => {
|
||||
const kw = searchText.value.trim().toLowerCase()
|
||||
const minR = Math.max(0, Math.floor(minReplicas.value || 0))
|
||||
return instances.value.filter((g) => {
|
||||
if (modeFilter.value !== 'all' && g.mode !== modeFilter.value) return false
|
||||
if (g.replicas < minR) return false
|
||||
if (!kw) return true
|
||||
return g.instance_id.toLowerCase().includes(kw)
|
||||
})
|
||||
})
|
||||
|
||||
const total = computed(() => filteredWorkers.value.length)
|
||||
const pagedWorkers = computed(() => {
|
||||
const start = (currentPage.value - 1) * pageSize.value
|
||||
return workers.value.slice(start, start + pageSize.value)
|
||||
return filteredWorkers.value.slice(start, start + pageSize.value)
|
||||
})
|
||||
// 数据刷新/变化时回到第一页
|
||||
// 数据刷新/筛选条件变化时回到第一页
|
||||
watch(workers, () => {
|
||||
currentPage.value = 1
|
||||
})
|
||||
watch([searchText, modeFilter, minReplicas], () => {
|
||||
currentPage.value = 1
|
||||
})
|
||||
|
||||
// 配置编辑
|
||||
type ConfigValueType = 'string' | 'number' | 'boolean' | 'object' | 'array'
|
||||
const configDialogVisible = ref(false)
|
||||
const configKey = ref('')
|
||||
const configValue = ref('')
|
||||
const configValueType = ref<'string' | 'number' | 'boolean'>('string')
|
||||
const configValueType = ref<ConfigValueType>('string')
|
||||
const configUpdating = ref(false)
|
||||
const configLoading = ref(false)
|
||||
const currentConfig = ref<Array<{ key: string; value: string; type: 'string' | 'number' | 'boolean' }>>([])
|
||||
const configJsonInvalid = ref(false)
|
||||
const configJsonError = ref('')
|
||||
const isJsonType = computed(
|
||||
() => configValueType.value === 'object' || configValueType.value === 'array',
|
||||
)
|
||||
const currentConfig = ref<Array<{ key: string; value: string; type: ConfigValueType }>>([])
|
||||
// 后端如实回报:mode=该类插件的配置优先来源模式,source=本次展示值实际来自哪一侧
|
||||
const configMode = ref<ConfigMode | ''>('')
|
||||
const configSource = ref<ConfigSource | ''>('')
|
||||
const configAlertTitle = computed(() => {
|
||||
if (configMode.value === 'cache') {
|
||||
return '写入插件运行时配置 [workers.config](路由配置除外),以运行时缓存为准:下发并重启对应副本,网关重启后保持。'
|
||||
}
|
||||
if (configMode.value === 'toml') {
|
||||
return '写入插件运行时配置 [workers.config](路由配置除外)。当前 TOML 模式(以 config.toml 为准):改动仅本次运行有效,网关重启后会被 config.toml 覆盖。'
|
||||
}
|
||||
return '写入插件运行时配置 [workers.config](路由配置除外),下发并重启所有对应副本。'
|
||||
})
|
||||
|
||||
const doLogin = async () => {
|
||||
const name = username.value.trim()
|
||||
@@ -346,13 +505,15 @@ const logout = () => {
|
||||
ElMessage.success('已退出登录')
|
||||
}
|
||||
|
||||
// 自动轮询:让扩缩容/停止/重启等操作的结果(副本关闭、模式变化)无需手动刷新即反映到列表
|
||||
// 自动轮询:让扩缩容/停止/重启/逐实例精细扩缩等操作的结果(副本关闭、模式、实例分组数)
|
||||
// 无需手动刷新即反映到列表。逐实例分组与总列表一并刷新,避免扩缩后实例表停留旧值。
|
||||
let pollTimer: ReturnType<typeof setInterval> | undefined
|
||||
const startPolling = () => {
|
||||
stopPolling()
|
||||
if (!token.value.trim()) return
|
||||
pollTimer = setInterval(() => {
|
||||
if (!loading.value) fetchWorkers()
|
||||
if (!instancesLoading.value) fetchInstances()
|
||||
}, 10000)
|
||||
}
|
||||
const stopPolling = () => {
|
||||
@@ -394,8 +555,68 @@ const scaleTarget = ref(0)
|
||||
const scalingType = ref('')
|
||||
watch(workerType, (t) => {
|
||||
scaleTarget.value = t ? countByType(t) : 0
|
||||
fetchInstances()
|
||||
})
|
||||
|
||||
// 逐实例精细扩缩
|
||||
type InstanceGroupWithTarget = WorkerInstanceGroup & { _target: number }
|
||||
const instances = ref<InstanceGroupWithTarget[]>([])
|
||||
const instancesLoading = ref(false)
|
||||
const scalingInstanceId = ref('')
|
||||
const fetchInstances = async () => {
|
||||
const t = workerType.value
|
||||
const tok = token.value.trim()
|
||||
if (!t || !tok) {
|
||||
instances.value = []
|
||||
return
|
||||
}
|
||||
instancesLoading.value = true
|
||||
try {
|
||||
const data = await listWorkerInstances(tok, t)
|
||||
instances.value = (data.result || []).map((g) => ({ ...g, _target: g.replicas }))
|
||||
} catch (e: any) {
|
||||
// 逐实例扩缩为增强能力:旧版运行时/网关不支持时静默降级,不影响主流程
|
||||
instances.value = []
|
||||
console.warn('获取实例分组失败', e)
|
||||
} finally {
|
||||
instancesLoading.value = false
|
||||
}
|
||||
}
|
||||
const applyInstanceScale = async (g: InstanceGroupWithTarget) => {
|
||||
const t = workerType.value
|
||||
if (!t) return
|
||||
const target = Math.floor(g._target)
|
||||
if (Number.isNaN(target) || target < 0) return
|
||||
if (target === g.replicas) {
|
||||
ElMessage.info('目标数量与当前相同,未调整')
|
||||
return
|
||||
}
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
target > g.replicas
|
||||
? `扩缩实例「${g.instance_id}」到 ${target} 个副本(新增 ${target - g.replicas} 个)?仅影响该实例,不影响其它实例。`
|
||||
: `缩容实例「${g.instance_id}」到 ${target} 个副本(关闭 ${g.replicas - target} 个)?将优雅排空并只影响该实例。`,
|
||||
'逐实例精细扩缩',
|
||||
{ type: 'warning', confirmButtonText: '确定', cancelButtonText: '取消' },
|
||||
)
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
scalingInstanceId.value = g.instance_id
|
||||
try {
|
||||
const res = await scaleWorkerInstance(token.value.trim(), t, g.instance_id, target)
|
||||
ElMessage.success(`已把实例「${g.instance_id}」调整至 ${target}(${res.result.mode})`)
|
||||
setTimeout(() => {
|
||||
fetchWorkers()
|
||||
fetchInstances()
|
||||
}, target > g.replicas ? 1200 : 2600)
|
||||
} catch (e: any) {
|
||||
ElMessage.error(`逐实例扩缩失败:${e.message}`)
|
||||
} finally {
|
||||
scalingInstanceId.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
const applyScale = async () => {
|
||||
const t = workerType.value
|
||||
if (!t) return
|
||||
@@ -422,7 +643,10 @@ const applyScale = async () => {
|
||||
const res = await scaleWorker(token.value.trim(), t, target)
|
||||
ElMessage.success(`已调整插件「${t}」副本数至 ${target}(${res.result.mode})`)
|
||||
scaleTarget.value = target
|
||||
setTimeout(fetchWorkers, target > cur ? 1200 : 2600)
|
||||
setTimeout(() => {
|
||||
fetchWorkers()
|
||||
fetchInstances()
|
||||
}, target > cur ? 1200 : 2600)
|
||||
} catch (e: any) {
|
||||
ElMessage.error(`调整副本数量失败:${e.message}`)
|
||||
} finally {
|
||||
@@ -508,18 +732,73 @@ const onSelectionChange = (rows: WorkerInstance[]) => {
|
||||
selected.value = rows
|
||||
}
|
||||
|
||||
const valueToDisplay = (v: unknown): { value: string; type: 'string' | 'number' | 'boolean' } => {
|
||||
const valueToDisplay = (v: unknown): { value: string; type: ConfigValueType } => {
|
||||
if (typeof v === 'boolean') return { value: String(v), type: 'boolean' }
|
||||
if (typeof v === 'number') return { value: String(v), type: 'number' }
|
||||
if (typeof v === 'object' && v !== null) return { value: JSON.stringify(v), type: 'string' }
|
||||
if (Array.isArray(v)) return { value: JSON.stringify(v, null, 2), type: 'array' }
|
||||
if (typeof v === 'object' && v !== null)
|
||||
return { value: JSON.stringify(v, null, 2), type: 'object' }
|
||||
return { value: String(v ?? ''), type: 'string' }
|
||||
}
|
||||
|
||||
// 对象/数组等嵌套配置以 JSON 文本编辑:实时校验 + 一键格式化,避免手写不可靠的 JSON 字符串
|
||||
const validateConfigJson = () => {
|
||||
if (!isJsonType.value) {
|
||||
configJsonInvalid.value = false
|
||||
configJsonError.value = ''
|
||||
return
|
||||
}
|
||||
const text = configValue.value.trim()
|
||||
if (!text) {
|
||||
configJsonInvalid.value = true
|
||||
configJsonError.value = '内容不能为空'
|
||||
return
|
||||
}
|
||||
try {
|
||||
const parsed: unknown = JSON.parse(text)
|
||||
if (configValueType.value === 'array' && !Array.isArray(parsed)) {
|
||||
configJsonInvalid.value = true
|
||||
configJsonError.value = '应为数组 ([])'
|
||||
return
|
||||
}
|
||||
if (
|
||||
configValueType.value === 'object' &&
|
||||
(typeof parsed !== 'object' || parsed === null || Array.isArray(parsed))
|
||||
) {
|
||||
configJsonInvalid.value = true
|
||||
configJsonError.value = '应为对象 ({})'
|
||||
return
|
||||
}
|
||||
configJsonInvalid.value = false
|
||||
configJsonError.value = ''
|
||||
} catch (e: any) {
|
||||
configJsonInvalid.value = true
|
||||
configJsonError.value = e?.message || '解析失败'
|
||||
}
|
||||
}
|
||||
|
||||
const formatConfigJson = () => {
|
||||
if (!isJsonType.value) return
|
||||
try {
|
||||
configValue.value = JSON.stringify(JSON.parse(configValue.value), null, 2)
|
||||
validateConfigJson()
|
||||
} catch {
|
||||
validateConfigJson()
|
||||
}
|
||||
}
|
||||
|
||||
const onConfigValueTypeChange = () => {
|
||||
// 切到对象/数组类型时,若当前文本是合法 JSON 则重新美化排版,便于编辑嵌套结构
|
||||
formatConfigJson()
|
||||
}
|
||||
|
||||
const openConfigDialog = async () => {
|
||||
configKey.value = ''
|
||||
configValue.value = ''
|
||||
configValueType.value = 'string'
|
||||
currentConfig.value = []
|
||||
configMode.value = ''
|
||||
configSource.value = ''
|
||||
configDialogVisible.value = true
|
||||
// 展示现有值:读取 [workers.config]
|
||||
const t = token.value.trim()
|
||||
@@ -528,6 +807,8 @@ const openConfigDialog = async () => {
|
||||
configLoading.value = true
|
||||
try {
|
||||
const data = await getWorkerConfig(t, type)
|
||||
configMode.value = data.mode || ''
|
||||
configSource.value = data.source || ''
|
||||
currentConfig.value = Object.entries(data.config || {}).map(([k, v]) => ({
|
||||
key: k,
|
||||
...valueToDisplay(v),
|
||||
@@ -540,10 +821,11 @@ const openConfigDialog = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
const selectConfigEntry = (row: { key: string; value: string; type: 'string' | 'number' | 'boolean' }) => {
|
||||
const selectConfigEntry = (row: { key: string; value: string; type: ConfigValueType }) => {
|
||||
configKey.value = row.key
|
||||
configValue.value = row.value
|
||||
configValueType.value = row.type
|
||||
validateConfigJson()
|
||||
}
|
||||
|
||||
const submitConfig = async () => {
|
||||
@@ -559,20 +841,36 @@ const submitConfig = async () => {
|
||||
return
|
||||
}
|
||||
// 按所选类型转换值
|
||||
let value: string | number | boolean = configValue.value
|
||||
let value: unknown = configValue.value
|
||||
if (configValueType.value === 'number') {
|
||||
if (configValue.value === '' || Number.isNaN(Number(configValue.value))) {
|
||||
if (configValue.value.trim() === '' || Number.isNaN(Number(configValue.value))) {
|
||||
ElMessage.warning('数字类型配置值不合法')
|
||||
return
|
||||
}
|
||||
value = Number(configValue.value)
|
||||
} else if (configValueType.value === 'boolean') {
|
||||
value = String(configValue.value).toLowerCase() === 'true'
|
||||
} else if (isJsonType.value) {
|
||||
// 对象/数组:以真正的 JSON 结构下发,而不是字符串
|
||||
validateConfigJson()
|
||||
if (configJsonInvalid.value) {
|
||||
ElMessage.warning(`JSON 不合法:${configJsonError.value}`)
|
||||
return
|
||||
}
|
||||
try {
|
||||
value = JSON.parse(configValue.value)
|
||||
} catch {
|
||||
ElMessage.warning('JSON 解析失败,请检查格式')
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
const displayValue =
|
||||
typeof value === 'object' && value !== null ? JSON.stringify(value) : String(value)
|
||||
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定将「${type}」的配置 ${key}=${String(value)} 下发并重启所有对应副本吗?`,
|
||||
`确定将「${type}」的配置 ${key}=${displayValue.length > 120 ? displayValue.slice(0, 120) + '…' : displayValue} 下发并重启所有对应副本吗?`,
|
||||
'配置更新确认',
|
||||
{ type: 'warning', confirmButtonText: '下发并重启', cancelButtonText: '取消' },
|
||||
)
|
||||
@@ -644,15 +942,56 @@ onUnmounted(() => stopPolling())
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
}
|
||||
.filter-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 14px;
|
||||
gap: 4px;
|
||||
}
|
||||
.scale-tip {
|
||||
margin-left: 12px;
|
||||
color: var(--cm-text-muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
.config-source {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
width: 100%;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.config-source-tip {
|
||||
color: var(--el-color-warning);
|
||||
font-size: 12px;
|
||||
}
|
||||
:deep(.el-table) {
|
||||
--el-table-header-bg-color: var(--cm-bg-elevated);
|
||||
--el-table-tr-bg-color: var(--cm-bg-soft);
|
||||
--el-table-row-hover-bg-color: rgba(79, 140, 255, 0.06);
|
||||
background: transparent;
|
||||
}
|
||||
.json-editor {
|
||||
width: calc(100% - 128px);
|
||||
}
|
||||
.json-editor :deep(.el-textarea__inner) {
|
||||
font-family: ui-monospace, 'SF Mono', Consolas, Menlo, monospace;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
.json-editor-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-top: 6px;
|
||||
}
|
||||
.json-err {
|
||||
color: var(--el-color-danger);
|
||||
font-size: 12px;
|
||||
}
|
||||
.json-ok {
|
||||
color: var(--el-color-success);
|
||||
font-size: 12px;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user