feat: 优化发布单冲突提示

- src/services/http.ts: 保留后端 envelope 错误码、状态和 details
- src/stores/platform.ts: 增加发布单列表轻量刷新入口
- src/views: 发布中心和运行记录接入 CONFLICT 刷新提示
- src/types/devops.ts: 同步 ReleaseRun version 字段
This commit is contained in:
湛兮
2026-06-12 05:22:28 +08:00
parent 0302660351
commit a2eb83051c
6 changed files with 167 additions and 14 deletions
+2
View File
@@ -98,6 +98,7 @@ interface BackendDeployRunStep {
interface BackendDeployRun {
id?: string
version?: number
projectKey?: string
environment?: EnvironmentName
ref?: string
@@ -993,6 +994,7 @@ function toReleaseRun(run: BackendDeployRun, projectList: ProjectConfig[]): Rele
return {
id: run.id || `${projectKey}-${ref}-${run.createdAt ?? Date.now()}`,
version: typeof run.version === 'number' && Number.isFinite(run.version) ? run.version : 0,
projectKey,
projectName: project?.name ?? projectKey,
environment: toEnvironmentName(run.environment),
+85 -8
View File
@@ -1,5 +1,29 @@
import axios from 'axios'
type DevopsApiErrorOptions = {
code?: string
message?: string
details?: unknown
status?: number
}
/**
* 后端 envelope 错误在浏览器侧的统一形态,保留 code/details 供页面做精确交互。
*/
export class DevopsApiError extends Error {
readonly code: string
readonly details: unknown
readonly status: number | undefined
constructor(options: DevopsApiErrorOptions) {
super(options.message || '请求失败,请稍后重试')
this.name = 'DevopsApiError'
this.code = options.code || 'HTTP_ERROR'
this.details = options.details
this.status = options.status
}
}
/**
* DevOps 平台统一 HTTP 实例,只识别后端 envelope,不承接任何第三方密钥。
*/
@@ -27,13 +51,7 @@ http.interceptors.response.use(
return response
},
(error) => {
const message =
error.response?.data?.error?.message ||
error.response?.data?.message ||
error.message ||
'请求失败,请稍后重试'
return Promise.reject(new Error(message))
return Promise.reject(toDevopsApiError(error))
},
)
@@ -59,7 +77,11 @@ function unwrapApiPayload(payload: unknown): unknown {
return envelope.data
}
throw new Error(envelope.error?.message || '请求失败,请稍后重试')
throw new DevopsApiError({
code: envelope.error?.code,
message: envelope.error?.message,
details: envelope.error?.details,
})
}
if (Object.keys(payload).length === 1 && 'data' in payload) {
@@ -76,3 +98,58 @@ function isRecord(value: unknown): value is Record<string, unknown> {
function isApiEnvelope(value: unknown): value is ApiEnvelope<unknown> {
return isRecord(value) && typeof value.success === 'boolean'
}
/**
* 类型守卫供页面区分业务错误与普通网络错误,避免靠中文文案判断状态。
*/
export function isDevopsApiError(error: unknown): error is DevopsApiError {
return error instanceof DevopsApiError
}
function toDevopsApiError(error: unknown): DevopsApiError {
if (isDevopsApiError(error)) {
return error
}
if (isRecord(error)) {
const response = isRecord(error.response) ? error.response : undefined
const status = typeof response?.status === 'number' ? response.status : undefined
const payload = response?.data
if (isApiEnvelope(payload)) {
return new DevopsApiError({
code: payload.error?.code,
message: payload.error?.message,
details: payload.error?.details,
status,
})
}
if (isRecord(payload)) {
const backendError = isRecord(payload.error) ? payload.error : undefined
const code = textValue(backendError?.code) || textValue(payload.code)
const message =
textValue(backendError?.message) ||
textValue(payload.message) ||
textValue(error.message)
return new DevopsApiError({
code,
message,
details: backendError?.details ?? payload.details,
status,
})
}
return new DevopsApiError({
message: textValue(error.message),
status,
})
}
return new DevopsApiError({})
}
function textValue(value: unknown): string | undefined {
return typeof value === 'string' && value.trim() ? value : undefined
}
+11
View File
@@ -124,6 +124,16 @@ export const usePlatformStore = defineStore('platform', () => {
return result
}
/**
* 发布单状态被其他请求推进时,只刷新 run 列表并同步项目环境摘要。
*/
async function refreshRuns() {
runs.value = await devopsApi.getRuns(projects.value)
applyProjectRunSummaries()
dataSource.value = devopsApi.getLastDataSource()
return runs.value
}
async function loadProjectRefs(projectKey: string) {
const refs = await devopsApi.getProjectRefs(projectKey)
projectRefs.value = {
@@ -332,6 +342,7 @@ export const usePlatformStore = defineStore('platform', () => {
retryDeployRun,
syncJenkinsRun,
syncJenkinsRuns,
refreshRuns,
loadProjectRefs,
loadProjectJenkinsDiagnostics,
loadProjectGiteaDiagnostics,
+1
View File
@@ -351,6 +351,7 @@ export interface PipelineStep {
export interface ReleaseRun {
id: string
version: number
projectKey: string
projectName: string
environment: EnvironmentName
+35
View File
@@ -8,6 +8,7 @@ import { computed, reactive, ref, watch } from 'vue'
import BpmnStatusViewer from '../components/BpmnStatusViewer.vue'
import RunExecutionDetails from '../components/RunExecutionDetails.vue'
import StatusPill from '../components/StatusPill.vue'
import { isDevopsApiError } from '../services/http'
import { usePlatformStore } from '../stores/platform'
import type { EnvironmentName, ProjectRefItem, ReleaseRun, ReleaseStatus } from '../types/devops'
@@ -169,6 +170,8 @@ async function triggerRelease() {
activeRun.value = run
localStatus.value = run.status
ElMessage.success('发布单已提交到后端')
} catch (error) {
await handleReleaseActionError(error, '发布单提交失败')
} finally {
submitting.value = false
}
@@ -197,11 +200,14 @@ async function cancelRelease() {
}
submitting.value = true
const runId = shownRun.value.id
try {
const run = await platform.cancelDeployRun(shownRun.value)
activeRun.value = run
localStatus.value = run.status
ElMessage.success('发布单已取消')
} catch (error) {
await handleReleaseActionError(error, '发布单取消失败', runId)
} finally {
submitting.value = false
}
@@ -213,11 +219,14 @@ async function retryRelease() {
}
submitting.value = true
const runId = shownRun.value.id
try {
const run = await platform.retryDeployRun(shownRun.value)
activeRun.value = run
localStatus.value = run.status
ElMessage.success('已提交重试发布单')
} catch (error) {
await handleReleaseActionError(error, '发布单重试失败', runId)
} finally {
submitting.value = false
}
@@ -229,11 +238,14 @@ async function syncJenkinsRun() {
}
submitting.value = true
const runId = shownRun.value.id
try {
const run = await platform.syncJenkinsRun(shownRun.value)
activeRun.value = run
localStatus.value = run.status
ElMessage.success('已同步 Jenkins 状态')
} catch (error) {
await handleReleaseActionError(error, 'Jenkins 状态同步失败', runId)
} finally {
submitting.value = false
}
@@ -267,6 +279,29 @@ function useFailedRun() {
activeRun.value = failed
}
async function handleReleaseActionError(
error: unknown,
fallbackMessage: string,
runId?: string,
) {
if (isDevopsApiError(error) && error.code === 'CONFLICT') {
const refreshedRuns = await platform.refreshRuns()
const refreshedRun = runId
? refreshedRuns.find((run) => run.id === runId)
: refreshedRuns[0]
if (refreshedRun) {
activeRun.value = refreshedRun
localStatus.value = refreshedRun.status
}
ElMessage.warning('发布单状态已变化,已刷新最新记录')
return
}
ElMessage.error(error instanceof Error ? error.message : fallbackMessage)
}
watch(
() => [form.projectKey, platform.projects.length],
() => {
+33 -6
View File
@@ -9,6 +9,7 @@ import { useRoute } from 'vue-router'
import BpmnStatusViewer from '../components/BpmnStatusViewer.vue'
import RunExecutionDetails from '../components/RunExecutionDetails.vue'
import StatusPill from '../components/StatusPill.vue'
import { isDevopsApiError } from '../services/http'
import { usePlatformStore } from '../stores/platform'
import type { ReleaseRun } from '../types/devops'
@@ -64,9 +65,13 @@ async function syncSelectedRun() {
return
}
const run = await platform.syncJenkinsRun(selectedRun.value)
selectedRunId.value = run.id
ElMessage.success('已同步 Jenkins 状态')
try {
const run = await platform.syncJenkinsRun(selectedRun.value)
selectedRunId.value = run.id
ElMessage.success('已同步 Jenkins 状态')
} catch (error) {
await handleRunActionError(error, 'Jenkins 状态同步失败')
}
}
async function retrySelectedRun() {
@@ -74,9 +79,13 @@ async function retrySelectedRun() {
return
}
const run = await platform.retryDeployRun(selectedRun.value)
selectedRunId.value = run.id
ElMessage.success('已提交重试发布单')
try {
const run = await platform.retryDeployRun(selectedRun.value)
selectedRunId.value = run.id
ElMessage.success('已提交重试发布单')
} catch (error) {
await handleRunActionError(error, '发布单重试失败')
}
}
async function syncAllJenkinsRuns() {
@@ -95,10 +104,28 @@ async function syncAllJenkinsRuns() {
? `已同步 ${result.syncedRuns.length} 个 Jenkins run`
: '当前没有需要同步的 Jenkins run',
)
} catch (error) {
await handleRunActionError(error, 'Jenkins 批量同步失败')
} finally {
syncingAll.value = false
}
}
async function handleRunActionError(error: unknown, fallbackMessage: string) {
if (isDevopsApiError(error) && error.code === 'CONFLICT') {
await platform.refreshRuns()
const stillSelected = platform.runs.find((run) => run.id === selectedRunId.value)
if (!stillSelected && platform.runs[0]) {
selectedRunId.value = platform.runs[0].id
}
ElMessage.warning('发布单状态已变化,已刷新最新记录')
return
}
ElMessage.error(error instanceof Error ? error.message : fallbackMessage)
}
</script>
<template>