- 修改设备移除时的本地清理方法,统一调用 clearLocalAuth - 优化设备数量限制校验逻辑,避免重复计算当前设备- 移除冗余的设备状态检查,简化设备移除流程- 调整 Redis 连接超时与等待时间,提升连接稳定性- 增强 MySQL 数据库连接配置,添加自动重连机制 -优化 Druid 连接池参数,提高数据库连接性能 - 简化客户端认证与数据上报逻辑,提升处理效率 - 移除过期设备状态更新逻辑,减少不必要的数据库操作- 调整慢 SQL 记录阈值,便于及时发现性能问题-优化版本分布与数据类型统计查询逻辑,提高响应速度
631 lines
15 KiB
Vue
631 lines
15 KiB
Vue
<template>
|
||
<div>
|
||
<div class="version-info" @click="handleVersionClick">
|
||
v{{ version || '-' }}
|
||
<span v-if="hasNewVersion" class="update-badge"></span>
|
||
</div>
|
||
<el-dialog v-model="show" width="522px" :close-on-click-modal="false" align-center
|
||
:class="['update-dialog', `stage-${stage}`]"
|
||
: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="skipVersion">跳过这个版本</el-button>
|
||
</div>
|
||
<div class="right-actions">
|
||
<el-button size="small" @click="remindLater">稍后提醒</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="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="100"
|
||
:show-text="false"
|
||
:stroke-width="6"
|
||
color="#67C23A"/>
|
||
<div class="progress-details">
|
||
<span style="font-weight: 500" v-if="prog.current !== '0 MB' && prog.total !== '0 MB'">{{ prog.current }} / {{ prog.total }}</span>
|
||
<span style="font-weight: 500" v-else>下载完成</span>
|
||
<div class="action-buttons">
|
||
<el-button size="small" type="primary" @click="installUpdate">立即重启</el-button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</el-dialog>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import {ref, computed, onMounted, onUnmounted, watch} 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 hasNewVersion = ref(false) // 控制小红点显示
|
||
const prog = ref({percentage: 0, current: '0 MB', total: '0 MB'})
|
||
const info = ref({
|
||
latestVersion: '2.4.8',
|
||
downloadUrl: '',
|
||
asarUrl: '',
|
||
jarUrl: '',
|
||
updateNotes: '• 优化了用户界面体验\n• 修复了已知问题\n• 提升了系统稳定性',
|
||
currentVersion: '',
|
||
hasUpdate: false
|
||
})
|
||
|
||
const SKIP_VERSION_KEY = 'skipped_version'
|
||
const REMIND_LATER_KEY = 'remind_later_time'
|
||
|
||
async function autoCheck(silent = false) {
|
||
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) {
|
||
hasNewVersion.value = false
|
||
if (!silent) {
|
||
ElMessage.info('当前已是最新版本')
|
||
}
|
||
return
|
||
}
|
||
|
||
// 发现新版本,更新信息并显示小红点
|
||
info.value = {
|
||
currentVersion: result.currentVersion,
|
||
latestVersion: result.latestVersion,
|
||
downloadUrl: result.downloadUrl || '',
|
||
asarUrl: result.asarUrl || '',
|
||
jarUrl: result.jarUrl || '',
|
||
updateNotes: '• 优化了用户界面体验\n• 修复了已知问题\n• 提升了系统稳定性\n• 同步更新前端和后端',
|
||
hasUpdate: true
|
||
}
|
||
hasNewVersion.value = true
|
||
|
||
// 检查是否跳过此版本
|
||
const skippedVersion = localStorage.getItem(SKIP_VERSION_KEY)
|
||
if (skippedVersion === result.latestVersion) {
|
||
// 跳过的版本:显示小红点,但不弹框
|
||
return
|
||
}
|
||
|
||
// 检查是否在稍后提醒时间内
|
||
const remindLater = localStorage.getItem(REMIND_LATER_KEY)
|
||
if (remindLater && Date.now() < parseInt(remindLater)) {
|
||
// 稍后提醒期间:显示小红点,但不弹框
|
||
return
|
||
}
|
||
|
||
// 首次发现新版本:显示小红点并弹框
|
||
show.value = true
|
||
stage.value = 'check'
|
||
if (!silent) {
|
||
ElMessage.success('发现新版本')
|
||
}
|
||
} catch (error) {
|
||
console.error('检查更新失败:', error)
|
||
if (!silent) {
|
||
ElMessage.error('检查更新失败')
|
||
}
|
||
}
|
||
}
|
||
|
||
function handleVersionClick() {
|
||
// 如果有新版本,直接显示更新对话框
|
||
if (hasNewVersion.value) {
|
||
// 重置状态确保从检查阶段开始
|
||
stage.value = 'check'
|
||
prog.value = {percentage: 0, current: '0 MB', total: '0 MB'}
|
||
show.value = true
|
||
} else {
|
||
// 没有新版本,执行检查更新
|
||
autoCheck(false)
|
||
}
|
||
}
|
||
|
||
function skipVersion() {
|
||
localStorage.setItem(SKIP_VERSION_KEY, info.value.latestVersion)
|
||
show.value = false
|
||
}
|
||
|
||
function remindLater() {
|
||
// 24小时后再提醒
|
||
localStorage.setItem(REMIND_LATER_KEY, (Date.now() + 24 * 60 * 60 * 1000).toString())
|
||
show.value = false
|
||
}
|
||
|
||
async function start() {
|
||
if (!info.value.asarUrl && !info.value.jarUrl) {
|
||
ElMessage.error('下载链接不可用')
|
||
return
|
||
}
|
||
|
||
stage.value = 'downloading'
|
||
prog.value = {percentage: 0, current: '0 MB', total: '0 MB'}
|
||
|
||
;(window as any).electronAPI.onDownloadProgress((progress: any) => {
|
||
prog.value = {
|
||
percentage: progress.percentage || 0,
|
||
current: progress.current || '0 MB',
|
||
total: progress.total || '0 MB'
|
||
}
|
||
})
|
||
|
||
try {
|
||
const response = await (window as any).electronAPI.downloadUpdate({
|
||
asarUrl: info.value.asarUrl,
|
||
jarUrl: info.value.jarUrl
|
||
})
|
||
|
||
if (response.success) {
|
||
stage.value = 'completed'
|
||
prog.value.percentage = 100
|
||
// 如果没有有效的进度信息,设置默认值
|
||
if (prog.value.current === '0 MB' && prog.value.total === '0 MB') {
|
||
// 保持原有的"0 MB"值,让模板中的条件判断来处理显示
|
||
}
|
||
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()
|
||
await autoCheck(true)
|
||
})
|
||
|
||
// 监听对话框关闭,重置状态
|
||
watch(show, (newValue) => {
|
||
if (!newValue) {
|
||
// 对话框关闭时重置状态
|
||
stage.value = 'check'
|
||
prog.value = {percentage: 0, current: '0 MB', total: '0 MB'}
|
||
}
|
||
})
|
||
|
||
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;
|
||
}
|
||
|
||
.update-badge {
|
||
position: absolute;
|
||
top: -2px;
|
||
right: -2px;
|
||
width: 8px;
|
||
height: 8px;
|
||
background: #f56c6c;
|
||
border-radius: 50%;
|
||
border: 2px solid #fff;
|
||
animation: pulse 2s ease-in-out infinite;
|
||
}
|
||
|
||
@keyframes pulse {
|
||
0%, 100% {
|
||
transform: scale(1);
|
||
opacity: 1;
|
||
}
|
||
50% {
|
||
transform: scale(1.1);
|
||
opacity: 0.8;
|
||
}
|
||
}
|
||
|
||
:deep(.update-dialog .el-dialog) {
|
||
border-radius: 16px;
|
||
box-shadow: 0 24px 48px rgba(0, 0, 0, 0.15);
|
||
}
|
||
|
||
/* 通用标题样式 */
|
||
:deep(.update-dialog .el-dialog__title) {
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
margin-left: 8px;
|
||
}
|
||
|
||
/* 默认标题样式(第一阶段 - 检查阶段) */
|
||
:deep(.update-dialog.stage-check .el-dialog__header) {
|
||
display: block;
|
||
text-align: left;
|
||
}
|
||
|
||
/* 第二阶段 - 下载中,标题居中 */
|
||
:deep(.update-dialog.stage-downloading .el-dialog__header) {
|
||
display: block;
|
||
text-align: center;
|
||
}
|
||
|
||
:deep(.update-dialog.stage-downloading .el-dialog__title) {
|
||
margin-left: 20px;
|
||
}
|
||
|
||
/* 第三阶段 - 下载完成,标题居中 */
|
||
:deep(.update-dialog.stage-completed .el-dialog__header) {
|
||
display: block;
|
||
text-align: center;
|
||
}
|
||
|
||
:deep(.update-dialog.stage-completed .el-dialog__title) {
|
||
margin-left: 20px;
|
||
}
|
||
|
||
: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 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;
|
||
}
|
||
|
||
.action-buttons {
|
||
display: flex;
|
||
gap: 8px;
|
||
}
|
||
|
||
: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> |