Files
erp_sb/electron-vue-template/src/renderer/components/common/UpdateDialog.vue
2025-09-30 17:16:11 +08:00

501 lines
11 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div>
<div class="version-info" @click="autoCheck">v{{ version || '-' }}</div>
<el-dialog v-model="show" width="522px" :close-on-click-modal="false" align-center class="update-dialog"
:title="stage === 'downloading' ? `正在更新 ${appName}` : '软件更新'">
<div v-if="stage === 'check'" class="update-content">
<div class="update-layout">
<div class="left-pane">
<img src="/icon/icon.png" class="app-icon app-icon-large" alt="App Icon"/>
</div>
<div class="right-pane">
<p class="announce">新版本的"{{ appName }}"已经发布</p>
<p class="desc">{{ appName }} {{ info.latestVersion }} 可供安装您现在的版本是 {{
version
}}要现在安装吗</p>
<div class="update-details form">
<h4>更新信息</h4>
<el-input
v-model="info.updateNotes"
type="textarea"
class="notes-box"
:rows="6"
readonly
resize="none"/>
</div>
<div class="update-actions row">
<div class="update-buttons">
<div class="left-actions">
<el-button size="small" @click="show=false">跳过这个版本</el-button>
</div>
<div class="right-actions">
<el-button size="small" @click="show=false">稍后提醒</el-button>
<el-button size="small" type="primary" @click="start">下载更新</el-button>
</div>
</div>
</div>
</div>
</div>
</div>
<div v-else-if="stage === 'downloading'" class="update-content">
<div class="download-main">
<div class="download-icon">
<img src="/icon/icon.png" class="app-icon" alt="App Icon"/>
</div>
<div class="download-content">
<div class="download-info">
<p>正在下载更新</p>
</div>
<div class="download-progress">
<el-progress
:percentage="prog.percentage"
:show-text="false"
:stroke-width="6"
color="#409EFF"/>
<div class="progress-details">
<span style="font-weight: 500">{{ prog.current }} / {{ prog.total }}</span>
<el-button size="small" @click="cancelDownload">取消</el-button>
</div>
</div>
</div>
</div>
</div>
<div v-else-if="stage === 'completed'" class="update-content">
<div class="update-header text-center">
<img src="/icon/icon.png" class="app-icon" alt="App Icon"/>
<h3>更新完成</h3>
<p>更新文件已下载将在重启后自动应用</p>
</div>
<div class="download-progress">
<div class="progress-info">
<span>{{ prog.current }} / {{ prog.total }}</span>
</div>
<el-progress
:percentage="100"
:show-text="false"
:stroke-width="6"
color="#67C23A"/>
</div>
<div class="update-buttons">
<el-button @click="cancelDownload">稍后更新</el-button>
<el-button type="primary" @click="installUpdate">重启应用新版本</el-button>
</div>
</div>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import {ref, computed, onMounted, onUnmounted} from 'vue'
import {ElMessage, ElMessageBox} from 'element-plus'
import {updateApi} from '../../api/update'
const props = defineProps<{ modelValue: boolean }>()
const emit = defineEmits<{ 'update:modelValue': [value: boolean] }>()
const show = computed({
get: () => props.modelValue,
set: (value) => emit('update:modelValue', value)
})
type Stage = 'check' | 'downloading' | 'completed'
const stage = ref<Stage>('check')
const appName = ref('我了个电商')
const version = ref('')
const prog = ref({percentage: 0, current: '0 MB', total: '0 MB', speed: ''})
const info = ref({
latestVersion: '2.4.8',
downloadUrl: '',
updateNotes: '• 优化了用户界面体验\n• 修复了已知问题\n• 提升了系统稳定性\n• 增加了新的功能模块\n• 优化了数据处理性能',
currentVersion: '',
hasUpdate: false
})
async function autoCheck() {
try {
version.value = await (window as any).electronAPI.getJarVersion()
const checkRes: any = await updateApi.checkUpdate(version.value)
const result = checkRes?.data || checkRes
if (!result.needUpdate) {
ElMessage.info('当前已是最新版本')
return
}
info.value = {
currentVersion: result.currentVersion,
latestVersion: result.latestVersion,
downloadUrl: result.downloadUrl || '',
updateNotes: '• 优化了用户界面体验\n• 修复了已知问题\n• 提升了系统稳定性\n• 轻量级更新仅替换app.asar',
hasUpdate: true
}
show.value = true
stage.value = 'check'
ElMessage.success('发现新版本')
} catch (error) {
console.error('检查更新失败:', error)
ElMessage.error('检查更新失败')
}
}
async function start() {
if (!info.value.downloadUrl) {
ElMessage.error('下载链接不可用')
return
}
stage.value = 'downloading'
prog.value = {percentage: 0, current: '0 MB', total: '0 MB', speed: ''}
;(window as any).electronAPI.onDownloadProgress((progress: any) => {
prog.value = {
percentage: progress.percentage || 0,
current: progress.current || '0 MB',
total: progress.total || '0 MB',
speed: progress.speed || ''
}
})
try {
const response = await (window as any).electronAPI.downloadUpdate(info.value.downloadUrl)
if (response.success) {
stage.value = 'completed'
prog.value.percentage = 100
ElMessage.success('下载完成')
} else {
ElMessage.error('下载失败: ' + (response.error || '未知错误'))
stage.value = 'check'
}
} catch (error) {
console.error('下载失败:', error)
ElMessage.error('下载失败')
stage.value = 'check'
}
}
async function cancelDownload() {
try {
(window as any).electronAPI.removeDownloadProgressListener()
await (window as any).electronAPI.cancelDownload()
show.value = false
stage.value = 'check'
} catch (error) {
console.error('取消下载失败:', error)
show.value = false
stage.value = 'check'
}
}
async function installUpdate() {
try {
await ElMessageBox.confirm(
'安装过程中程序将自动重启,请确保已保存所有工作。确定要立即安装更新吗?',
'确认安装',
{
confirmButtonText: '立即安装',
cancelButtonText: '取消',
type: 'warning'
}
)
const response = await (window as any).electronAPI.installUpdate()
if (response.success) {
ElMessage.success('应用即将重启')
setTimeout(() => show.value = false, 1000)
}
} catch (error) {
if (error !== 'cancel') ElMessage.error('安装失败')
}
}
onMounted(async () => {
version.value = await (window as any).electronAPI.getJarVersion()
})
onUnmounted(() => {
(window as any).electronAPI.removeDownloadProgressListener()
})
</script>
<style scoped>
.version-info {
position: fixed;
right: 10px;
bottom: 10px;
background: rgba(255, 255, 255, 0.9);
padding: 5px 10px;
border-radius: 4px;
font-size: 12px;
color: #909399;
z-index: 1000;
cursor: pointer;
user-select: none;
}
:deep(.update-dialog .el-dialog) {
border-radius: 16px;
box-shadow: 0 24px 48px rgba(0, 0, 0, 0.15);
}
:deep(.update-dialog .el-dialog__header) {
display: block;
text-align: left;
}
:deep(.update-dialog .el-dialog__body) {
padding: 0;
}
.update-content {
text-align: left;
}
.update-layout {
display: grid;
grid-template-columns: 88px 1fr;
align-items: start;
margin-bottom: 5px;
}
.left-pane {
display: flex;
flex-direction: column;
align-items: flex-start;
}
.app-icon-large {
width: 70px;
height: 70px;
border-radius: 12px;
margin: 4px 0 0 0;
}
.right-pane {
min-width: 0;
}
.right-pane .announce {
font-size: 16px;
font-weight: 600;
color: #1f2937;
margin: 4px 0 6px;
word-break: break-word;
}
.right-pane .desc {
font-size: 13px;
color: #6b7280;
line-height: 1.6;
margin: 0;
word-break: break-word;
}
.update-header {
display: flex;
align-items: flex-start;
margin-bottom: 24px;
}
.update-header.text-center {
text-align: center;
flex-direction: column;
align-items: center;
}
.app-icon {
width: 70px;
height: 70px;
border-radius: 12px;
margin-right: 16px;
flex-shrink: 0;
}
.update-header.text-center .app-icon {
margin-right: 0;
margin-bottom: 16px;
}
.update-header h3 {
font-size: 20px;
font-weight: 600;
margin: 16px 0 8px 0;
color: #1f2937;
}
.update-header p {
font-size: 14px;
color: #6b7280;
margin: 0;
line-height: 1.5;
}
.update-details {
border-radius: 8px;
padding: 0;
margin: 12px 0 8px 0;
}
.update-details.form {
max-height: none;
}
.notes-box :deep(textarea.el-textarea__inner) {
white-space: pre-wrap;
}
.update-details h4 {
font-size: 14px;
font-weight: 600;
color: #374151;
margin: 0 0 8px 0;
}
.update-actions.row {
display: flex;
flex-direction: column;
align-items: stretch;
gap: 12px;
}
.update-buttons {
display: flex;
justify-content: space-between;
gap: 12px;
}
.update-actions.row .update-buttons {
justify-content: space-between;
}
:deep(.update-actions.row .update-buttons .el-button) {
flex: none;
min-width: 100px;
}
.left-actions {
display: flex;
gap: 12px;
}
.right-actions {
display: flex;
gap: 8px;
}
:deep(.update-buttons .el-button) {
flex: 1;
height: 32px;
font-size: 13px;
border-radius: 8px;
}
.download-header {
text-align: center;
margin-bottom: 20px;
}
.download-header h3 {
font-size: 14px;
font-weight: 500;
margin: 0;
color: #1f2937;
}
.download-main {
display: grid;
grid-template-columns: 80px 1fr;
align-items: start;
}
.download-icon {
display: flex;
justify-content: center;
}
.download-icon .app-icon {
width: 64px;
height: 64px;
border-radius: 12px;
}
.download-content {
min-width: 0;
}
.download-info {
margin-bottom: 12px;
}
.download-info p {
font-size: 14px;
font-weight: 600;
color: #6b7280;
margin: 0;
}
.download-progress {
margin: 0;
}
.progress-info {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
font-size: 14px;
color: #6b7280;
}
.progress-details {
margin-top: 12px;
display: flex;
justify-content: space-between;
align-items: center;
}
.progress-details span {
font-size: 12px;
color: #909399;
}
:deep(.el-progress-bar__outer) {
border-radius: 4px;
background-color: #e5e7eb;
}
:deep(.el-progress-bar__inner) {
border-radius: 4px;
transition: width 0.3s ease;
}
:deep(.update-buttons .el-button--primary) {
background-color: #2563eb;
border-color: #2563eb;
font-weight: 500;
}
:deep(.update-buttons .el-button--primary:hover) {
background-color: #1d4ed8;
border-color: #1d4ed8;
}
:deep(.update-buttons .el-button:not(.el-button--primary)) {
background-color: #f3f4f6;
border-color: #d1d5db;
color: #374151;
font-weight: 500;
}
:deep(.update-buttons .el-button:not(.el-button--primary):hover) {
background-color: #e5e7eb;
border-color: #9ca3af;
}
</style>