配置管理增加二级菜单
Some checks are pending
Security Checks / security (push) Waiting to run

This commit is contained in:
smail 2026-09-02 21:38:20 +08:00
parent a8100db3e9
commit 1b25dff25e
3 changed files with 150 additions and 67 deletions

View File

@ -81,8 +81,52 @@
.degraded-alert {
margin-top: 16px;
}
.section-select {
width: 150px;
.config-workspace {
display: grid;
grid-template-columns: 176px minmax(0, 1fr);
align-items: start;
gap: 20px;
}
.config-submenu {
position: sticky;
top: 0;
display: grid;
gap: 6px;
align-self: start;
padding: 8px;
border: 1px solid var(--pm-border-color);
border-radius: var(--pm-radius-lg);
background: var(--pm-bg-secondary);
}
.config-submenu-item {
width: 100%;
padding: 12px 14px;
color: var(--pm-text-secondary);
text-align: left;
font: inherit;
border: 0;
border-radius: var(--pm-radius-sm);
background: transparent;
cursor: pointer;
transition: color .2s ease, background-color .2s ease;
}
.config-submenu-item:hover,
.config-submenu-item.active {
color: var(--pm-primary-color);
background: var(--el-color-primary-light-9);
}
.config-submenu-item.active {
font-weight: 600;
}
.config-main {
display: grid;
gap: 24px;
min-width: 0;
}
.selected-section-label {
color: var(--pm-primary-color);
font-size: 14px;
white-space: nowrap;
}
.editor-meta {
display: flex;
@ -231,8 +275,24 @@
align-items: stretch;
flex-direction: column;
}
.section-select {
width: 100%;
.config-workspace {
grid-template-columns: 1fr;
gap: 16px;
}
.config-submenu {
position: static;
display: flex;
gap: 6px;
overflow-x: auto;
padding: 6px;
}
.config-submenu-item {
flex: 0 0 auto;
width: auto;
white-space: nowrap;
}
.selected-section-label {
white-space: normal;
}
.action-row .el-button,
.audit-actions .el-button {
@ -295,4 +355,4 @@
align-items: stretch;
flex-direction: column;
}
}
}

View File

