@@ -111,9 +111,9 @@
< div class = "section-heading" >
< div >
< h2 > 历史更新记录 < / h2 >
< p > 记录版本 获取、 下载 、 重启安装和最终执行结果 。 < / p >
< p > 按版本汇总 获取、 下载 、 重启安装和最终执行结果 。 < / p >
< / div >
< span > 最近 { { historyRows . length } } 条 < / span >
< span > 最近 { { historyTimeline . length } } 个版本 · { { historyRows . length } } 条记录 < / span >
< / div >
< t-alert v-if = "historyError" theme="error" :close-btn="false" >
@@ -122,23 +122,65 @@
< t-link theme = "primary" @click ="() => loadHistory()" > 重新加载 < / t -link >
< / div >
< / t-alert >
< t-table
v-if = "historyLoading || historyRows.length"
:columns = "historyColumns"
:data = "historyRows"
row -key = " publicId "
:loading = " historyLoad ing "
table -layout = " fixed "
:horizontal-scroll-affixed-bottom = "true "
>
< template # occurredAt = "{ row }" > { { formatDateTime ( row . occurredAt ) } } < / template >
< template # actionCode = "{ row }" > { { actionLabel ( row . actionCode ) } } < / template >
< template # version = "{ row }" > { { historyVersion ( row ) } } < / template >
< template # resultCode = "{ row }" >
< t-tag :theme = "resultTheme(row.resultCode)" variant = "light" > { { resultLabel ( row . resultCode ) } } < / t-tag >
< / template >
< template # reason = "{ row }" > { { row . reason || '-' } } < / template >
< / t-table >
< div v-if = "historyLoading" class="history-loading" aria-live="polite" >
< t -loading text = "正在加载历史更新记录" / >
< / div >
< div v-else-if = "historyTimeline.length" class="update-timeline" aria-label="按版本排列的更新时间线" >
< article
v-for = "item in historyTimel ine "
:key = "item.key"
class = "update-timeline__item "
:data-version = "item.version"
>
< div class = "update-timeline__rail" aria -hidden = " true " >
< span class = "update-timeline__marker" :class = "`update-timeline__marker--${resultTheme(item.resultCode)}`" >
< t-icon : name = "item.resultCode === 'SUCCESS' ? 'check' : 'error-circle'" / >
< / span >
< / div >
< div class = "timeline-card" >
< div class = "timeline-card__header" >
< div class = "timeline-card__identity" >
< strong > { { item . version } } < / strong >
< span > { { formatDateTime ( item . occurredAt ) } } · { { item . username || '系统' } } < / span >
< / div >
< t-tag :theme = "resultTheme(item.resultCode)" variant = "light" > { { resultLabel ( item . resultCode ) } } < / t-tag >
< / div >
< div class = "timeline-card__summary" >
< span class = "timeline-card__label" > 执行流程 < / span >
< span > { { item . operations . join ( ' · ' ) } } < / span >
< span class = "timeline-card__count" > { { item . steps . length } } 条记录 < / span >
< / div >
< p class = "timeline-card__reason" > { { item . reason || '已记录本次版本更新操作。' } } < / p >
< div v-if = "item.releaseNotes" class="timeline-card__notes" >
< span > 版本说明 < / span >
< p > { { item . releaseNotes } } < / p >
< / div >
< div class = "timeline-steps" aria -label = " 更新步骤 " >
< div v-for = "step in item.steps" :key="step.row.publicId" class="timeline-step" >
< span class = "timeline-step__dot" :class = "`timeline-step__dot--${resultTheme(step.row.resultCode)}`" / >
< div class = "timeline-step__body" >
< div class = "timeline-step__heading" >
< strong > { { actionLabel ( step . row . actionCode ) } } < / strong >
< t-tag size = "small" :theme = "resultTheme(step.row.resultCode)" variant = "light" >
{ { resultLabel ( step . row . resultCode ) } }
< / t-tag >
< time > { { formatDateTime ( step . row . occurredAt ) } } < / time >
< / div >
< p > { { step . row . reason || '操作已记录' } } · { { step . row . username || '系统' } } < / p >
< / div >
< / div >
< / div >
< div class = "timeline-card__footer" >
< span > 最后更新 : { { formatDateTime ( item . occurredAt ) } } < / span >
< t-button variant = "text" size = "small" @click ="openHistoryDetail(item)" > 查看完整记录 < / t -button >
< / div >
< / div >
< / article >
< / div >
< t-empty v-else description = "暂无系统更新记录" / >
< / section >
@@ -223,11 +265,47 @@
< / div >
< / div >
< / t-dialog >
< t-dialog
v -model :visible = "historyDetailVisible"
header = "更新日志详情"
aria -label = " 更新日志详情 "
width = "min(680px, calc(100vw - 32px))"
:footer = "false"
>
< div v-if = "selectedHistory" class="history-detail" role="dialog" aria-label="更新日志详情" aria-modal="true" >
< div class = "history-detail__header" >
< div >
< span > 目标版本 < / span >
< strong > { { selectedHistory . version } } < / strong >
< / div >
< t-tag :theme = "resultTheme(selectedHistory.resultCode)" variant = "light" >
{ { resultLabel ( selectedHistory . resultCode ) } }
< / t-tag >
< / div >
< p class = "history-detail__reason" > { { selectedHistory . reason || '已记录本次版本更新操作。' } } < / p >
< div v-if = "selectedHistory.releaseNotes" class="history-detail__notes" >
< span > 版本说明 < / span >
< p > { { selectedHistory . releaseNotes } } < / p >
< / div >
< div class = "history-detail__steps" aria -label = " 更新日志 " >
< div v-for = "step in selectedHistory.steps" :key="step.row.publicId" class="history-detail__step" >
< div >
< strong > { { actionLabel ( step . row . actionCode ) } } < / strong >
< t-tag size = "small" :theme = "resultTheme(step.row.resultCode)" variant = "light" >
{ { resultLabel ( step . row . resultCode ) } }
< / t-tag >
< / div >
< time > { { formatDateTime ( step . row . occurredAt ) } } · { { step . row . username || '系统' } } < / time >
< p > { { step . row . reason || '操作已记录' } } < / p >
< / div >
< / div >
< / div >
< / t-dialog >
< / div >
< / template >
< script setup lang = "ts" >
import { isAxiosError } from 'axios' ;
import type { PrimaryTableCol } from 'tdesign-vue-next' ;
import { MessagePlugin } from 'tdesign-vue-next' ;
import { computed , onBeforeUnmount , onMounted , ref } from 'vue' ;
@@ -244,6 +322,22 @@ import {
defineOptions ( { name : 'SystemUpdatePage' } ) ;
type TagTheme = 'default' | 'primary' | 'success' | 'warning' | 'danger' ;
interface HistoryTimelineStep {
row : AuditLog ;
}
interface HistoryTimelineItem {
key : string ;
version : string ;
occurredAt : string ;
username : string ;
resultCode : string ;
reason : string ;
operations : string [ ] ;
releaseNotes : string ;
steps : HistoryTimelineStep [ ] ;
}
const UPDATE _PENDING _KEY = 'kaidi-system-update-pending' ;
const updateStatus = ref < SystemUpdateView | null > ( null ) ;
@@ -259,6 +353,8 @@ const historyError = ref('');
const updateRefreshSeconds = ref < number | null > ( null ) ;
const restartCountdown = ref < number | null > ( null ) ;
const updateDialogVisible = ref ( false ) ;
const historyDetailVisible = ref ( false ) ;
const selectedHistory = ref < HistoryTimelineItem | null > ( null ) ;
const connectionInterrupted = ref ( false ) ;
const localEvents = ref < SystemUpdateEvent [ ] > ( [ ] ) ;
const awaitingRefresh = ref ( false ) ;
@@ -387,14 +483,39 @@ const updateStateTheme = computed<TagTheme>(() => {
return 'default' ;
} ) ;
const historyColumns : PrimaryTableCol [ ] = [
{ colKey : 'occurredAt' , title : '操作时间' , width : 180 } ,
{ colKey : 'version' , title : '版本' , width : 170 , ellipsis : true } ,
{ colKey : 'actionCode' , title : '操作' , width : 150 } ,
{ colKey : 'username' , title : '操作人' , width : 130 , ellipsis : true } ,
{ colKey : 'resultCode' , title : '结果' , width : 100 } ,
{ colKey : 'reason' , title : '说明' , minWidth : 220 , ellipsis : true } ,
] ;
const historyTimeline = computed < HistoryTimelineItem [ ] > ( ( ) => {
const grouped = new Map < string , AuditLog [ ] > ( ) ;
for ( const row of historyRows . value ) {
const version = historyVersion ( row ) ;
const key = version === '-' ? ` unknown: ${ row . requestId || row . publicId } ` : ` version: ${ version } ` ;
const rows = grouped . get ( key ) || [ ] ;
rows . push ( row ) ;
grouped . set ( key , rows ) ;
}
return [ ... grouped . entries ( ) ]
. map ( ( [ key , rows ] ) => {
const newestFirst = rows . slice ( ) . sort ( ( left , right ) => compareHistoryDates ( right . occurredAt , left . occurredAt ) ) ;
const terminal = newestFirst . find ( ( row ) => isTerminalHistoryAction ( row . actionCode ) ) ;
const representative = terminal || newestFirst [ 0 ] ;
const chronological = newestFirst . slice ( ) . reverse ( ) ;
const operations = [ ... new Set ( chronological . map ( ( row ) => actionLabel ( row . actionCode ) ) . filter ( Boolean ) ) ] ;
const releaseNotes = newestFirst . map ( ( row ) => historyReleaseNotes ( row ) ) . find ( Boolean ) || '' ;
return {
key ,
version : representative ? historyVersion ( representative ) : '-' ,
occurredAt : representative ? . occurredAt || '' ,
username : representative ? . username || '' ,
resultCode : representative ? . resultCode || 'UNKNOWN' ,
reason : representative ? . reason || '' ,
operations ,
releaseNotes ,
steps : chronological . map ( ( row ) => ( { row } ) ) ,
} ;
} )
. sort ( ( left , right ) => compareHistoryDates ( right . occurredAt , left . occurredAt ) ) ;
} ) ;
async function loadStatus ( silent = false ) {
if ( ! silent ) {
@@ -705,20 +826,49 @@ function eventStageLabel(stage: string) {
) ;
}
function compareHistoryDates ( left : string , right : string ) {
const leftTime = Date . parse ( left ) ;
const rightTime = Date . parse ( right ) ;
if ( Number . isNaN ( leftTime ) || Number . isNaN ( rightTime ) ) return left . localeCompare ( right ) ;
return leftTime - rightTime ;
}
function isTerminalHistoryAction ( action : string ) {
return [ 'SYSTEM_UPDATE_SUCCEEDED' , 'SYSTEM_UPDATE_FAILED' , 'SYSTEM_UPDATE_RECOVERY_REQUIRED' ] . includes ( action ) ;
}
function parseHistoryPayload ( source ? : string | null ) {
if ( ! source ) return null ;
try {
const parsed : unknown = JSON . parse ( source ) ;
return parsed && typeof parsed === 'object' && ! Array . isArray ( parsed ) ? ( parsed as Record < string , unknown > ) : null ;
} catch {
return null ;
}
}
function historyVersion ( row : AuditLog ) {
for ( const source of [ row . afterJson , row . beforeJson ] ) {
if ( ! source ) continue ;
try {
const parsed = JSON . parse ( source ) as Record < string , unknown > ;
const version = parsed . targetVersion || parsed . latestVersion || parsed . currentVersion || parsed . version ;
if ( version ) return String ( version ) ;
} catch {
continue ;
}
const parsed = parseHistoryPayload ( source ) ;
const version = parsed ? . targetVersion || parsed ? . latestVersion || parsed ? . currentVersion || parsed ? . version ;
if ( version !== undefined && version !== null && String ( version ) . trim ( ) ) return String ( version ) ;
}
return '-' ;
}
function historyReleaseNotes ( row : AuditLog ) {
for ( const source of [ row . afterJson , row . beforeJson ] ) {
const parsed = parseHistoryPayload ( source ) ;
if ( typeof parsed ? . releaseNotes === 'string' && parsed . releaseNotes . trim ( ) ) return parsed . releaseNotes ;
}
return '' ;
}
function openHistoryDetail ( item : HistoryTimelineItem ) {
selectedHistory . value = item ;
historyDetailVisible . value = true ;
}
function actionLabel ( action : string ) {
return (
{
@@ -926,6 +1076,313 @@ onBeforeUnmount(() => {
line - height : 24 px ;
}
. history - loading {
display : flex ;
justify - content : center ;
min - height : 160 px ;
padding : 32 px 0 ;
}
. update - timeline {
display : flex ;
flex - direction : column ;
gap : 18 px ;
}
. update - timeline _ _item {
display : grid ;
grid - template - columns : 24 px minmax ( 0 , 1 fr ) ;
gap : 14 px ;
min - width : 0 ;
}
. update - timeline _ _rail {
position : relative ;
display : flex ;
justify - content : center ;
}
. update - timeline _ _rail : : after {
position : absolute ;
top : 30 px ;
bottom : - 18 px ;
width : 1 px ;
background : var ( -- td - component - border ) ;
content : '' ;
}
. update - timeline _ _item : last - child . update - timeline _ _rail : : after {
display : none ;
}
. update - timeline _ _marker {
z - index : 1 ;
display : inline - flex ;
align - items : center ;
justify - content : center ;
width : 24 px ;
height : 24 px ;
color : var ( -- td - text - color - secondary ) ;
background : var ( -- td - bg - color - container ) ;
border : 2 px solid var ( -- td - component - border ) ;
border - radius : 50 % ;
}
. update - timeline _ _marker -- success {
color : # fff ;
background : var ( -- td - success - color ) ;
border - color : var ( -- td - success - color ) ;
}
. update - timeline _ _marker -- danger {
color : # fff ;
background : var ( -- td - error - color ) ;
border - color : var ( -- td - error - color ) ;
}
. update - timeline _ _marker -- warning {
color : # fff ;
background : var ( -- td - warning - color ) ;
border - color : var ( -- td - warning - color ) ;
}
. timeline - card {
min - width : 0 ;
padding : 16 px ;
background : var ( -- td - bg - color - container ) ;
border : 1 px solid var ( -- td - component - border ) ;
border - radius : 6 px ;
}
. timeline - card _ _header ,
. timeline - card _ _summary ,
. timeline - card _ _footer ,
. timeline - step _ _heading ,
. history - detail _ _header {
display : flex ;
align - items : center ;
gap : 10 px ;
min - width : 0 ;
}
. timeline - card _ _header ,
. timeline - card _ _footer ,
. history - detail _ _header {
justify - content : space - between ;
}
. timeline - card _ _identity {
display : flex ;
flex - direction : column ;
gap : 3 px ;
min - width : 0 ;
}
. timeline - card _ _identity strong {
overflow : hidden ;
color : var ( -- td - text - color - primary ) ;
font - size : 16 px ;
line - height : 24 px ;
text - overflow : ellipsis ;
white - space : nowrap ;
}
. timeline - card _ _identity span ,
. timeline - card _ _count ,
. timeline - card _ _footer ,
. timeline - step _ _heading time ,
. history - detail _ _header span ,
. history - detail _ _step time {
color : var ( -- td - text - color - secondary ) ;
font - size : 12 px ;
}
. timeline - card _ _summary {
flex - wrap : wrap ;
margin - top : 12 px ;
padding : 9 px 10 px ;
color : var ( -- td - text - color - primary ) ;
font - size : 13 px ;
line - height : 20 px ;
background : var ( -- td - bg - color - secondarycontainer ) ;
border - radius : 4 px ;
}
. timeline - card _ _label {
color : var ( -- td - text - color - secondary ) ;
}
. timeline - card _ _count {
margin - left : auto ;
white - space : nowrap ;
}
. timeline - card _ _reason ,
. history - detail _ _reason {
margin : 12 px 0 0 ;
color : var ( -- td - text - color - primary ) ;
line - height : 22 px ;
overflow - wrap : anywhere ;
}
. timeline - card _ _notes ,
. history - detail _ _notes {
display : grid ;
grid - template - columns : 72 px minmax ( 0 , 1 fr ) ;
gap : 10 px ;
margin - top : 12 px ;
padding - top : 12 px ;
border - top : 1 px solid var ( -- td - component - border ) ;
}
. timeline - card _ _notes > span ,
. history - detail _ _notes > span {
color : var ( -- td - text - color - secondary ) ;
font - size : 12 px ;
}
. timeline - card _ _notes p ,
. history - detail _ _notes p {
margin : 0 ;
color : var ( -- td - text - color - primary ) ;
line - height : 22 px ;
white - space : pre - wrap ;
overflow - wrap : anywhere ;
}
. timeline - steps {
display : flex ;
flex - direction : column ;
gap : 10 px ;
margin - top : 14 px ;
}
. timeline - step {
display : grid ;
grid - template - columns : 10 px minmax ( 0 , 1 fr ) ;
gap : 10 px ;
min - width : 0 ;
}
. timeline - step _ _dot {
width : 8 px ;
height : 8 px ;
margin - top : 7 px ;
background : var ( -- td - component - border ) ;
border - radius : 50 % ;
}
. timeline - step _ _dot -- success {
background : var ( -- td - success - color ) ;
}
. timeline - step _ _dot -- danger {
background : var ( -- td - error - color ) ;
}
. timeline - step _ _dot -- warning {
background : var ( -- td - warning - color ) ;
}
. timeline - step _ _body {
min - width : 0 ;
}
. timeline - step _ _heading {
flex - wrap : wrap ;
}
. timeline - step _ _heading strong {
color : var ( -- td - text - color - primary ) ;
font - size : 13 px ;
font - weight : 600 ;
}
. timeline - step _ _heading time {
margin - left : auto ;
white - space : nowrap ;
}
. timeline - step _ _body p {
margin : 3 px 0 0 ;
color : var ( -- td - text - color - secondary ) ;
font - size : 12 px ;
line - height : 20 px ;
overflow - wrap : anywhere ;
}
. timeline - card _ _footer {
margin - top : 14 px ;
padding - top : 10 px ;
border - top : 1 px solid var ( -- td - component - border ) ;
}
. history - detail {
display : flex ;
flex - direction : column ;
gap : 14 px ;
min - width : 0 ;
}
. history - detail _ _header > div {
display : flex ;
flex - direction : column ;
gap : 4 px ;
}
. history - detail _ _header strong {
color : var ( -- td - text - color - primary ) ;
font - size : 18 px ;
line - height : 26 px ;
}
. history - detail _ _steps {
display : flex ;
flex - direction : column ;
gap : 12 px ;
max - height : 420 px ;
padding : 12 px ;
overflow : auto ;
background : var ( -- td - bg - color - secondarycontainer ) ;
border : 1 px solid var ( -- td - component - border ) ;
border - radius : 6 px ;
}
. history - detail _ _step {
padding - bottom : 12 px ;
border - bottom : 1 px solid var ( -- td - component - border ) ;
}
. history - detail _ _step : last - child {
padding - bottom : 0 ;
border - bottom : 0 ;
}
. history - detail _ _step > div {
display : flex ;
align - items : center ;
flex - wrap : wrap ;
gap : 8 px ;
}
. history - detail _ _step strong {
color : var ( -- td - text - color - primary ) ;
font - size : 13 px ;
}
. history - detail _ _step time {
display : block ;
margin - top : 4 px ;
}
. history - detail _ _step p {
margin : 4 px 0 0 ;
color : var ( -- td - text - color - secondary ) ;
font - size : 13 px ;
line - height : 20 px ;
overflow - wrap : anywhere ;
}
. alert - content {
justify - content : space - between ;
gap : 12 px ;
@@ -1077,6 +1534,40 @@ onBeforeUnmount(() => {
gap : 8 px ;
}
. update - timeline _ _item {
grid - template - columns : 18 px minmax ( 0 , 1 fr ) ;
gap : 8 px ;
}
. update - timeline _ _marker {
width : 18 px ;
height : 18 px ;
}
. update - timeline _ _rail : : after {
top : 24 px ;
}
. timeline - card {
padding : 12 px ;
}
. timeline - card _ _count {
width : 100 % ;
margin - left : 0 ;
}
. timeline - step _ _heading time {
width : 100 % ;
margin - left : 0 ;
}
. timeline - card _ _notes ,
. history - detail _ _notes {
grid - template - columns : 1 fr ;
gap : 4 px ;
}
. download - progress _ _meta ,
. dialog - status - line ,
. dialog - actions {