新增: DockerNode+CI状态读取+模板CRUD+CIStatus面板

- DockerNode: 环境检测(docker --version)+授权+容器内执行(12个测试)

- ci_status.rs: Gitea commit status API 读取(7个测试,失败返回空不阻塞)

- CIStatus.vue: CI检查面板(通过/失败/pending 汇总+可点击跳转)

- 模板CRUD IPC: list/save/delete templates(内置只读+自定义KV持久化)

- TemplateInfo 结构体(IPC传输用)

- 零编译警告,vue-tsc+vite build 通过
This commit is contained in:
2026-07-02 01:02:39 +08:00
parent f1fb8655c3
commit d5b0459a8a
7 changed files with 858 additions and 1 deletions

View File

@@ -0,0 +1,120 @@
<template>
<div class="ci-status" v-if="statuses.length > 0">
<div class="ci-header">
<span class="ci-title">CI 检查</span>
<span class="ci-summary" :class="summaryClass">{{ summaryText }}</span>
</div>
<div class="ci-list">
<a
v-for="check in statuses"
:key="check.name"
:href="check.url || undefined"
class="ci-item"
:class="'ci-item--' + check.status"
target="_blank"
rel="noopener"
>
<span class="ci-item-icon">{{ statusIcon(check.status) }}</span>
<span class="ci-item-name">{{ check.name }}</span>
</a>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, ref, onMounted } from 'vue'
import { invoke } from '@tauri-apps/api/core'
interface CheckStatus {
name: string
status: string
url: string | null
}
const props = defineProps<{
repoUrl: string
commitSha: string
}>()
const statuses = ref<CheckStatus[]>([])
const summaryClass = computed(() => {
const hasFailure = statuses.value.some(s => s.status === 'failure')
const hasPending = statuses.value.some(s => s.status === 'pending')
if (hasFailure) return 'ci-summary--fail'
if (hasPending) return 'ci-summary--pending'
return 'ci-summary--pass'
})
const summaryText = computed(() => {
const pass = statuses.value.filter(s => s.status === 'success').length
const fail = statuses.value.filter(s => s.status === 'failure').length
const pending = statuses.value.filter(s => s.status === 'pending').length
if (fail > 0) return `${fail} 项失败`
if (pending > 0) return `${pending} 项进行中`
return `${pass} 项通过`
})
function statusIcon(status: string): string {
switch (status) {
case 'success': return '✓'
case 'failure': return '✗'
case 'pending': return '⏳'
default: return '○'
}
}
onMounted(async () => {
try {
statuses.value = await invoke<CheckStatus[]>('get_commit_status', {
repoUrl: props.repoUrl,
commitSha: props.commitSha,
})
} catch {
statuses.value = []
}
})
</script>
<style scoped>
.ci-status {
border: 1px solid var(--df-border);
border-radius: 8px;
padding: 8px 12px;
margin: 8px 0;
background: var(--df-bg-secondary);
font-size: 12px;
}
.ci-header {
display: flex;
justify-content: space-between;
margin-bottom: 8px;
font-weight: 600;
}
.ci-title { color: var(--df-text); }
.ci-summary--pass { color: var(--df-success); }
.ci-summary--fail { color: var(--df-danger); }
.ci-summary--pending { color: var(--df-warning); }
.ci-list {
display: flex;
flex-wrap: wrap;
gap: 4px;
}
.ci-item {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 2px 8px;
border-radius: 4px;
font-size: 11px;
background: var(--df-bg);
border: 1px solid var(--df-border);
text-decoration: none;
color: var(--df-text);
}
.ci-item--success { border-color: var(--df-success); }
.ci-item--failure { border-color: var(--df-danger); }
.ci-item--pending { border-color: var(--df-warning); }
.ci-item-icon { font-size: 10px; font-weight: 700; }
.ci-item:hover { opacity: 0.8; }
</style>