合并配置标题状态并在可视化表单改造前建立检查点
Some checks are pending
Security Checks / security (push) Waiting to run

This commit is contained in:
smail 2026-09-05 22:53:00 +08:00
parent be51853815
commit fd67cca2d4
4 changed files with 229 additions and 108 deletions

View File

@ -0,0 +1,139 @@
.config-overview {
min-width: 0;
padding-bottom: 18px;
border-bottom: 1px solid var(--pm-border-color);
}
.overview-heading {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 16px;
}
.overview-copy {
flex: 1;
min-width: 0;
}
.overview-title {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 8px 10px;
}
.overview-title h3 {
margin: 0;
color: var(--pm-text-primary);
font-size: 20px;
line-height: 1.4;
}
.overview-copy .settings-desc {
margin: 6px 0 0;
}
.overview-actions {
display: flex;
align-items: center;
flex-wrap: wrap;
justify-content: flex-end;
gap: 8px;
padding-top: 2px;
}
.overview-meta {
display: flex;
flex-wrap: wrap;
gap: 10px 0;
margin: 16px 0 0;
}
.overview-meta-item {
display: flex;
align-items: baseline;
flex-wrap: wrap;
gap: 4px 8px;
min-width: 0;
padding: 0 16px;
border-left: 1px solid var(--pm-border-color);
font-size: 12px;
line-height: 1.7;
}
.overview-meta-item:first-child {
padding-left: 0;
border-left: 0;
}
.overview-meta-item dt {
color: var(--pm-text-secondary);
}
.overview-meta-item dd {
margin: 0;
color: var(--pm-text-primary);
font-weight: 600;
font-variant-numeric: tabular-nums;
overflow-wrap: anywhere;
}
.overview-meta-item .tag-list {
flex: 1;
min-width: 0;
gap: 6px;
}
.overview-meta-warning {
flex: 1;
}
.overview-meta-warning dt {
color: var(--el-color-warning-dark-2);
}
.overview-meta :deep(.el-tag) {
max-width: 100%;
height: auto;
min-height: 20px;
white-space: normal;
overflow-wrap: anywhere;
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
border: 0;
}
@container config-main (max-width: 620px) {
.overview-heading {
flex-wrap: wrap;
gap: 12px;
}
.overview-copy {
flex-basis: 100%;
}
.overview-actions {
justify-content: flex-start;
padding-top: 0;
}
.overview-meta {
gap: 8px 16px;
}
.overview-meta-item {
padding: 0;
border-left: 0;
}
}

View File

