feat: split online update into explicit stages
Release / release (push) Canceled after 0s

This commit is contained in:
Qiufeng
2026-08-18 09:01:34 +08:00
parent 3c7847c8f5
commit 6d4ae00ae0
5 changed files with 246 additions and 84 deletions
@@ -29,30 +29,40 @@
<div class="update-actions">
<t-button
variant="outline"
:loading="fetchingUpdate"
:disabled="!canFetchUpdate || updateBusy || restarting"
@click="fetchUpdate"
:loading="checkingUpdate"
:disabled="!canCheckUpdate || checkingUpdate || downloadingUpdate || installingUpdate"
@click="checkLatest"
>
<template #icon><t-icon name="cloud-download" /></template>
获取更新
<template #icon><t-icon name="refresh" /></template>
获取版本
</t-button>
<t-button
v-if="canRestart"
v-if="canDownloadUpdate"
theme="primary"
:loading="restarting"
:disabled="updateBusy || fetchingUpdate"
@click="restartNow"
:loading="downloadingUpdate"
:disabled="checkingUpdate || downloadingUpdate || installingUpdate"
@click="downloadNow"
>
<template #icon><t-icon name="cloud-download" /></template>
立即更新
</t-button>
<t-button
v-if="canInstall"
theme="primary"
:loading="installingUpdate"
:disabled="checkingUpdate || downloadingUpdate || installingUpdate"
@click="installNow"
>
<template #icon><t-icon name="poweroff" /></template>
立即重启
立即更新并重启
</t-button>
</div>
</div>
<t-steps :current="updateStep" :layout="stepLayout" status="process" class="update-steps">
<t-step-item title="获取版本" content="检查最新发布" />
<t-step-item title="下载校验" content="下载并验证签名" />
<t-step-item title="立即重启" content="备份并切换版本" />
<t-step-item title="下载更新" content="下载并验证签名" />
<t-step-item title="安装重启" content="备份并切换版本" />
<t-step-item title="完成更新" content="健康检查后刷新" />
</t-steps>
@@ -66,7 +76,11 @@
新版本已通过健康检查,页面将在 {{ updateRefreshSeconds }} 秒后自动刷新。
</t-alert>
<t-alert v-else :theme="updateStatus?.enabled === false ? 'warning' : 'info'" :close-btn="false">
{{ updateStatus?.message || '正在读取系统版本状态' }}
<div class="update-status-message">
<strong>{{ updateStateLabel }}</strong>
<span>{{ updateStatus?.message || '正在读取系统版本状态' }}</span>
<small v-if="updateActionHint">{{ updateActionHint }}</small>
</div>
</t-alert>
<div v-if="updateStatus?.releaseNotes" class="release-notes">
@@ -133,8 +147,9 @@ type TagTheme = 'default' | 'primary' | 'success' | 'warning' | 'danger';
const updateStatus = ref<SystemUpdateView | null>(null);
const isNarrow = ref(false);
const statusLoading = ref(false);
const fetchingUpdate = ref(false);
const restarting = ref(false);
const checkingUpdate = ref(false);
const downloadingUpdate = ref(false);
const installingUpdate = ref(false);
const statusError = ref('');
const historyRows = ref<AuditLog[]>([]);
const historyLoading = ref(false);
@@ -167,14 +182,27 @@ const currentIsLatest = computed(
!updateBusy.value &&
!['FAILED', 'UNKNOWN'].includes(updateStatus.value.state),
);
const canFetchUpdate = computed(
const canCheckUpdate = computed(
() =>
updateStatus.value?.enabled !== false &&
updateStatus.value?.state !== 'READY' &&
(updateStatus.value?.allowedActions.includes('CHECK') === true ||
updateStatus.value?.allowedActions.includes('DOWNLOAD') === true),
!updateBusy.value &&
updateStatus.value?.allowedActions.includes('CHECK') === true,
);
const canDownloadUpdate = computed(
() =>
updateStatus.value?.enabled !== false &&
!updateBusy.value &&
Boolean(updateStatus.value?.latestVersion) &&
updateStatus.value?.updateAvailable === true &&
updateStatus.value?.allowedActions.includes('DOWNLOAD') === true,
);
const canInstall = computed(
() =>
updateStatus.value?.enabled !== false &&
!updateBusy.value &&
updateStatus.value?.state === 'READY' &&
updateStatus.value?.allowedActions.includes('INSTALL') === true,
);
const canRestart = computed(() => updateStatus.value?.allowedActions.includes('INSTALL') === true);
const updateStep = computed(() => {
const state = updateStatus.value?.state || 'IDLE';
if (state === 'SUCCEEDED' || state === 'CURRENT' || currentIsLatest.value) return 3;
@@ -190,11 +218,11 @@ const updateStateLabel = computed(() => {
DISABLED: '未启用',
CURRENT: '已是最新',
QUEUED: '已排队',
DOWNLOAD_QUEUED: '等待下载',
DOWNLOAD_QUEUED: '下载已排队',
INSTALL_QUEUED: '等待重启',
VERIFYING: '校验中',
DOWNLOADING: '下载中',
READY: '等待重启',
READY: '下载完成',
BACKING_UP: '备份中',
INSTALLING: '安装中',
RUNNING: '重启中',
@@ -206,6 +234,20 @@ const updateStateLabel = computed(() => {
'待获取'
);
});
const updateActionHint = computed(() => {
const state = updateStatus.value?.state || 'IDLE';
if (state === 'READY') {
return canInstall.value ? '下载完成,请点击“立即更新并重启”安装新版本。' : '下载完成,但当前账号没有安装权限。';
}
if (canDownloadUpdate.value) return '已发现新版本,请点击“立即更新”开始下载。';
if (updateStatus.value?.updateAvailable) return '已发现新版本,但当前账号没有下载权限。';
if (['DOWNLOAD_QUEUED', 'VERIFYING', 'DOWNLOADING'].includes(state)) return '更新包正在下载和校验,请稍候。';
if (['INSTALL_QUEUED', 'BACKING_UP', 'INSTALLING', 'RUNNING'].includes(state)) {
return '更新包已确认,系统正在备份、切换并重启。';
}
if (state === 'IDLE' && !updateStatus.value?.latestVersion) return '点击“获取版本”检查最新发布。';
return '';
});
const updateStateTheme = computed<TagTheme>(() => {
const state = updateStatus.value?.state || 'IDLE';
if (['SUCCEEDED', 'CURRENT', 'READY'].includes(state) || currentIsLatest.value) return 'success';
@@ -233,7 +275,7 @@ async function loadStatus(silent = false) {
updateStatus.value = status;
if (installStates.has(status.state)) awaitingRefresh.value = true;
handleCompletion(status);
if (busyStates.has(status.state)) startPolling();
if (busyStates.has(status.state) || downloadingUpdate.value || installingUpdate.value) startPolling();
else stopPolling();
} catch (error) {
if (!silent) statusError.value = friendlyError(error, '系统版本状态读取失败');
@@ -266,9 +308,9 @@ async function refreshPage() {
await Promise.all([loadStatus(), loadHistory()]);
}
async function fetchUpdate() {
if (fetchingUpdate.value || updateBusy.value || !canFetchUpdate.value) return;
fetchingUpdate.value = true;
async function checkLatest() {
if (checkingUpdate.value || updateBusy.value || !canCheckUpdate.value) return;
checkingUpdate.value = true;
statusError.value = '';
try {
const checked = await checkSystemUpdate();
@@ -278,36 +320,61 @@ async function fetchUpdate() {
MessagePlugin.info('当前已是最新版本');
return;
}
if (!checked.latestVersion || !checked.allowedActions.includes('DOWNLOAD')) {
throw new Error('发现新版本,但当前账号没有下载更新包的权限');
if (checked.state === 'READY') {
MessagePlugin.success(
checked.allowedActions.includes('INSTALL')
? '更新包已下载完成,请点击“立即更新并重启”安装新版本'
: '更新包已下载完成,但当前账号没有安装权限',
);
} else if (!checked.allowedActions.includes('DOWNLOAD')) {
MessagePlugin.info('发现新版本,但当前账号没有下载权限');
} else {
MessagePlugin.success(`发现新版本 ${checked.latestVersion},请点击“立即更新”开始下载`);
}
updateStatus.value = await downloadSystemUpdate(checked.latestVersion);
await loadHistory(true);
MessagePlugin.success('更新包已开始下载,下载和签名校验完成后可立即重启');
startPolling();
} catch (error) {
statusError.value = friendlyError(error, '获取更新失败');
statusError.value = friendlyError(error, '获取版本失败');
} finally {
fetchingUpdate.value = false;
checkingUpdate.value = false;
}
}
async function restartNow() {
async function downloadNow() {
const version = updateStatus.value?.latestVersion;
if (!version || restarting.value || !canRestart.value) return;
restarting.value = true;
if (!version || downloadingUpdate.value || !canDownloadUpdate.value) return;
downloadingUpdate.value = true;
statusError.value = '';
startPolling();
try {
updateStatus.value = await downloadSystemUpdate(version);
MessagePlugin.success('更新包已开始下载,完成后可点击“立即更新并重启”');
void loadHistory(true);
} catch (error) {
statusError.value = friendlyError(error, '下载更新包失败');
void loadStatus(true);
} finally {
downloadingUpdate.value = false;
startPolling();
}
}
async function installNow() {
const version = updateStatus.value?.latestVersion;
if (!version || installingUpdate.value || !canInstall.value) return;
installingUpdate.value = true;
statusError.value = '';
stopRefreshCountdown();
awaitingRefresh.value = true;
startPolling();
try {
updateStatus.value = await installSystemUpdate(version, '管理员确认立即重启');
awaitingRefresh.value = true;
MessagePlugin.success('重启任务已提交,系统恢复后页面将自动刷新');
await loadHistory(true);
startPolling();
updateStatus.value = await installSystemUpdate(version, '管理员确认立即更新并重启');
MessagePlugin.success('更新任务已提交,系统完成切换并重启后页面将自动刷新');
void loadHistory(true);
} catch (error) {
statusError.value = friendlyError(error, '立即重启失败');
statusError.value = friendlyError(error, '立即更新并重启失败');
void loadStatus(true);
} finally {
restarting.value = false;
installingUpdate.value = false;
startPolling();
}
}
@@ -324,6 +391,7 @@ function stopPolling() {
function handleCompletion(status: SystemUpdateView) {
if (!awaitingRefresh.value) return;
if (status.state === 'SUCCEEDED') {
statusError.value = '';
awaitingRefresh.value = false;
startRefreshCountdown();
void loadHistory(true);
@@ -374,7 +442,7 @@ function actionLabel(action: string) {
{
SYSTEM_UPDATE_CHECK: '获取版本',
SYSTEM_UPDATE_DOWNLOAD_REQUEST: '下载更新包',
SYSTEM_UPDATE_REQUEST: '立即重启',
SYSTEM_UPDATE_REQUEST: '立即更新并重启',
}[action] || action
);
}
@@ -503,6 +571,17 @@ onBeforeUnmount(() => {
border-top: 1px solid var(--td-component-border);
}
.update-status-message {
display: flex;
flex-wrap: wrap;
align-items: baseline;
gap: 4px 10px;
}
.update-status-message small {
color: var(--td-text-color-secondary);
}
.release-notes span,
.section-heading p,
.section-heading > span {