@ -135,6 +135,12 @@ export function usePage() {
}
}
const selectSection = value => {
if (selectedSection.value === value) return
selectedSection.value = value
loadSection()
}
const loadBackups = async () => {
if (!canRollback.value) return
backupsLoading.value = true
@ -426,7 +432,7 @@ export function usePage() {
return {
loading, working, status, stateLabel, stateTagType, sectionOptions: sections,
selectedSection, editorText, sectionError, sensitiveHint, secretField, secretValue,
selectedSection, selectSection, editorText, sectionError, sensitiveHint, secretField, secretValue,
confirmed, savedGeneration, validationResult, canWrite, canRollback, canAudit, canExportAudit,
backupsLoading, backups, rollbackGeneration, rollbackConfirmed, rollbackPreview,
auditLoading, auditItems, loadAll, loadSection, validateConfig, saveConfig, applyConfig,

View File

@ -14,58 +14,72 @@
class="config-alert"
/>
<section class="status-panel">
<div class="status-heading">
<div>
<span class="section-eyebrow">运行状态</span>
<h4>配置快照</h4>
</div>
<el-button size="small" plain :loading="loading" @click="loadAll">刷新</el-button>
</div>
<div class="status-grid">
<div class="status-item">
<span>状态</span>
<el-tag :type="stateTagType">{{ stateLabel }}</el-tag>
</div>
<div class="status-item">
<span>当前生效代次</span>
<strong>{{ status.activeGeneration || '-' }}</strong>
</div>
<div class="status-item">
<span>最近校验时间</span>
<strong>{{ formatTime(status.lastValidAt) }}</strong>
</div>
<div class="status-item status-wide">
<span>待重启服务</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>
<div class="config-workspace">
<nav class="config-submenu" aria-label="配置分区">
<button
v-for="item in sectionOptions"
:key="item.value"
type="button"
class="config-submenu-item"
:class="{ active: selectedSection === item.value }"
:aria-current="selectedSection === item.value ? 'page' : undefined"
@click="selectSection(item.value)"
>
{{ item.label }}
</button>
</nav>
<section class="editor-panel">
<div class="panel-heading">
<div>
<span class="section-eyebrow">分区配置</span>
<h4>编辑候选配置</h4>
</div>
<el-select v-model="selectedSection" class="section-select" aria-label="配置分区" @change="loadSection">
<el-option v-for="item in sectionOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</div>
<div class="config-main">
<section class="status-panel">
<div class="status-heading">
<div>
<span class="section-eyebrow">运行状态</span>
<h4>配置快照</h4>
</div>
<el-button size="small" plain :loading="loading" @click="loadAll">刷新</el-button>
</div>
<div class="status-grid">
<div class="status-item">
<span>状态</span>
<el-tag :type="stateTagType">{{ stateLabel }}</el-tag>
</div>
<div class="status-item">
<span>当前生效代次</span>
<strong>{{ status.activeGeneration || '-' }}</strong>
</div>
<div class="status-item">
<span>最近校验时间</span>
<strong>{{ formatTime(status.lastValidAt) }}</strong>
</div>
<div class="status-item status-wide">
<span>待重启服务</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">
<div class="panel-heading">
<div>
<span class="section-eyebrow">分区配置</span>
<h4>编辑候选配置</h4>
</div>
<strong class="selected-section-label">{{ sectionOptions.find(item => item.value === selectedSection)?.label }}</strong>
</div>
<el-alert
v-if="sectionError"
@ -124,9 +138,9 @@
<li v-for="issue in validationResult.issues" :key="`${issue.code}-${issue.message}`">{{ issue.code }}:{{ issue.message }}</li>
</ul>
</div>
</section>
</section>
<section v-if="canMigrate" class="migration-panel">
<section v-if="canMigrate" class="migration-panel">
<div class="panel-heading">
<div>
<span class="section-eyebrow">数据库迁移</span>
@ -172,9 +186,9 @@
<span>目标数据库:{{ migrationRecord.target?.name || migrationRecord.target?.localRef || '-' }}</span>
<span v-if="migrationRecord.errorCode" class="migration-error">错误代码:{{ migrationRecord.errorCode }}</span>
</div>
</section>
</section>
<section v-if="canRestart && status.pendingRestart?.length" class="restart-panel">
<section v-if="canRestart && status.pendingRestart?.length" class="restart-panel">
<div class="panel-heading">
<div>
<span class="section-eyebrow">受控重启</span>
@ -196,9 +210,9 @@
<span>服务管理器:{{ restartPreview.serviceManager }}</span>
<span>{{ restartPreview.triggerAvailable ? '重启通道可用' : '重启通道暂不可用,请先检查服务部署' }}</span>
</div>
</section>
</section>
<section v-if="canRollback" class="rollback-panel">
<section v-if="canRollback" class="rollback-panel">
<div class="panel-heading">
<div>
<span class="section-eyebrow">受控回滚</span>
@ -231,9 +245,9 @@
<span>候选代次:{{ rollbackPreview.candidateGeneration }}</span>
<span>变更分区:{{ rollbackPreview.changedSections?.map(sectionLabel).join('、') || '无' }}</span>
</div>
</section>
</section>
<section v-if="canAudit" class="audit-panel">
<section v-if="canAudit" class="audit-panel">
<div class="panel-heading">
<div>
<span class="section-eyebrow">审计记录</span>
@ -255,7 +269,9 @@
</div>
</div>
<el-empty v-else description="暂无配置审计记录" :image-size="72" />
</section>
</section>
</div>
</div>
</div>
</template>
@ -271,6 +287,7 @@ const {
stateTagType,
sectionOptions,
selectedSection,
selectSection,
editorText,
sectionError,
sensitiveHint,
@ -325,4 +342,4 @@ const {
void computed
</script>
<style scoped src="./ConfigSettings.css"></style>
<style scoped src="./ConfigSettings.css"></style>