@ -1,14 +1,63 @@
<template>
<div class="config-page" :data-config-page="selectedSection" v-loading="loading">
<div class="settings-header">
<div>
<h3>{{ currentSection.label }}</h3>
<p class="settings-desc">{{ currentSection.description }}</p>
<header class="config-overview" aria-labelledby="config-page-title">
<div class="overview-heading">
<div class="overview-copy">
<div class="overview-title">
<h3 id="config-page-title">{{ currentSection.label }}</h3>
<el-tag v-if="!loading && !availabilityError" :type="stateTagType" effect="light" size="small">
<span class="sr-only">运行状态:</span>{{ stateLabel }}
</el-tag>
</div>
<p class="settings-desc">{{ currentSection.description }}</p>
</div>
<div class="overview-actions">
<el-tag class="access-tag" :type="canWrite ? 'info' : 'warning'" effect="plain" size="small">
{{ canWrite ? '敏感凭据已脱敏' : '只读模式' }}
</el-tag>
<el-button
v-if="!availabilityError"
size="small"
plain
:loading="loading"
:disabled="!!working || sectionLoading || loading"
@click="refreshPage"
>
刷新状态
</el-button>
</div>
</div>
<el-tag class="access-tag" :type="canWrite ? 'info' : 'warning'" effect="plain">
{{ canWrite ? '敏感凭据已脱敏' : '只读模式' }}
</el-tag>
</div>
<template v-if="!loading && !availabilityError">
<dl class="overview-meta" aria-label="当前配置状态">
<div class="overview-meta-item">
<dt>生效代次</dt>
<dd>{{ status.activeGeneration || '-' }}</dd>
</div>
<div class="overview-meta-item">
<dt>最近校验</dt>
<dd>{{ formatTime(status.lastValidAt) }}</dd>
</div>
<div class="overview-meta-item" :class="{ 'overview-meta-warning': status.pendingRestart?.length }">
<dt>待重启服务</dt>
<dd v-if="status.pendingRestart?.length" class="tag-list">
<el-tag v-for="service in status.pendingRestart" :key="service" type="warning" effect="plain" size="small">
{{ serviceLabel(service) }}
</el-tag>
</dd>
<dd v-else>无需重启</dd>
</div>
</dl>
<el-alert
v-for="issue in status.degradedReasons || []"
:key="`${issue.code}-${issue.message}`"
:title="`${issue.code}:${issue.message}`"
type="warning"
:closable="false"
show-icon
class="degraded-alert"
/>
</template>
</header>
<el-alert
v-if="!canWrite"
@ -25,50 +74,6 @@
</div>
<div v-else-if="!loading" class="config-page-body">
<section class="status-panel">
<div class="status-heading">
<div>
<h4>配置快照</h4>
<p class="heading-note">当前运行状态与生效版本</p>
</div>
<el-button size="small" plain :loading="loading" :disabled="!!working || sectionLoading" @click="refreshPage">
刷新状态
</el-button>
</div>
<div class="status-grid">
<div class="status-item">
<span class="status-label">运行状态</span>
<el-tag :type="stateTagType" effect="light">{{ stateLabel }}</el-tag>
</div>
<div class="status-item">
<span class="status-label">当前生效代次</span>
<strong class="generation-value">{{ status.activeGeneration || '-' }}</strong>
</div>
<div class="status-item">
<span class="status-label">最近校验时间</span>
<strong>{{ formatTime(status.lastValidAt) }}</strong>
</div>
<div class="status-item" :class="{ 'status-warning': status.pendingRestart?.length }">
<span class="status-label">待重启服务</span>
<div v-if="status.pendingRestart?.length" class="tag-list">
<el-tag v-for="service in status.pendingRestart" :key="service" type="warning" effect="plain">
{{ serviceLabel(service) }}
</el-tag>
</div>
<strong v-else>无需重启</strong>
</div>
</div>
<el-alert
v-for="issue in status.degradedReasons || []"
:key="`${issue.code}-${issue.message}`"
:title="`${issue.code}:${issue.message}`"
type="warning"
:closable="false"
show-icon
class="degraded-alert"
/>
</section>
<section class="editor-panel" :aria-busy="sectionLoading">
<div class="panel-heading">
<div>
@ -194,3 +199,4 @@ const {
</script>
<style scoped src="../ConfigSettings.css"></style>
<style scoped src="./ConfigEditor.css"></style>

View File

@ -23,16 +23,14 @@
position: static;
}
.settings-header,
.panel-heading,
.status-heading {
.panel-heading {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 16px;
}
.settings-header h3,
.panel-heading h4,
.status-heading h4 {
.panel-heading h4 {
margin: 0;
color: var(--pm-text-primary);
}
@ -59,8 +57,7 @@
.config-availability .el-alert {
width: 100%;
}
.panel-heading h4,
.status-heading h4 {
.panel-heading h4 {
font-size: 16px;
line-height: 1.5;
}
@ -83,61 +80,15 @@
font-size: 12px;
font-weight: 700;
}
.status-panel,
.editor-panel,
.maintenance-panel {
border: 1px solid var(--pm-border-color);
border-radius: 16px;
background: var(--pm-bg-secondary);
}
.status-panel,
.editor-panel {
padding: 20px;
}
.status-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 10px;
margin-top: 16px;
}
.status-item {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 8px;
min-width: 0;
padding: 12px;
border: 1px solid transparent;
border-radius: 10px;
background: var(--pm-bg-primary);
}
.status-item .status-label {
color: var(--pm-text-secondary);
font-size: 12px;
line-height: 1.5;
}
.status-item strong {
color: var(--pm-text-primary);
font-size: 13px;
line-height: 1.6;
overflow-wrap: anywhere;
}
.status-item .generation-value {
font-size: 18px;
line-height: 1.3;
font-variant-numeric: tabular-nums;
}
.status-warning {
border-color: var(--el-color-warning-light-5);
background: var(--el-color-warning-light-9);
}
.status-item :deep(.el-tag) {
max-width: 100%;
height: auto;
min-height: 24px;
white-space: normal;
overflow-wrap: anywhere;
}
.tag-list {
display: flex;
flex-wrap: wrap;
@ -488,15 +439,11 @@
flex: none;
}
@container config-main (max-width: 620px) {
.status-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.secret-editor {
grid-template-columns: minmax(0, 1fr);
}
}
@container config-main (max-width: 400px) {
.status-panel,
.editor-panel {
padding: 16px;
}

View File

@ -245,6 +245,35 @@ test('离开、刷新和正在执行的变更受保护,取消时保留编辑
assert.equal(await page.beforeLeave(), false)
})
test('标题合并配置状态,保留刷新保护和降级提示,不再渲染快照卡片', () => {
const require = createRequire(new URL('../../frontend/package.json', import.meta.url))
const { parse, compileScript, compileStyle } = require('@vue/compiler-sfc')
const root = new URL('../../frontend/src/views/components/ConfigSettings/ConfigEditor/', import.meta.url)
const template = readFileSync(new URL('ConfigEditor.vue', root), 'utf8')
const css = readFileSync(new URL('ConfigEditor.css', root), 'utf8')
const { descriptor, errors } = parse(template)
assert.deepEqual(errors, [])
assert.doesNotThrow(() => compileScript(descriptor, { id: 'config-overview', inlineTemplate: true }))
assert.deepEqual(compileStyle({ source: css, id: 'data-v-config-overview', scoped: true }).errors, [])
const header = template.match(/<header[\s\S]*?<\/header>/)?.[0] || ''
assert.match(header, /aria-labelledby="config-page-title"/)
assert.match(header, /<h3 id="config-page-title">{{ currentSection.label }}/)
assert.match(header, /<span class="sr-only">运行状态:<\/span>{{ stateLabel }}/)
assert.match(header, /<dl class="overview-meta" aria-label="当前配置状态">/)
assert.equal((header.match(/<dt>/g) || []).length, 3)
assert.match(header, /status.activeGeneration/)
assert.match(header, /formatTime\(status.lastValidAt\)/)
assert.match(header, /v-for="service in status.pendingRestart"/)
assert.match(header, /v-for="issue in status.degradedReasons \|\| \[\]"/)
assert.match(header, /v-if="!loading && !availabilityError"/)
assert.match(header, /:disabled="!!working \|\| sectionLoading \|\| loading"/)
assert.match(header, /@click="refreshPage"/)
assert.match(header, /canWrite \? '敏感凭据已脱敏' : '只读模式'/)
assert.doesNotMatch(template, /配置快照|status-panel|status-grid/)
assert.match(css, /@container config-main \(max-width: 620px\)/)
assert.match(css, /overflow-wrap: anywhere/)
})
test('卸载发生在状态请求期间,不再发送后续分区请求', async () => {
const pending = deferred()
const requests = []