feat: Scheme 显示名称、打包结果按类型显示、错误分类
- Scheme 配置新增 displayName 字段,下拉框和任务列表显示友好名称 - Ad_Hoc 类型显示下载二维码,App_Store 类型显示 IPA 下载链接 - 新增 error_category 错误分类(证书/描述文件/编译/依赖等) - 打包服务增加详细错误分析和分类
This commit is contained in:
@@ -24,7 +24,7 @@
|
||||
<label>选择 Scheme</label>
|
||||
<select v-model="form.scheme_id">
|
||||
<option v-for="(scheme, id) in schemes" :key="id" :value="id">
|
||||
{{ scheme.name }}
|
||||
{{ scheme.displayName || scheme.name }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
@@ -51,14 +51,27 @@
|
||||
<div class="log-panel">
|
||||
<div class="log-header">
|
||||
<h3>实时日志 {{ currentTaskId ? `- ${currentTaskId}` : '' }}</h3>
|
||||
<button v-if="currentTaskId" class="btn btn-danger" style="width: auto; padding: 6px 12px;" @click="cancelTask">
|
||||
取消任务
|
||||
</button>
|
||||
<div class="log-actions">
|
||||
<button class="btn-toggle-logs" @click="showVerboseLogs = !showVerboseLogs">
|
||||
{{ showVerboseLogs ? '收起日志' : '展开详细日志' }}
|
||||
</button>
|
||||
<button v-if="currentTaskId" class="btn btn-danger" style="width: auto; padding: 6px 12px;" @click="cancelTask">
|
||||
取消任务
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="log-content" ref="logContainer">
|
||||
<div v-for="(log, i) in logs" :key="i" :class="['log-line', log.level]">
|
||||
[{{ formatTime(log.timestamp) }}] {{ log.message }}
|
||||
</div>
|
||||
<template v-for="(log, i) in logs" :key="i">
|
||||
<div v-if="showVerboseLogs || log.level === 'step' || log.level === 'error' || log.level === 'warn'"
|
||||
:class="['log-line', log.level]">
|
||||
<template v-if="log.level === 'step'">
|
||||
<div class="step-indicator">{{ log.message }}</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
[{{ formatTime(log.timestamp) }}] {{ log.message }}
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
<div v-if="!logs.length && !currentTaskId" class="log-line info" style="color: #666;">
|
||||
选择任务或创建新任务查看日志
|
||||
</div>
|
||||
@@ -84,16 +97,23 @@
|
||||
<span class="result-label">状态</span>
|
||||
<span :class="['task-status', `status-${completedTask.status}`]">{{ statusText(completedTask.status) }}</span>
|
||||
</div>
|
||||
<div v-if="completedTask.error_message" class="result-row">
|
||||
<span class="result-label">错误信息</span>
|
||||
<span class="error-msg">{{ completedTask.error_message }}</span>
|
||||
<div v-if="completedTask.error_category" class="result-row error-category-row">
|
||||
<span class="result-label">失败原因</span>
|
||||
<span class="error-hint">{{ errorCategoryHint(completedTask.error_category) }}</span>
|
||||
</div>
|
||||
<div v-if="completedTask.oss_url" class="result-row">
|
||||
<div v-if="completedTask.error_message" class="result-row">
|
||||
<span class="result-label">错误详情</span>
|
||||
<details class="error-details">
|
||||
<summary>查看详细错误信息</summary>
|
||||
<pre class="error-detail">{{ completedTask.error_message }}</pre>
|
||||
</details>
|
||||
</div>
|
||||
<div v-if="completedTask.build_type === 'App_Store' && completedTask.oss_url" class="result-row">
|
||||
<span class="result-label">下载链接</span>
|
||||
<a :href="completedTask.oss_url" target="_blank" class="download-link">点击下载 IPA</a>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="completedTask.qr_code_path" class="qr-section">
|
||||
<div v-if="completedTask.build_type === 'Ad_Hoc' && completedTask.qr_code_path" class="qr-section">
|
||||
<img :src="`/api/tasks/${completedTask.id}/qrcode`" alt="下载二维码" class="qr-image">
|
||||
<p class="qr-hint">扫码下载安装</p>
|
||||
</div>
|
||||
@@ -119,6 +139,7 @@
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, nextTick, watch, onUnmounted } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
|
||||
const apps = ref({})
|
||||
const schemes = ref({})
|
||||
@@ -136,7 +157,10 @@ const currentTaskId = ref(null)
|
||||
const completedTask = ref(null)
|
||||
const logs = ref([])
|
||||
const logContainer = ref(null)
|
||||
const showVerboseLogs = ref(false)
|
||||
const route = useRoute()
|
||||
let activeWs = null
|
||||
let skipNextConnect = false
|
||||
|
||||
const fetchTaskDetail = async (taskId) => {
|
||||
try {
|
||||
@@ -154,9 +178,19 @@ const connectWs = (taskId) => {
|
||||
activeWs.close()
|
||||
activeWs = null
|
||||
}
|
||||
showVerboseLogs.value = false
|
||||
if (!taskId) {
|
||||
logs.value = []
|
||||
completedTask.value = null
|
||||
return
|
||||
}
|
||||
// 从历史页跳转时跳过连接(日志已在 onMounted 中填充)
|
||||
if (skipNextConnect) {
|
||||
skipNextConnect = false
|
||||
return
|
||||
}
|
||||
logs.value = []
|
||||
completedTask.value = null
|
||||
if (!taskId) return
|
||||
|
||||
const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:'
|
||||
const ws = new WebSocket(`${protocol}//${location.host}/ws/tasks/${taskId}`)
|
||||
@@ -205,6 +239,23 @@ onMounted(async () => {
|
||||
schemes.value = await schemesRes.json()
|
||||
branches.value = await branchesRes.json()
|
||||
const allTasks = await tasksRes.json()
|
||||
|
||||
// 从历史页跳转过来时,读取 taskId 查询参数
|
||||
const queryTaskId = route.query.taskId
|
||||
if (queryTaskId) {
|
||||
const task = allTasks.find(t => t.id === queryTaskId)
|
||||
if (task) {
|
||||
// 已完成/失败的任务,直接显示结果
|
||||
if (task.status === 'completed' || task.status === 'failed' || task.status === 'cancelled') {
|
||||
completedTask.value = task
|
||||
logs.value = [{ timestamp: task.completed_at || task.created_at, level: task.status === 'failed' ? 'error' : 'step', message: task.status === 'failed' ? `打包失败: ${task.error_message || ''}` : '打包完成' }]
|
||||
}
|
||||
// 设置 currentTaskId(触发 watch → connectWs,用 flag 跳过)
|
||||
skipNextConnect = true
|
||||
currentTaskId.value = queryTaskId
|
||||
}
|
||||
}
|
||||
|
||||
tasks.value = allTasks.filter(t =>
|
||||
t.status === 'pending' || t.status === 'running' || t.id === currentTaskId.value
|
||||
)
|
||||
@@ -279,6 +330,20 @@ const statusText = (s) => {
|
||||
const map = { pending: '等待中', running: '打包中', completed: '已完成', failed: '失败', cancelled: '已取消' }
|
||||
return map[s] || s
|
||||
}
|
||||
|
||||
const errorCategoryHint = (cat) => {
|
||||
const map = {
|
||||
certificate: '签名证书问题:证书过期、未安装或不匹配',
|
||||
provisioning: '描述文件问题:描述文件过期、不匹配或未安装',
|
||||
compilation: '代码编译错误:请检查代码语法和类型引用',
|
||||
dependency: '依赖问题:Pod 依赖缺失或版本不兼容',
|
||||
git: '代码拉取失败:请检查分支是否存在',
|
||||
config: '配置替换失败:请检查打包配置',
|
||||
build: '构建配置错误:请检查 Scheme 和 Workspace 配置',
|
||||
unknown: '打包失败:请查看详细日志排查原因',
|
||||
}
|
||||
return map[cat] || map.unknown
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -335,4 +400,27 @@ const statusText = (s) => {
|
||||
.qr-image { width: 180px; height: 180px; border: 1px solid #f0f0f0; border-radius: 8px; }
|
||||
.qr-hint { margin-top: 8px; font-size: 13px; color: #999; }
|
||||
.error-msg { color: #ff4d4f; font-size: 13px; word-break: break-all; }
|
||||
|
||||
.log-actions { display: flex; align-items: center; gap: 8px; }
|
||||
.btn-toggle-logs {
|
||||
background: none; border: 1px solid #555; color: #a0a0a0;
|
||||
padding: 4px 10px; border-radius: 4px; font-size: 12px; cursor: pointer;
|
||||
}
|
||||
.btn-toggle-logs:hover { border-color: #1890ff; color: #1890ff; }
|
||||
|
||||
.step-indicator {
|
||||
background: rgba(24, 144, 255, 0.1); border-left: 3px solid #1890ff;
|
||||
padding: 6px 12px; margin: 4px 0; border-radius: 0 4px 4px 0;
|
||||
font-weight: 600; color: #1890ff; font-size: 13px;
|
||||
}
|
||||
|
||||
.error-category-row { background: #fff2f0; border-radius: 6px; padding: 8px 12px; margin: 4px 0; }
|
||||
.error-hint { color: #ff4d4f; font-size: 13px; font-weight: 500; }
|
||||
.error-details { font-size: 13px; }
|
||||
.error-details summary { color: #1890ff; cursor: pointer; font-size: 12px; }
|
||||
.error-detail {
|
||||
background: #fafafa; padding: 8px; border-radius: 4px; margin-top: 6px;
|
||||
font-family: 'Monaco', 'Menlo', monospace; font-size: 11px; color: #666;
|
||||
max-height: 150px; overflow-y: auto; white-space: pre-wrap; word-break: break-all;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user