501 lines
11 KiB
Vue
501 lines
11 KiB
Vue
<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> |