feat(): 添加更新功能
This commit is contained in:
@@ -15,6 +15,7 @@ import SettingsPanel from "./components/SettingsPanel.vue";
|
||||
import LogViewer from "./components/LogViewer.vue";
|
||||
import AppStatusBar, { type StatusBarItem } from "./components/AppStatusBar.vue";
|
||||
import CloseDialog from "./components/CloseDialog.vue";
|
||||
import UpdateDialog from "./components/UpdateDialog.vue";
|
||||
import { useMonitorStore } from "./stores/monitor";
|
||||
|
||||
const activeTab = ref("monitor");
|
||||
@@ -81,6 +82,7 @@ onUnmounted(() => store.cleanup());
|
||||
</a-layout>
|
||||
|
||||
<CloseDialog />
|
||||
<UpdateDialog />
|
||||
</template>
|
||||
|
||||
<style>
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
import { useMonitorStore } from "../stores/monitor";
|
||||
|
||||
const store = useMonitorStore();
|
||||
|
||||
const progressPercent = computed(() => {
|
||||
if (!store.updateProgress || !store.updateProgress.total) return 0;
|
||||
return Math.round((store.updateProgress.downloaded / store.updateProgress.total) * 100);
|
||||
});
|
||||
|
||||
const downloadedMB = computed(() => {
|
||||
if (!store.updateProgress) return "0";
|
||||
return (store.updateProgress.downloaded / 1024 / 1024).toFixed(1);
|
||||
});
|
||||
|
||||
const totalMB = computed(() => {
|
||||
if (!store.updateProgress || !store.updateProgress.total) return "0";
|
||||
return (store.updateProgress.total / 1024 / 1024).toFixed(1);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<a-modal
|
||||
v-model:open="store.showUpdateDialog"
|
||||
title="发现新版本"
|
||||
:width="400"
|
||||
centered
|
||||
:closable="!store.isUpdating"
|
||||
:maskClosable="!store.isUpdating"
|
||||
>
|
||||
<div class="update-body">
|
||||
<div v-if="!store.isUpdating">
|
||||
<p class="version-info">
|
||||
新版本 <span class="version-tag">v{{ store.updateInfo?.version }}</span> 已发布
|
||||
</p>
|
||||
<div v-if="store.updateInfo?.notes" class="update-notes">
|
||||
<p class="notes-title">更新说明:</p>
|
||||
<p class="notes-content">{{ store.updateInfo.notes }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="update-progress">
|
||||
<p>正在下载更新...</p>
|
||||
<a-progress :percent="progressPercent" :status="progressPercent >= 100 ? 'success' : 'active'" />
|
||||
<p class="progress-detail">{{ downloadedMB }} / {{ totalMB }} MB</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<template v-if="!store.isUpdating">
|
||||
<a-button @click="store.dismissUpdate()">稍后提醒</a-button>
|
||||
<a-button type="primary" @click="store.confirmUpdate()">立即更新</a-button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<a-button disabled>更新中,请稍候...</a-button>
|
||||
</template>
|
||||
</template>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.update-body {
|
||||
padding: 4px 0 8px;
|
||||
}
|
||||
.version-info {
|
||||
margin: 0 0 12px;
|
||||
font-size: 15px;
|
||||
color: #333;
|
||||
}
|
||||
.version-tag {
|
||||
font-weight: 600;
|
||||
color: #1677ff;
|
||||
}
|
||||
.notes-title {
|
||||
margin: 0 0 4px;
|
||||
font-size: 13px;
|
||||
color: #888;
|
||||
}
|
||||
.notes-content {
|
||||
margin: 0;
|
||||
padding: 8px 12px;
|
||||
background: #f5f5f5;
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
color: #555;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
.update-progress {
|
||||
text-align: center;
|
||||
}
|
||||
.update-progress p {
|
||||
margin: 0 0 8px;
|
||||
}
|
||||
.progress-detail {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
margin-top: 4px !important;
|
||||
}
|
||||
</style>
|
||||
+113
-18
@@ -18,6 +18,8 @@ import {
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { listen, type UnlistenFn } from "@tauri-apps/api/event";
|
||||
import { openUrl } from "@tauri-apps/plugin-opener";
|
||||
import { check, type Update } from "@tauri-apps/plugin-updater";
|
||||
import { relaunch } from "@tauri-apps/plugin-process";
|
||||
|
||||
export const APP_NAME = "工单系统监测";
|
||||
const API_BASE = "https://crm.yunvip123.com/api";
|
||||
@@ -76,6 +78,10 @@ export const useMonitorStore = defineStore("monitor", () => {
|
||||
});
|
||||
const showCloseDialog = ref(false);
|
||||
const postponeResumeTime = ref<number | null>(null);
|
||||
const showUpdateDialog = ref(false);
|
||||
const updateInfo = ref<{ version: string; notes: string; date: string } | null>(null);
|
||||
const updateProgress = ref<{ total: number; downloaded: number } | null>(null);
|
||||
const isUpdating = ref(false);
|
||||
|
||||
// ===== 内部状态 =====
|
||||
let sessionId = "";
|
||||
@@ -85,7 +91,9 @@ export const useMonitorStore = defineStore("monitor", () => {
|
||||
let postponeTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
let unlistenNotification: UnlistenFn | null = null;
|
||||
let unlistenClose: UnlistenFn | null = null;
|
||||
let pendingUpdate: Update | null = null;
|
||||
let trayIconInstance: TrayIcon | null = null;
|
||||
let cachedSvgImg: HTMLImageElement | null = null;
|
||||
|
||||
// ===== 工具函数 =====
|
||||
function formatTime(date: Date): string {
|
||||
@@ -541,6 +549,60 @@ export const useMonitorStore = defineStore("monitor", () => {
|
||||
}
|
||||
|
||||
// ===== 配置更新 =====
|
||||
|
||||
async function checkForUpdates() {
|
||||
try {
|
||||
addLog("INFO", "正在检查应用更新...", "UPDATER");
|
||||
const update = await check();
|
||||
if (update) {
|
||||
pendingUpdate = update;
|
||||
updateInfo.value = {
|
||||
version: update.version,
|
||||
notes: update.body || "",
|
||||
date: update.date || "",
|
||||
};
|
||||
showUpdateDialog.value = true;
|
||||
addLog("INFO", `发现新版本: ${update.version}`, "UPDATER");
|
||||
} else {
|
||||
addLog("INFO", "当前已是最新版本", "UPDATER");
|
||||
}
|
||||
} catch (e: any) {
|
||||
addLog("WARN", `检查更新失败: ${e.message || e}`, "UPDATER");
|
||||
}
|
||||
}
|
||||
|
||||
async function confirmUpdate() {
|
||||
if (!pendingUpdate) return;
|
||||
isUpdating.value = true;
|
||||
updateProgress.value = { total: 0, downloaded: 0 };
|
||||
try {
|
||||
addLog("INFO", "开始下载更新...", "UPDATER");
|
||||
await pendingUpdate.downloadAndInstall((event) => {
|
||||
if (event.event === "Started" && event.data.contentLength) {
|
||||
updateProgress.value = { total: event.data.contentLength, downloaded: 0 };
|
||||
} else if (event.event === "Progress") {
|
||||
if (updateProgress.value) {
|
||||
updateProgress.value.downloaded += event.data.chunkLength;
|
||||
}
|
||||
} else if (event.event === "Finished") {
|
||||
addLog("INFO", "更新下载完成,即将重启...", "UPDATER");
|
||||
}
|
||||
});
|
||||
await relaunch();
|
||||
} catch (e: any) {
|
||||
addLog("ERROR", `更新安装失败: ${e.message || e}`, "UPDATER");
|
||||
message.value = "更新安装失败";
|
||||
isUpdating.value = false;
|
||||
updateProgress.value = null;
|
||||
}
|
||||
}
|
||||
|
||||
function dismissUpdate() {
|
||||
showUpdateDialog.value = false;
|
||||
pendingUpdate = null;
|
||||
updateInfo.value = null;
|
||||
}
|
||||
|
||||
async function updateConfig() {
|
||||
isLoading.value = true;
|
||||
try {
|
||||
@@ -563,29 +625,59 @@ export const useMonitorStore = defineStore("monitor", () => {
|
||||
|
||||
// ===== 系统托盘 =====
|
||||
|
||||
/** 用 Canvas 渲染彩色圆形背景 + 白色"工"字,返回 RGBA 像素数据 */
|
||||
function makeTrayIconRGBA(r: number, g: number, b: number, size = 32): Uint8Array {
|
||||
/** 加载 SVG 图片(带缓存) */
|
||||
async function loadSvgImage(): Promise<HTMLImageElement> {
|
||||
if (cachedSvgImg) return cachedSvgImg;
|
||||
return new Promise((resolve, reject) => {
|
||||
const img = new window.Image();
|
||||
img.onload = () => { cachedSvgImg = img; resolve(img); };
|
||||
img.onerror = reject;
|
||||
img.src = "/gong.svg";
|
||||
});
|
||||
}
|
||||
|
||||
/** 加载 SVG → Canvas 渲染 → 像素替色 → 返回 RGBA 数据 */
|
||||
async function makeTrayIconRGBA(r: number, g: number, b: number, size = 32): Promise<Uint8Array> {
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = size;
|
||||
canvas.height = size;
|
||||
const ctx = canvas.getContext("2d")!;
|
||||
|
||||
// 圆形背景
|
||||
ctx.clearRect(0, 0, size, size);
|
||||
ctx.beginPath();
|
||||
ctx.arc(size / 2, size / 2, size / 2 - 1, 0, Math.PI * 2);
|
||||
ctx.fillStyle = `rgb(${r},${g},${b})`;
|
||||
ctx.fill();
|
||||
try {
|
||||
const svgImg = await loadSvgImage();
|
||||
|
||||
// 白色"工"字
|
||||
ctx.fillStyle = "#ffffff";
|
||||
ctx.font = `bold ${Math.round(size * 0.62)}px "Microsoft YaHei","SimHei",sans-serif`;
|
||||
ctx.textAlign = "center";
|
||||
ctx.textBaseline = "middle";
|
||||
ctx.fillText("工", size / 2, size / 2 + 1);
|
||||
ctx.drawImage(svgImg, 0, 0, size, size);
|
||||
const imageData = ctx.getImageData(0, 0, size, size);
|
||||
const data = imageData.data;
|
||||
|
||||
const imageData = ctx.getImageData(0, 0, size, size);
|
||||
return new Uint8Array(imageData.data.buffer);
|
||||
for (let i = 0; i < data.length; i += 4) {
|
||||
// 根据亮度判断:暗像素(文字) → 白色,亮像素(背景) → 状态色
|
||||
const brightness = (data[i] * 0.299 + data[i + 1] * 0.587 + data[i + 2] * 0.114);
|
||||
if (brightness < 128) {
|
||||
data[i] = data[i + 1] = data[i + 2] = 255;
|
||||
} else {
|
||||
data[i] = r;
|
||||
data[i + 1] = g;
|
||||
data[i + 2] = b;
|
||||
}
|
||||
data[i + 3] = 255;
|
||||
}
|
||||
|
||||
ctx.putImageData(imageData, 0, 0);
|
||||
} catch {
|
||||
// SVG 加载失败,回退到纯 Canvas 绘制
|
||||
addLog("WARN", "SVG 图标加载失败,使用回退方案", "SYSTEM");
|
||||
ctx.fillStyle = `rgb(${r},${g},${b})`;
|
||||
ctx.fillRect(0, 0, size, size);
|
||||
ctx.fillStyle = "#ffffff";
|
||||
ctx.font = `bold ${Math.round(size * 0.62)}px "Microsoft YaHei","SimHei",sans-serif`;
|
||||
ctx.textAlign = "center";
|
||||
ctx.textBaseline = "middle";
|
||||
ctx.fillText("工", size / 2, size / 2 + 1);
|
||||
}
|
||||
|
||||
const finalData = ctx.getImageData(0, 0, size, size);
|
||||
return new Uint8Array(finalData.data.buffer);
|
||||
}
|
||||
|
||||
/** 根据当前应用状态更新托盘图标和提示文字 */
|
||||
@@ -620,7 +712,7 @@ export const useMonitorStore = defineStore("monitor", () => {
|
||||
tooltip = `${APP_NAME} - 监测中`;
|
||||
}
|
||||
|
||||
const icon = await Image.new(makeTrayIconRGBA(...color), 32, 32);
|
||||
const icon = await Image.new(await makeTrayIconRGBA(...color), 32, 32);
|
||||
await trayIconInstance.setIcon(icon);
|
||||
await trayIconInstance.setTooltip(tooltip);
|
||||
} catch (e: any) {
|
||||
@@ -686,7 +778,7 @@ export const useMonitorStore = defineStore("monitor", () => {
|
||||
const menu = await buildTrayMenu();
|
||||
|
||||
// 初始状态:灰色(未开始)
|
||||
const icon = await Image.new(makeTrayIconRGBA(158, 158, 158), 32, 32);
|
||||
const icon = await Image.new(await makeTrayIconRGBA(158, 158, 158), 32, 32);
|
||||
|
||||
trayIconInstance = await TrayIcon.new({
|
||||
icon,
|
||||
@@ -763,6 +855,7 @@ export const useMonitorStore = defineStore("monitor", () => {
|
||||
}
|
||||
|
||||
checkNetworkStatus();
|
||||
checkForUpdates();
|
||||
|
||||
if (config.value.auto_monitor && rememberPassword.value) {
|
||||
const canAutoStart = loginMode.value === "token"
|
||||
@@ -795,12 +888,14 @@ export const useMonitorStore = defineStore("monitor", () => {
|
||||
isLoggedIn, isMonitoring, isPostponed, postponeResumeTime, autoStartEnabled,
|
||||
logs, networkStatus, config, ticketCounts,
|
||||
showCloseDialog,
|
||||
showUpdateDialog, updateInfo, updateProgress, isUpdating,
|
||||
// 方法
|
||||
initialize, cleanup,
|
||||
addLog, clearLogs, clearMessage, saveSettings, exportLogs,
|
||||
startMonitoring, stopMonitoring, resumeMonitoring, manualCheck,
|
||||
testNotification, checkNetworkStatus, updateConfig,
|
||||
minimizeToTray, handleCloseAction,
|
||||
checkForUpdates, confirmUpdate, dismissUpdate,
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user