Files
devops-platform-web/src/views/SettingsView.vue
T
2026-06-11 23:52:01 +08:00

444 lines
11 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { Refresh } from '@element-plus/icons-vue'
import AuditJsonCell from '../components/AuditJsonCell.vue'
import StatusPill from '../components/StatusPill.vue'
import { usePlatformStore } from '../stores/platform'
import type {
AuditLogEntry,
IntegrationConfigItem,
IntegrationConfigState,
} from '../types/devops'
const platform = usePlatformStore()
type VariableTagType = 'success' | 'warning' | 'danger' | 'info'
type VariableGroup = {
key: string
label: string
names: string[]
emptyText: string
tagType?: VariableTagType
}
const configStatusMeta: Record<
IntegrationConfigState,
{ label: string; type: 'success' | 'warning' | 'danger' }
> = {
configured: { label: '已配置', type: 'success' },
partial: { label: '部分配置', type: 'warning' },
missing: { label: '未配置', type: 'danger' },
}
function configStatusLabel(status: IntegrationConfigState): string {
return configStatusMeta[status]?.label ?? '未配置'
}
function configStatusType(
status: IntegrationConfigState,
): 'success' | 'warning' | 'danger' {
return configStatusMeta[status]?.type ?? 'danger'
}
function formatCheckedAt(value: string): string {
if (!value) {
return '-'
}
const date = new Date(value)
if (Number.isNaN(date.getTime())) {
return value
}
return new Intl.DateTimeFormat('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
}).format(date)
}
function auditActor(record: AuditLogEntry): string {
return record.actorName || record.actorId || '-'
}
function auditResource(record: AuditLogEntry): string {
return record.resourceId
? `${record.resourceType} / ${record.resourceId}`
: record.resourceType
}
function auditPayload(record: AuditLogEntry): unknown {
return record.after ?? record.parameterDigest ?? record.before ?? null
}
function variableGroups(item: IntegrationConfigItem): VariableGroup[] {
return [
{
key: 'required',
label: '必需变量',
names: item.required,
emptyText: '暂无',
},
{
key: 'configured',
label: '已识别',
names: item.configured,
emptyText: '暂无',
tagType: 'success',
},
{
key: 'missing',
label: '缺失',
names: item.missing,
emptyText: '无',
tagType: 'danger',
},
{
key: 'optional',
label: '可选变量',
names: item.optional ?? [],
emptyText: '暂无',
tagType: 'info',
},
]
}
function variableHelp(item: IntegrationConfigItem, name: string) {
return item.variableHelp?.[name]
}
function variableDescription(
item: IntegrationConfigItem,
name: string,
group: VariableGroup,
): string {
const help = variableHelp(item, name)
if (help?.description) {
return help.description
}
if (item.note) {
return item.note
}
return `后端暂未返回 ${item.name} ${group.label} ${name} 的变量说明;请在 /settings/integration-config 响应中补充 variableHelp。`
}
function variableSourceLabel(item: IntegrationConfigItem, name: string): string {
return variableHelp(item, name)?.description ? '后端返回' : '等待后端说明'
}
</script>
<template>
<div class="page-shell">
<div class="page-header">
<div>
<h1>系统配置</h1>
<p>展示后端集成配置状态API 契约来源流程映射Agent 边界和通知维护状态</p>
</div>
</div>
<section class="panel">
<div class="panel-header">
<h2>后端集成配置状态</h2>
<span>检查时间{{ formatCheckedAt(platform.integrationConfig.checkedAt) }}</span>
</div>
<div class="panel-body">
<div class="secret-boundary">
这里只展示环境变量名和配置状态Jenkins tokenGitea token通知 webhookLLM key数据库连接串不会进入浏览器
</div>
<div class="config-list">
<article
v-for="item in platform.integrationConfig.integrations"
:key="item.key"
class="config-row"
>
<div class="config-row-main">
<div>
<h3>{{ item.name }}</h3>
<p v-if="item.note">{{ item.note }}</p>
</div>
<ElTag :type="configStatusType(item.status)" effect="light" round>
{{ configStatusLabel(item.status) }}
</ElTag>
</div>
<div class="variable-grid">
<div
v-for="group in variableGroups(item)"
:key="group.key"
class="variable-group"
>
<strong>{{ group.label }}</strong>
<div class="variable-tags">
<ElTooltip
v-for="name in group.names"
:key="`${group.key}-${name}`"
effect="light"
placement="top"
:show-after="160"
>
<template #content>
<div class="variable-tooltip">
<strong class="mono">{{ name }}</strong>
<p>{{ variableDescription(item, name, group) }}</p>
<small>{{ group.label }} · {{ variableSourceLabel(item, name) }}</small>
<small v-if="variableHelp(item, name)?.example">
示例{{ variableHelp(item, name)?.example }}
</small>
<a
v-if="variableHelp(item, name)?.docUrl"
:href="variableHelp(item, name)?.docUrl"
target="_blank"
rel="noreferrer"
>
查看官方文档
</a>
</div>
</template>
<ElTag class="mono variable-tag" :type="group.tagType" effect="plain">
{{ name }}
</ElTag>
</ElTooltip>
<span v-if="group.names.length === 0" class="empty-value">
{{ group.emptyText }}
</span>
</div>
</div>
</div>
</article>
</div>
</div>
</section>
<div class="section-grid">
<section v-for="item in platform.settings" :key="item.key" class="panel">
<div class="panel-header">
<h2>{{ item.name }}</h2>
<StatusPill :state="item.status" />
</div>
<div class="panel-body setting-card">
<dl>
<dt>责任方</dt>
<dd>{{ item.owner }}</dd>
<dt>契约</dt>
<dd>{{ item.contract }}</dd>
<dt>边界</dt>
<dd>{{ item.boundary }}</dd>
</dl>
</div>
</section>
</div>
<section class="panel">
<div class="panel-header">
<h2>最近运维审计</h2>
<ElButton :icon="Refresh" size="small" @click="platform.loadAuditLogs()">
刷新
</ElButton>
</div>
<div class="panel-body">
<ElTable :data="platform.auditLogs" border>
<ElTableColumn label="时间" min-width="150">
<template #default="{ row }">
{{ formatCheckedAt(row.createdAt) }}
</template>
</ElTableColumn>
<ElTableColumn prop="action" label="动作" min-width="220" />
<ElTableColumn label="资源" min-width="220">
<template #default="{ row }">
<span class="mono">{{ auditResource(row) }}</span>
</template>
</ElTableColumn>
<ElTableColumn label="操作者" min-width="130">
<template #default="{ row }">
{{ auditActor(row) }}
</template>
</ElTableColumn>
<ElTableColumn label="脱敏摘要" min-width="420">
<template #default="{ row }">
<AuditJsonCell :value="auditPayload(row)" />
</template>
</ElTableColumn>
</ElTable>
<ElEmpty
v-if="platform.auditLogs.length === 0"
description="暂无审计记录"
:image-size="72"
/>
</div>
</section>
<section class="panel">
<div class="panel-header">
<h2>前端环境变量</h2>
<span>第一阶段仅保留 API 地址</span>
</div>
<div class="panel-body compact-list">
<div class="list-row">
<span>
<strong class="mono">VITE_API_BASE_URL</strong>
<small>联调时指向后端网关未配置时默认 /api</small>
</span>
<ElTag type="info" round>后端接口</ElTag>
</div>
<div class="list-row">
<span>
<strong>密钥策略</strong>
<small>Jenkins tokenGitea token通知 webhookLLM key 均不得进入前端</small>
</span>
<ElTag type="success" round>前端隔离</ElTag>
</div>
</div>
</section>
</div>
</template>
<style scoped>
.setting-card dl {
display: grid;
grid-template-columns: 74px minmax(0, 1fr);
gap: 10px 12px;
margin: 0;
font-size: 13px;
}
.setting-card dt {
color: #7b8798;
}
.setting-card dd {
margin: 0;
color: #172033;
line-height: 1.6;
}
.secret-boundary {
margin-bottom: 14px;
padding: 12px 14px;
border: 1px solid #dbeafe;
border-radius: 8px;
background: #eff6ff;
color: #25508f;
font-size: 13px;
line-height: 1.6;
}
.config-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.config-row {
padding: 14px;
border: 1px solid #edf1f7;
border-radius: 8px;
background: #fbfcff;
}
.config-row-main {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 12px;
margin-bottom: 12px;
}
.config-row-main h3 {
margin: 0;
color: #182033;
font-size: 15px;
font-weight: 700;
letter-spacing: 0;
}
.config-row-main p {
margin: 6px 0 0;
color: #748093;
font-size: 12px;
line-height: 1.5;
}
.variable-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 10px;
}
.variable-group {
min-width: 0;
}
.variable-group strong {
display: block;
margin-bottom: 8px;
color: #4b5565;
font-size: 12px;
}
.variable-tags {
display: flex;
flex-wrap: wrap;
gap: 6px;
min-height: 24px;
}
.variable-tag {
cursor: help;
}
.variable-tooltip {
max-width: 320px;
}
.variable-tooltip strong,
.variable-tooltip small,
.variable-tooltip a {
display: block;
}
.variable-tooltip p {
margin: 6px 0;
color: #4b5565;
font-size: 12px;
line-height: 1.6;
}
.variable-tooltip small {
margin-top: 4px;
color: #7b8798;
font-size: 12px;
}
.variable-tooltip a {
margin-top: 6px;
color: #1d5fd0;
font-size: 12px;
}
.empty-value {
color: #9aa4b2;
font-size: 12px;
line-height: 24px;
}
@media (max-width: 1080px) {
.variable-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (max-width: 680px) {
.config-row-main {
flex-direction: column;
}
.variable-grid {
grid-template-columns: 1fr;
}
}
</style>