feat: show signed release changelog in update history
Release / release (push) Canceled after 0s

This commit is contained in:
Qiufeng
2026-08-19 08:27:08 +08:00
parent fae8225299
commit 6b04d8dd0a
15 changed files with 190 additions and 243 deletions
@@ -111,9 +111,9 @@
<div class="section-heading">
<div>
<h2>历史更新记录</h2>
<p>按版本汇总获取、下载、重启安装和最终执行结果。</p>
<p>按版本展示签名 Release 提供的更新日志与最终结果。</p>
</div>
<span>最近 {{ historyTimeline.length }} 个版本 · {{ historyRows.length }} 条记录</span>
<span>已记录 {{ historyTimeline.length }} 个版本</span>
</div>
<t-alert v-if="historyError" theme="error" :close-btn="false">
@@ -146,37 +146,19 @@
<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 class="timeline-card__notes">
<span>更新日志</span>
<p>{{ item.releaseNotes || '该版本的签名 Release 未提供更新日志。' }}</p>
</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>
<p class="timeline-card__reason">{{ item.reason || '更新结果已记录。' }}</p>
<div class="timeline-card__published">
<span v-if="item.publishedAt">发布时间:{{ formatDateTime(item.publishedAt) }}</span>
<span>记录时间:{{ formatDateTime(item.occurredAt) }} · {{ item.username || '系统' }}</span>
</div>
<div class="timeline-card__footer">
<span>最后更新:{{ formatDateTime(item.occurredAt) }}</span>
<t-button variant="text" size="small" @click="openHistoryDetail(item)">查看完整记录</t-button>
<span>目标版本 {{ item.version }}</span>
<t-button variant="text" size="small" @click="openHistoryDetail(item)">查看更新日志</t-button>
</div>
</div>
</article>
@@ -283,23 +265,17 @@
{{ 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 class="history-detail__metadata">
<span v-if="selectedHistory.publishedAt">发布时间:{{ formatDateTime(selectedHistory.publishedAt) }}</span>
<span
>记录时间:{{ formatDateTime(selectedHistory.occurredAt) }} · {{ selectedHistory.username || '系统' }}</span
>
</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 class="history-detail__notes">
<span>更新日志</span>
<p>{{ selectedHistory.releaseNotes || '该版本的签名 Release 未提供更新日志。' }}</p>
</div>
<p class="history-detail__reason">{{ selectedHistory.reason || '更新结果已记录。' }}</p>
</div>
</t-dialog>
</div>
@@ -322,10 +298,6 @@ import {
defineOptions({ name: 'SystemUpdatePage' });
type TagTheme = 'default' | 'primary' | 'success' | 'warning' | 'danger';
interface HistoryTimelineStep {
row: AuditLog;
}
interface HistoryTimelineItem {
key: string;
version: string;
@@ -333,9 +305,8 @@ interface HistoryTimelineItem {
username: string;
resultCode: string;
reason: string;
operations: string[];
releaseNotes: string;
steps: HistoryTimelineStep[];
publishedAt: string;
}
const UPDATE_PENDING_KEY = 'kaidi-system-update-pending';
@@ -498,9 +469,8 @@ const historyTimeline = computed<HistoryTimelineItem[]>(() => {
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) || '';
const publishedAt = newestFirst.map((row) => historyPublishedAt(row)).find(Boolean) || '';
return {
key,
@@ -509,9 +479,8 @@ const historyTimeline = computed<HistoryTimelineItem[]>(() => {
username: representative?.username || '',
resultCode: representative?.resultCode || 'UNKNOWN',
reason: representative?.reason || '',
operations,
releaseNotes,
steps: chronological.map((row) => ({ row })),
publishedAt,
};
})
.sort((left, right) => compareHistoryDates(right.occurredAt, left.occurredAt));
@@ -571,7 +540,7 @@ async function loadHistory(silent = false) {
objectType: 'SYSTEM_UPDATE',
sort: 'occurredAt,desc',
page: 1,
size: 20,
size: 100,
});
historyRows.value = result.items;
} catch (error) {
@@ -859,7 +828,24 @@ function historyVersion(row: AuditLog) {
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;
for (const key of ['releaseNotes', 'changelog', 'updateLog', 'notes']) {
const value = parsed?.[key];
if (typeof value === 'string' && value.trim()) return value.trim();
if (Array.isArray(value)) {
const lines = value
.filter((item): item is string => typeof item === 'string' && Boolean(item.trim()))
.map((item) => item.trim());
if (lines.length) return lines.join('\n');
}
}
}
return '';
}
function historyPublishedAt(row: AuditLog) {
for (const source of [row.afterJson, row.beforeJson]) {
const parsed = parseHistoryPayload(source);
if (typeof parsed?.publishedAt === 'string' && parsed.publishedAt.trim()) return parsed.publishedAt;
}
return '';
}
@@ -869,19 +855,6 @@ function openHistoryDetail(item: HistoryTimelineItem) {
historyDetailVisible.value = true;
}
function actionLabel(action: string) {
return (
{
SYSTEM_UPDATE_CHECK: '获取版本',
SYSTEM_UPDATE_DOWNLOAD_REQUEST: '下载更新包',
SYSTEM_UPDATE_REQUEST: '立即更新并重启',
SYSTEM_UPDATE_SUCCEEDED: '更新完成',
SYSTEM_UPDATE_FAILED: '更新失败',
SYSTEM_UPDATE_RECOVERY_REQUIRED: '更新需人工恢复',
}[action] || action
);
}
function resultLabel(result: string) {
return { SUCCESS: '成功', FAILED: '失败', DENIED: '已拒绝', BLOCKED: '已阻断' }[result] || result;
}
@@ -1155,9 +1128,7 @@ onBeforeUnmount(() => {
}
.timeline-card__header,
.timeline-card__summary,
.timeline-card__footer,
.timeline-step__heading,
.history-detail__header {
display: flex;
align-items: center;
@@ -1188,35 +1159,13 @@ onBeforeUnmount(() => {
}
.timeline-card__identity span,
.timeline-card__count,
.timeline-card__footer,
.timeline-step__heading time,
.history-detail__header span,
.history-detail__step time {
.history-detail__metadata {
color: var(--td-text-color-secondary);
font-size: 12px;
}
.timeline-card__summary {
flex-wrap: wrap;
margin-top: 12px;
padding: 9px 10px;
color: var(--td-text-color-primary);
font-size: 13px;
line-height: 20px;
background: var(--td-bg-color-secondarycontainer);
border-radius: 4px;
}
.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: 12px 0 0;
@@ -1250,65 +1199,15 @@ onBeforeUnmount(() => {
overflow-wrap: anywhere;
}
.timeline-steps {
.timeline-card__published,
.history-detail__metadata {
display: flex;
flex-direction: column;
gap: 10px;
margin-top: 14px;
}
.timeline-step {
display: grid;
grid-template-columns: 10px minmax(0, 1fr);
gap: 10px;
min-width: 0;
}
.timeline-step__dot {
width: 8px;
height: 8px;
margin-top: 7px;
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: 13px;
font-weight: 600;
}
.timeline-step__heading time {
margin-left: auto;
white-space: nowrap;
}
.timeline-step__body p {
margin: 3px 0 0;
gap: 6px 16px;
margin-top: 10px;
color: var(--td-text-color-secondary);
font-size: 12px;
line-height: 20px;
overflow-wrap: anywhere;
}
.timeline-card__footer {
@@ -1336,53 +1235,6 @@ onBeforeUnmount(() => {
line-height: 26px;
}
.history-detail__steps {
display: flex;
flex-direction: column;
gap: 12px;
max-height: 420px;
padding: 12px;
overflow: auto;
background: var(--td-bg-color-secondarycontainer);
border: 1px solid var(--td-component-border);
border-radius: 6px;
}
.history-detail__step {
padding-bottom: 12px;
border-bottom: 1px 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: 8px;
}
.history-detail__step strong {
color: var(--td-text-color-primary);
font-size: 13px;
}
.history-detail__step time {
display: block;
margin-top: 4px;
}
.history-detail__step p {
margin: 4px 0 0;
color: var(--td-text-color-secondary);
font-size: 13px;
line-height: 20px;
overflow-wrap: anywhere;
}
.alert-content {
justify-content: space-between;
gap: 12px;
@@ -1552,16 +1404,6 @@ onBeforeUnmount(() => {
padding: 12px;
}
.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: 1fr;