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:
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -351,6 +351,7 @@ export interface PipelineStep {
|
||||
|
||||
export interface ReleaseRun {
|
||||
id: string
|
||||
version: number
|
||||
projectKey: string
|
||||
projectName: string
|
||||
environment: EnvironmentName
|
||||
|
||||
@@ -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
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user