初始版本
This commit is contained in:
701
app/pages/developer/build.vue
Normal file
701
app/pages/developer/build.vue
Normal file
@@ -0,0 +1,701 @@
|
||||
<template>
|
||||
<div class="dev-page">
|
||||
<!-- 页面头部 -->
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h2 class="page-title">🔨 构建任务</h2>
|
||||
<p class="page-desc">管理应用的 CI/CD 构建任务,触发构建、查看构建状态和日志</p>
|
||||
</div>
|
||||
<a-space>
|
||||
<a-button @click="loadBuilds">
|
||||
<template #icon><ReloadOutlined /></template>
|
||||
刷新
|
||||
</a-button>
|
||||
<a-button type="primary" :loading="triggering" @click="showTriggerModal = true">
|
||||
<template #icon><PlayCircleOutlined /></template>
|
||||
触发构建
|
||||
</a-button>
|
||||
</a-space>
|
||||
</div>
|
||||
|
||||
<!-- 统计卡片 -->
|
||||
<a-row :gutter="[16, 16]" class="mb-6">
|
||||
<a-col :xs="12" :md="6" v-for="stat in buildStats" :key="stat.key">
|
||||
<div class="stat-card" :class="stat.color">
|
||||
<div class="stat-icon">{{ stat.icon }}</div>
|
||||
<div class="stat-info">
|
||||
<div class="stat-value">{{ stat.value }}</div>
|
||||
<div class="stat-label">{{ stat.label }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<!-- 筛选栏 -->
|
||||
<div class="panel mb-4">
|
||||
<div class="panel-header">
|
||||
<span class="panel-title">📋 构建记录</span>
|
||||
<a-space>
|
||||
<a-select v-model:value="filterStatus" style="width: 120px" placeholder="构建状态" allow-clear @change="loadBuilds">
|
||||
<a-select-option value="">全部状态</a-select-option>
|
||||
<a-select-option value="pending">排队中</a-select-option>
|
||||
<a-select-option value="running">构建中</a-select-option>
|
||||
<a-select-option value="success">成功</a-select-option>
|
||||
<a-select-option value="failed">失败</a-select-option>
|
||||
<a-select-option value="cancelled">已取消</a-select-option>
|
||||
</a-select>
|
||||
<a-select v-model:value="filterAppId" style="width: 200px" placeholder="选择应用" allow-clear @change="loadBuilds">
|
||||
<a-select-option v-for="app in apps" :key="app.productId" :value="app.productId">
|
||||
{{ app.productName }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
<a-input-search v-model:value="searchKeyword" placeholder="构建编号/分支" style="width: 180px" @search="loadBuilds" />
|
||||
</a-space>
|
||||
</div>
|
||||
|
||||
<!-- 构建列表 -->
|
||||
<a-spin :spinning="loading">
|
||||
<div v-if="builds.length > 0" class="build-list">
|
||||
<div v-for="build in builds" :key="build.id" class="build-item" :class="'status-' + build.status">
|
||||
<div class="build-status-bar" :class="build.status">
|
||||
<div class="status-indicator">
|
||||
<span class="status-dot"></span>
|
||||
<span class="status-text">{{ statusText(build.status) }}</span>
|
||||
</div>
|
||||
<span class="build-time">{{ formatTime(build.createTime) }}</span>
|
||||
</div>
|
||||
|
||||
<div class="build-content">
|
||||
<div class="build-main">
|
||||
<div class="build-info">
|
||||
<div class="build-number">
|
||||
<span class="ci-badge" :class="build.ciType">{{ ciTypeText(build.ciType) }}</span>
|
||||
<span class="number-text">#{{ build.buildNumber || build.id }}</span>
|
||||
</div>
|
||||
<div class="build-meta">
|
||||
<span v-if="build.branch" class="meta-item">
|
||||
<CodeSandboxOutlined /> {{ build.branch }}
|
||||
</span>
|
||||
<span v-if="build.commitAuthor" class="meta-item">
|
||||
<UserOutlined /> {{ build.commitAuthor }}
|
||||
</span>
|
||||
<span v-if="build.duration" class="meta-item">
|
||||
<ClockCircleOutlined /> {{ formatDuration(build.duration) }}
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="build.commitMessage" class="commit-message">{{ build.commitMessage }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="build-actions">
|
||||
<a-button size="small" type="link" @click="viewLog(build)">查看日志</a-button>
|
||||
|
||||
<template v-if="build.status === 'pending' || build.status === 'running'">
|
||||
<a-popconfirm title="确定要取消此构建?" @confirm="handleCancel(build)">
|
||||
<a-button danger size="small">取消</a-button>
|
||||
</a-popconfirm>
|
||||
</template>
|
||||
|
||||
<template v-if="build.status === 'failed'">
|
||||
<a-button type="primary" size="small" @click="handleRetry(build)">
|
||||
<template #icon><ReloadOutlined /></template>
|
||||
重试
|
||||
</a-button>
|
||||
</template>
|
||||
|
||||
<template v-if="build.artifactUrl">
|
||||
<a-button size="small" type="link">
|
||||
<template #icon><DownloadOutlined /></template>
|
||||
<a :href="build.artifactUrl" target="_blank">下载产物</a>
|
||||
</a-button>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="build.status === 'failed' && build.errorMessage" class="build-error">
|
||||
<WarningOutlined /> {{ build.errorMessage }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a-empty v-else description="暂无构建记录" class="py-8">
|
||||
<template #image>
|
||||
<div class="empty-icon">🔨</div>
|
||||
</template>
|
||||
</a-empty>
|
||||
</a-spin>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div v-if="pagination.total > 0" class="pagination-wrapper">
|
||||
<a-pagination
|
||||
v-model:current="pagination.current"
|
||||
v-model:pageSize="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
:show-size-changer="true"
|
||||
:show-quick-jumper="true"
|
||||
@change="handlePageChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 触发构建弹窗 -->
|
||||
<a-modal v-model:open="showTriggerModal" title="触发构建" :confirm-loading="triggering" @ok="handleTrigger" @cancel="resetTriggerForm">
|
||||
<a-form :model="triggerForm" layout="vertical">
|
||||
<a-form-item label="选择应用" required>
|
||||
<a-select v-model:value="triggerForm.appId" placeholder="选择要构建的应用" @change="handleAppChange">
|
||||
<a-select-option v-for="app in apps" :key="app.productId" :value="app.productId">
|
||||
{{ app.productName }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="选择流水线(可选)">
|
||||
<a-select v-model:value="triggerForm.pipelineId" placeholder="默认使用第一个可用流水线">
|
||||
<a-select-option v-for="p in pipelines" :key="p.id" :value="p.id">
|
||||
{{ p.name }} ({{ p.env }})
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="分支">
|
||||
<a-input v-model:value="triggerForm.branch" placeholder="留空则使用默认分支(main)" />
|
||||
</a-form-item>
|
||||
|
||||
<a-alert v-if="triggerForm.appId" type="info" show-icon>
|
||||
<template #message>
|
||||
提示:构建任务将在 CI 系统后台执行,可随时刷新页面查看构建进度。
|
||||
</template>
|
||||
</a-alert>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
|
||||
<!-- 构建日志弹窗 -->
|
||||
<a-modal v-model:open="showLogModal" title="构建日志" width="800px" :footer="null">
|
||||
<div class="log-container">
|
||||
<pre class="log-content">{{ buildLog || '加载中...' }}</pre>
|
||||
</div>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
ReloadOutlined,
|
||||
PlayCircleOutlined,
|
||||
CodeSandboxOutlined,
|
||||
UserOutlined,
|
||||
ClockCircleOutlined,
|
||||
DownloadOutlined,
|
||||
WarningOutlined,
|
||||
} from '@ant-design/icons-vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import {
|
||||
pageBuild,
|
||||
listBuildByApp,
|
||||
triggerBuild,
|
||||
cancelBuild,
|
||||
retryBuild,
|
||||
getBuildLog,
|
||||
getBuildStats,
|
||||
} from '@/api/app/cicd'
|
||||
import type { AppBuild } from '@/api/app/cicd'
|
||||
import { getDeveloperApps } from '@/api/app/appProduct'
|
||||
import type { AppProduct } from '@/api/app/appProduct/model'
|
||||
import { listPipelineByApp } from '@/api/app/cicd'
|
||||
import type { AppPipeline } from '@/api/app/cicd'
|
||||
|
||||
definePageMeta({ layout: 'developer' })
|
||||
useHead({ title: '构建任务 - 开发者中心' })
|
||||
|
||||
const userId = import.meta.client ? localStorage.getItem('UserId') : null
|
||||
|
||||
// 加载状态
|
||||
const loading = ref(false)
|
||||
const triggering = ref(false)
|
||||
const apps = ref<AppProduct[]>([])
|
||||
const builds = ref<AppBuild[]>([])
|
||||
const pipelines = ref<AppPipeline[]>([])
|
||||
|
||||
// 筛选
|
||||
const filterStatus = ref('')
|
||||
const filterAppId = ref<number | undefined>()
|
||||
const searchKeyword = ref('')
|
||||
|
||||
// 分页
|
||||
const pagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
})
|
||||
|
||||
// 构建统计
|
||||
const buildStats = reactive([
|
||||
{ key: 'total', icon: '📊', label: '总构建', value: 0, color: 'blue' },
|
||||
{ key: 'running', icon: '⏳', label: '进行中', value: 0, color: 'orange' },
|
||||
{ key: 'success', icon: '✅', label: '成功', value: 0, color: 'green' },
|
||||
{ key: 'failed', icon: '❌', label: '失败', value: 0, color: 'red' },
|
||||
])
|
||||
|
||||
// 触发构建弹窗
|
||||
const showTriggerModal = ref(false)
|
||||
const triggerForm = reactive({
|
||||
appId: undefined as number | undefined,
|
||||
pipelineId: undefined as number | undefined,
|
||||
branch: '',
|
||||
})
|
||||
|
||||
// 日志弹窗
|
||||
const showLogModal = ref(false)
|
||||
const buildLog = ref('')
|
||||
const currentBuild = ref<AppBuild | null>(null)
|
||||
|
||||
// ========== 加载数据 ==========
|
||||
|
||||
async function loadApps() {
|
||||
try {
|
||||
const res = await getDeveloperApps({ page: 1, limit: 100, userId: userId ? Number(userId) : undefined })
|
||||
apps.value = (res as any)?.data?.records || res?.list || []
|
||||
} catch (e) {
|
||||
console.error('加载应用列表失败:', e)
|
||||
}
|
||||
}
|
||||
|
||||
async function loadBuilds() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await pageBuild({
|
||||
page: pagination.current,
|
||||
limit: pagination.pageSize,
|
||||
appId: filterAppId.value,
|
||||
status: filterStatus.value || undefined,
|
||||
})
|
||||
|
||||
if (res?.data?.code === 200) {
|
||||
builds.value = res.data.data.records || []
|
||||
pagination.total = res.data.data.total || 0
|
||||
updateStats()
|
||||
} else {
|
||||
builds.value = []
|
||||
pagination.total = 0
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('加载构建记录失败:', e)
|
||||
builds.value = []
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function loadPipelines(appId: number) {
|
||||
try {
|
||||
const res = await listPipelineByApp(appId)
|
||||
if (res?.data?.code === 200) {
|
||||
pipelines.value = res.data.data || []
|
||||
} else {
|
||||
pipelines.value = []
|
||||
}
|
||||
} catch (e) {
|
||||
pipelines.value = []
|
||||
}
|
||||
}
|
||||
|
||||
async function loadStats() {
|
||||
if (!filterAppId.value) return
|
||||
|
||||
try {
|
||||
const res = await getBuildStats(filterAppId.value)
|
||||
if (res?.data?.code === 200) {
|
||||
const stats = res.data.data
|
||||
buildStats[0].value = stats.total || 0
|
||||
buildStats[1].value = stats.running || 0
|
||||
buildStats[2].value = stats.success || 0
|
||||
buildStats[3].value = stats.failed || 0
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('加载统计数据失败:', e)
|
||||
}
|
||||
}
|
||||
|
||||
function updateStats() {
|
||||
buildStats[0].value = builds.value.length
|
||||
buildStats[1].value = builds.value.filter(b => b.status === 'pending' || b.status === 'running').length
|
||||
buildStats[2].value = builds.value.filter(b => b.status === 'success').length
|
||||
buildStats[3].value = builds.value.filter(b => b.status === 'failed').length
|
||||
}
|
||||
|
||||
// ========== 操作 ==========
|
||||
|
||||
async function handleTrigger() {
|
||||
if (!triggerForm.appId) {
|
||||
message.error('请选择应用')
|
||||
return
|
||||
}
|
||||
|
||||
triggering.value = true
|
||||
try {
|
||||
const res = await triggerBuild(triggerForm.appId, triggerForm.branch || undefined)
|
||||
if (res?.data?.code === 200) {
|
||||
message.success('构建已触发!')
|
||||
showTriggerModal.value = false
|
||||
resetTriggerForm()
|
||||
loadBuilds()
|
||||
} else {
|
||||
message.error(res?.data?.message || '触发失败')
|
||||
}
|
||||
} catch (e: any) {
|
||||
message.error(e?.message || '触发构建失败')
|
||||
} finally {
|
||||
triggering.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleCancel(build: AppBuild) {
|
||||
cancelBuild(build.id!).then(res => {
|
||||
if (res?.data?.code === 200) {
|
||||
message.success('构建已取消')
|
||||
loadBuilds()
|
||||
} else {
|
||||
message.error(res?.data?.message || '取消失败')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function handleRetry(build: AppBuild) {
|
||||
retryBuild(build.id!).then(res => {
|
||||
if (res?.data?.code === 200) {
|
||||
message.success('构建已重试')
|
||||
loadBuilds()
|
||||
} else {
|
||||
message.error(res?.data?.message || '重试失败')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function viewLog(build: AppBuild) {
|
||||
currentBuild.value = build
|
||||
showLogModal.value = true
|
||||
buildLog.value = ''
|
||||
|
||||
try {
|
||||
const res = await getBuildLog(build.id!)
|
||||
if (res?.data?.code === 200) {
|
||||
buildLog.value = res.data.data?.log || '暂无日志'
|
||||
} else {
|
||||
buildLog.value = res?.data?.message || '获取日志失败'
|
||||
}
|
||||
} catch (e: any) {
|
||||
buildLog.value = '获取日志失败: ' + (e?.message || '未知错误')
|
||||
}
|
||||
}
|
||||
|
||||
function handleAppChange(appId: number) {
|
||||
triggerForm.pipelineId = undefined
|
||||
triggerForm.branch = ''
|
||||
if (appId) {
|
||||
loadPipelines(appId)
|
||||
}
|
||||
}
|
||||
|
||||
function resetTriggerForm() {
|
||||
triggerForm.appId = undefined
|
||||
triggerForm.pipelineId = undefined
|
||||
triggerForm.branch = ''
|
||||
pipelines.value = []
|
||||
}
|
||||
|
||||
function handlePageChange(page: number, pageSize: number) {
|
||||
pagination.current = page
|
||||
pagination.pageSize = pageSize
|
||||
loadBuilds()
|
||||
}
|
||||
|
||||
// ========== 辅助函数 ==========
|
||||
|
||||
function statusText(status?: string) {
|
||||
const map: Record<string, string> = {
|
||||
pending: '排队中',
|
||||
running: '构建中',
|
||||
success: '成功',
|
||||
failed: '失败',
|
||||
cancelled: '已取消',
|
||||
}
|
||||
return map[status || ''] || '未知'
|
||||
}
|
||||
|
||||
function ciTypeText(type?: string) {
|
||||
const map: Record<string, string> = {
|
||||
gitea: 'Gitea',
|
||||
jenkins: 'Jenkins',
|
||||
github: 'GitHub',
|
||||
}
|
||||
return map[type || ''] || type || 'CI'
|
||||
}
|
||||
|
||||
function formatTime(time?: string) {
|
||||
if (!time) return ''
|
||||
return new Date(time).toLocaleString('zh-CN')
|
||||
}
|
||||
|
||||
function formatDuration(seconds?: number) {
|
||||
if (!seconds) return ''
|
||||
if (seconds < 60) return `${seconds}s`
|
||||
const mins = Math.floor(seconds / 60)
|
||||
const secs = seconds % 60
|
||||
if (mins < 60) return `${mins}m ${secs}s`
|
||||
const hours = Math.floor(mins / 60)
|
||||
const mins2 = mins % 60
|
||||
return `${hours}h ${mins2}m`
|
||||
}
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
loadApps()
|
||||
loadBuilds()
|
||||
loadStats()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.dev-page {
|
||||
min-height: 100%;
|
||||
padding: 20px 24px 28px;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
color: #1f2937;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.page-desc {
|
||||
font-size: 13px;
|
||||
color: #9ca3af;
|
||||
margin: 4px 0 0;
|
||||
}
|
||||
|
||||
/* 统计卡片 */
|
||||
.stat-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 16px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid transparent;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.stat-card:hover { transform: translateY(-1px); }
|
||||
.stat-card.blue { background: #eff6ff; border-color: #dbeafe; }
|
||||
.stat-card.orange { background: #fff7ed; border-color: #fed7aa; }
|
||||
.stat-card.green { background: #f0fdf4; border-color: #bbf7d0; }
|
||||
.stat-card.red { background: #fff1f2; border-color: #fecdd3; }
|
||||
.stat-icon { font-size: 28px; flex-shrink: 0; }
|
||||
.stat-value { font-size: 22px; font-weight: 700; color: rgba(0,0,0,0.85); }
|
||||
.stat-label { font-size: 12px; color: rgba(0,0,0,0.45); margin-top: 2px; }
|
||||
|
||||
/* 面板 */
|
||||
.panel {
|
||||
background: #fff;
|
||||
border: 1px solid #f0f0f0;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.panel-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 14px 18px;
|
||||
border-bottom: 1px solid #f5f5f5;
|
||||
}
|
||||
|
||||
.panel-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: rgba(0,0,0,0.85);
|
||||
}
|
||||
|
||||
/* 构建列表 */
|
||||
.build-list {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.build-item {
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.build-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.build-item:hover {
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.build-status-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8px 18px;
|
||||
background: #f9f9f9;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.status-indicator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: #999;
|
||||
}
|
||||
|
||||
.status-pending .status-dot { background: #faad14; }
|
||||
.status-running .status-dot { background: #1890ff; animation: pulse 1s infinite; }
|
||||
.status-success .status-dot { background: #52c41a; }
|
||||
.status-failed .status-dot { background: #ff4d4f; }
|
||||
.status-cancelled .status-dot { background: #d9d9d9; }
|
||||
|
||||
.status-text {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.status-pending .status-text { color: #faad14; }
|
||||
.status-running .status-text { color: #1890ff; }
|
||||
.status-success .status-text { color: #52c41a; }
|
||||
.status-failed .status-text { color: #ff4d4f; }
|
||||
.status-cancelled .status-text { color: #999; }
|
||||
|
||||
.build-time {
|
||||
font-size: 12px;
|
||||
color: rgba(0,0,0,0.45);
|
||||
}
|
||||
|
||||
.build-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 14px 18px;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.build-main {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.build-number {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.ci-badge {
|
||||
font-size: 11px;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.ci-badge.gitea { background: #f0f0f0; color: #333; }
|
||||
.ci-badge.jenkins { background: #dbeafe; color: #1d4ed8; }
|
||||
.ci-badge.github { background: #f0f9ff; color: #0366d6; }
|
||||
|
||||
.number-text {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: rgba(0,0,0,0.85);
|
||||
}
|
||||
|
||||
.build-meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
font-size: 12px;
|
||||
color: rgba(0,0,0,0.45);
|
||||
}
|
||||
|
||||
.meta-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.commit-message {
|
||||
margin-top: 6px;
|
||||
font-size: 12px;
|
||||
color: rgba(0,0,0,0.65);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
.build-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.build-error {
|
||||
padding: 10px 18px;
|
||||
background: #fff2f0;
|
||||
border-top: 1px solid #ffccc7;
|
||||
font-size: 12px;
|
||||
color: #ff4d4f;
|
||||
}
|
||||
|
||||
.py-8 { padding: 40px 0; }
|
||||
.mb-6 { margin-bottom: 24px; }
|
||||
.mb-4 { margin-bottom: 16px; }
|
||||
|
||||
.empty-icon {
|
||||
font-size: 48px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
/* 日志 */
|
||||
.log-container {
|
||||
background: #1e1e1e;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
max-height: 500px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.log-content {
|
||||
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||||
font-size: 12px;
|
||||
line-height: 1.6;
|
||||
color: #d4d4d4;
|
||||
margin: 0;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.pagination-wrapper {
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.5; }
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user