feat: 完善运维控制台产品化体验
This commit is contained in:
+376
-13
@@ -2,13 +2,82 @@
|
||||
/**
|
||||
* 项目配置页展示项目仓库、发布环境、Gitea 仓库诊断和真实 Jenkins job 诊断状态。
|
||||
*/
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { Promotion, Refresh, Search } from '@element-plus/icons-vue'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import StatusPill from '../components/StatusPill.vue'
|
||||
import { usePlatformStore } from '../stores/platform'
|
||||
import type { EnvironmentStatus, ProjectConfig } from '../types/devops'
|
||||
import type { EnvironmentStatus, HealthState, ProjectConfig, ProjectPermissionLevel } from '../types/devops'
|
||||
|
||||
const platform = usePlatformStore()
|
||||
const diagnosticsLoading = ref(false)
|
||||
const searchKeyword = ref('')
|
||||
const readinessFilter = ref<ProjectReadinessState | ''>('')
|
||||
const permissionFilter = ref<ProjectPermissionLevel | ''>('')
|
||||
|
||||
type ProjectReadinessState = 'ready' | 'attention' | 'blocked' | 'readonly'
|
||||
|
||||
const readinessOptions: Array<{ label: string; value: ProjectReadinessState }> = [
|
||||
{ label: '可发布', value: 'ready' },
|
||||
{ label: '需关注', value: 'attention' },
|
||||
{ label: '有阻断', value: 'blocked' },
|
||||
{ label: '只读', value: 'readonly' },
|
||||
]
|
||||
|
||||
const permissionOptions: Array<{ label: string; value: ProjectPermissionLevel }> = [
|
||||
{ label: '构建权限', value: 'build' },
|
||||
{ label: '只读权限', value: 'read' },
|
||||
]
|
||||
|
||||
const filteredProjects = computed(() => {
|
||||
const keyword = searchKeyword.value.trim().toLowerCase()
|
||||
|
||||
return platform.projects.filter((project) => {
|
||||
if (readinessFilter.value && projectReadinessState(project) !== readinessFilter.value) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (permissionFilter.value && platform.projectPermissionLevel(project.key) !== permissionFilter.value) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (!keyword) {
|
||||
return true
|
||||
}
|
||||
|
||||
return [
|
||||
project.name,
|
||||
project.key,
|
||||
project.serviceType,
|
||||
project.repository,
|
||||
project.defaultBranch,
|
||||
project.tagPolicy,
|
||||
project.permissionLevelName,
|
||||
]
|
||||
.filter((value): value is string => typeof value === 'string' && value.length > 0)
|
||||
.some((value) => value.toLowerCase().includes(keyword))
|
||||
})
|
||||
})
|
||||
|
||||
const projectOverview = computed(() => {
|
||||
const ready = platform.projects.filter((project) => projectReadinessState(project) === 'ready').length
|
||||
const attention = platform.projects.filter((project) => projectReadinessState(project) === 'attention').length
|
||||
const blocked = platform.projects.filter((project) => projectReadinessState(project) === 'blocked').length
|
||||
const readOnly = platform.projects.filter((project) => projectReadinessState(project) === 'readonly').length
|
||||
|
||||
return [
|
||||
{ key: 'total', label: '纳管项目', value: platform.projects.length, tone: 'neutral' },
|
||||
{ key: 'ready', label: '可发布', value: ready, tone: 'success' },
|
||||
{ key: 'attention', label: '需关注', value: attention, tone: attention > 0 ? 'warning' : 'neutral' },
|
||||
{ key: 'blocked', label: '有阻断', value: blocked, tone: blocked > 0 ? 'danger' : 'neutral' },
|
||||
{ key: 'readonly', label: '只读项目', value: readOnly, tone: readOnly > 0 ? 'muted' : 'neutral' },
|
||||
]
|
||||
})
|
||||
|
||||
const hasActiveFilters = computed(() => Boolean(
|
||||
searchKeyword.value.trim() ||
|
||||
readinessFilter.value ||
|
||||
permissionFilter.value,
|
||||
))
|
||||
|
||||
onMounted(() => {
|
||||
void refreshProjectDiagnostics()
|
||||
@@ -80,23 +149,136 @@ function webhookHookText(hook: ProjectConfig['giteaWebhookHooks'][number]): stri
|
||||
|
||||
return `#${hook.id} ${hook.type} · ${active} · ${events}${branch}${matched}`
|
||||
}
|
||||
|
||||
function projectReadinessState(project: ProjectConfig): ProjectReadinessState {
|
||||
if (projectHasBlockingIssue(project)) {
|
||||
return 'blocked'
|
||||
}
|
||||
|
||||
if (projectHasWarningIssue(project)) {
|
||||
return 'attention'
|
||||
}
|
||||
|
||||
if (!platform.canBuildProject(project.key)) {
|
||||
return 'readonly'
|
||||
}
|
||||
|
||||
return 'ready'
|
||||
}
|
||||
|
||||
function projectReadinessLabel(project: ProjectConfig): string {
|
||||
const state = projectReadinessState(project)
|
||||
|
||||
if (state === 'ready') {
|
||||
return '可发布'
|
||||
}
|
||||
|
||||
if (state === 'blocked') {
|
||||
return '有阻断'
|
||||
}
|
||||
|
||||
if (state === 'readonly') {
|
||||
return '只读'
|
||||
}
|
||||
|
||||
return '需关注'
|
||||
}
|
||||
|
||||
function projectReadinessTagType(
|
||||
project: ProjectConfig,
|
||||
): 'success' | 'warning' | 'danger' | 'info' {
|
||||
const state = projectReadinessState(project)
|
||||
|
||||
if (state === 'ready') {
|
||||
return 'success'
|
||||
}
|
||||
|
||||
if (state === 'blocked') {
|
||||
return 'danger'
|
||||
}
|
||||
|
||||
return state === 'attention' ? 'warning' : 'info'
|
||||
}
|
||||
|
||||
function projectReadinessSummary(project: ProjectConfig): string {
|
||||
const issues = projectIssueSummaries(project)
|
||||
|
||||
if (issues.length > 0) {
|
||||
return issues[0]
|
||||
}
|
||||
|
||||
if (!platform.canBuildProject(project.key)) {
|
||||
return '当前账号只有只读权限'
|
||||
}
|
||||
|
||||
return 'Gitea、Jenkins、Webhook 和通知链路均未发现异常'
|
||||
}
|
||||
|
||||
function projectIssueSummaries(project: ProjectConfig): string[] {
|
||||
const issues: string[] = []
|
||||
|
||||
appendHealthIssue(issues, 'Gitea 仓库', project.giteaStatus, project.giteaMessage)
|
||||
appendHealthIssue(issues, 'Webhook / Relay', project.giteaWebhookStatus, project.giteaWebhookMessage)
|
||||
|
||||
for (const env of project.environments) {
|
||||
appendHealthIssue(issues, `${env.label} Jenkins`, env.jenkinsStatus, env.jenkinsMessage)
|
||||
}
|
||||
|
||||
appendHealthIssue(issues, '通知链路', project.integrations.wecom)
|
||||
|
||||
return issues
|
||||
}
|
||||
|
||||
function appendHealthIssue(
|
||||
issues: string[],
|
||||
label: string,
|
||||
state: HealthState,
|
||||
message?: string,
|
||||
) {
|
||||
if (state === 'healthy') {
|
||||
return
|
||||
}
|
||||
|
||||
const stateText = state === 'warning' ? '待配置' : state === 'offline' ? '离线' : '异常'
|
||||
issues.push(`${label}${message ? `:${message}` : `:${stateText}`}`)
|
||||
}
|
||||
|
||||
function projectHasBlockingIssue(project: ProjectConfig): boolean {
|
||||
return projectHealthStates(project).some((state) => state === 'error' || state === 'offline')
|
||||
}
|
||||
|
||||
function projectHasWarningIssue(project: ProjectConfig): boolean {
|
||||
return projectHealthStates(project).some((state) => state === 'warning')
|
||||
}
|
||||
|
||||
function projectHealthStates(project: ProjectConfig): HealthState[] {
|
||||
return [
|
||||
project.giteaStatus,
|
||||
project.giteaWebhookStatus,
|
||||
project.integrations.wecom,
|
||||
...project.environments.map((env) => env.jenkinsStatus),
|
||||
]
|
||||
}
|
||||
|
||||
function resetProjectFilters() {
|
||||
searchKeyword.value = ''
|
||||
readinessFilter.value = ''
|
||||
permissionFilter.value = ''
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page-shell">
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h1>项目配置</h1>
|
||||
<p>展示后端返回的项目、环境、Jenkins Job、Gitea 仓库和通知通道绑定关系。</p>
|
||||
</div>
|
||||
<ElButton type="primary" plain disabled>新增项目</ElButton>
|
||||
</div>
|
||||
|
||||
<section class="panel">
|
||||
<div class="panel-header">
|
||||
<h2>项目清单</h2>
|
||||
<h1>项目配置</h1>
|
||||
<p>展示后端返回的项目、环境、Jenkins Job、Gitea 仓库和通知通道绑定关系。</p>
|
||||
</div>
|
||||
<div class="toolbar">
|
||||
<ElTag type="info" round>后端配置源</ElTag>
|
||||
<ElButton
|
||||
size="small"
|
||||
:icon="Refresh"
|
||||
type="primary"
|
||||
plain
|
||||
:loading="diagnosticsLoading"
|
||||
@click="refreshProjectDiagnostics"
|
||||
@@ -104,8 +286,55 @@ function webhookHookText(hook: ProjectConfig['giteaWebhookHooks'][number]): stri
|
||||
刷新项目诊断
|
||||
</ElButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section class="project-overview-grid">
|
||||
<article
|
||||
v-for="item in projectOverview"
|
||||
:key="item.key"
|
||||
class="project-overview-card"
|
||||
:class="`tone-${item.tone}`"
|
||||
>
|
||||
<span>{{ item.label }}</span>
|
||||
<strong>{{ item.value }}</strong>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<div class="panel-header">
|
||||
<h2>项目清单</h2>
|
||||
<span>{{ filteredProjects.length }} / {{ platform.projects.length }} 个项目</span>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<ElTable :data="platform.projects" row-key="key">
|
||||
<div class="project-filter-bar">
|
||||
<ElInput
|
||||
v-model="searchKeyword"
|
||||
clearable
|
||||
:prefix-icon="Search"
|
||||
placeholder="搜索项目 / 仓库 / 默认分支"
|
||||
class="project-search-input"
|
||||
/>
|
||||
<ElSelect v-model="readinessFilter" clearable placeholder="发布态">
|
||||
<ElOption
|
||||
v-for="option in readinessOptions"
|
||||
:key="option.value"
|
||||
:label="option.label"
|
||||
:value="option.value"
|
||||
/>
|
||||
</ElSelect>
|
||||
<ElSelect v-model="permissionFilter" clearable placeholder="权限">
|
||||
<ElOption
|
||||
v-for="option in permissionOptions"
|
||||
:key="option.value"
|
||||
:label="option.label"
|
||||
:value="option.value"
|
||||
/>
|
||||
</ElSelect>
|
||||
<ElButton :disabled="!hasActiveFilters" @click="resetProjectFilters">
|
||||
重置筛选
|
||||
</ElButton>
|
||||
</div>
|
||||
<ElTable :data="filteredProjects" row-key="key" empty-text="没有符合条件的项目">
|
||||
<ElTableColumn type="expand">
|
||||
<template #default="{ row }">
|
||||
<div class="env-config">
|
||||
@@ -216,6 +445,23 @@ function webhookHookText(hook: ProjectConfig['giteaWebhookHooks'][number]): stri
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn prop="name" label="项目" min-width="150" />
|
||||
<ElTableColumn label="发布态" min-width="190">
|
||||
<template #default="{ row }">
|
||||
<div class="readiness-cell">
|
||||
<ElTag :type="projectReadinessTagType(row)" effect="light" round>
|
||||
{{ projectReadinessLabel(row) }}
|
||||
</ElTag>
|
||||
<small>{{ projectReadinessSummary(row) }}</small>
|
||||
</div>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="权限" width="120">
|
||||
<template #default="{ row }">
|
||||
<ElTag :type="platform.canBuildProject(row.key) ? 'success' : 'info'" effect="plain" round>
|
||||
{{ platform.canBuildProject(row.key) ? '构建' : '只读' }}
|
||||
</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn prop="serviceType" label="类型" min-width="130" />
|
||||
<ElTableColumn prop="repository" label="Gitea 仓库" min-width="320">
|
||||
<template #default="{ row }">
|
||||
@@ -234,6 +480,30 @@ function webhookHookText(hook: ProjectConfig['giteaWebhookHooks'][number]): stri
|
||||
</div>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="操作" width="170" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<div class="project-actions">
|
||||
<RouterLink
|
||||
v-slot="{ navigate }"
|
||||
custom
|
||||
:to="{ name: 'releases', query: { projectKey: row.key } }"
|
||||
>
|
||||
<ElButton
|
||||
:icon="Promotion"
|
||||
link
|
||||
type="primary"
|
||||
:disabled="!platform.canBuildProject(row.key)"
|
||||
@click="navigate"
|
||||
>
|
||||
发布
|
||||
</ElButton>
|
||||
</RouterLink>
|
||||
<RouterLink :to="{ name: 'runs', query: { projectKey: row.key } }">
|
||||
<ElButton link type="primary">运行记录</ElButton>
|
||||
</RouterLink>
|
||||
</div>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</ElTable>
|
||||
</div>
|
||||
</section>
|
||||
@@ -241,6 +511,66 @@ function webhookHookText(hook: ProjectConfig['giteaWebhookHooks'][number]): stri
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.project-overview-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.project-overview-card {
|
||||
min-width: 0;
|
||||
padding: 12px 14px;
|
||||
border: 1px solid #d8e2ef;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.project-overview-card span {
|
||||
display: block;
|
||||
color: #64748b;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.project-overview-card strong {
|
||||
display: block;
|
||||
margin-top: 6px;
|
||||
color: #0f172a;
|
||||
font-size: 22px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.project-overview-card.tone-success {
|
||||
border-color: #bbf7d0;
|
||||
background: #f0fdf4;
|
||||
}
|
||||
|
||||
.project-overview-card.tone-warning {
|
||||
border-color: #fde68a;
|
||||
background: #fffbeb;
|
||||
}
|
||||
|
||||
.project-overview-card.tone-danger {
|
||||
border-color: #fecaca;
|
||||
background: #fef2f2;
|
||||
}
|
||||
|
||||
.project-overview-card.tone-muted {
|
||||
border-color: #e2e8f0;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.project-filter-bar {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(260px, 1.4fr) 150px 150px max-content;
|
||||
gap: 10px;
|
||||
margin-bottom: 12px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.project-search-input {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.env-config {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
@@ -295,6 +625,24 @@ dd {
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.readiness-cell {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.readiness-cell small {
|
||||
color: #64748b;
|
||||
font-size: 12px;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.project-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.diagnostic-message {
|
||||
margin-left: 8px;
|
||||
color: #596578;
|
||||
@@ -340,7 +688,22 @@ dd {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
@media (max-width: 1080px) {
|
||||
.project-overview-grid {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.project-filter-bar {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.project-overview-grid,
|
||||
.project-filter-bar {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.env-config {
|
||||
grid-template-columns: 1fr;
|
||||
padding: 12px;
|
||||
|
||||
Reference in New Issue
Block a user