فهرست منبع

feat(ota): 添加版本更新功能

- 新增 OTA 路由和页面组件
- 实现版本检查和更新逻辑
- 添加下载进度和状态显示
- 优化用户交互和提示
panqiuyao 5 ماه پیش
والد
کامیت
62062cb364

+ 165 - 0
electron/controller/ota.js

@@ -0,0 +1,165 @@
+'use strict';
+
+const Addon = require('ee-core/addon');
+const { Controller } = require('ee-core');
+const config = require('../config/config.default');
+const path = require('path');
+const fs = require('fs');
+const { ipcMain, app, BrowserWindow, shell, dialog } = require('electron');
+const { session } = require('electron');
+const CoreWindow = require("ee-core/electron/window");
+const Log = require('ee-core/log');
+const { spawn } = require('child_process');
+/**
+ * example
+ * @class
+ */
+class OTAController extends Controller {
+
+  constructor(ctx) {
+    super(ctx);
+  }
+
+  async updateVersion(url) {
+    const status = {
+      error: -1,
+      available: 1,
+      noAvailable: 2,
+      downloading: 3,
+      downloaded: 4,
+    };
+
+    const win = new BrowserWindow({ show: false });
+
+    // 设置下载路径为系统临时目录
+
+    win.webContents.downloadURL(url);
+
+
+    win.webContents.session.on('will-download', (event, item, webContents) => {
+
+
+
+
+      item.on('updated', (event, state) => {
+
+        Log.info('[addon:updated] 状态: ', state);
+        if (state === 'interrupted') {
+          Log.error('[addon:autoUpdater] 下载中断');
+        } else if (state === 'progressing') {
+          const receivedBytes = item.getReceivedBytes();
+          const totalBytes = item.getTotalBytes();
+          const percentNumber = Math.floor((receivedBytes / totalBytes) * 100);
+          const transferredSize = this.bytesChange(receivedBytes);
+          const totalSize = this.bytesChange(totalBytes);
+          const text = `已下载 ${percentNumber}% (${transferredSize}/${totalSize})`;
+
+          let info = {
+            status: status.downloading,
+            desc: text,
+            percentNumber: percentNumber,
+            totalSize: totalSize,
+            transferredSize: transferredSize
+          };
+          Log.info('[addon:updated] 下载进度: ', text);
+          this.sendStatusToWindow(info);
+        }
+      });
+
+      item.once('done', (event, state) => {
+
+        Log.info('[addon:done] 状态: ', state);
+        if (state === 'completed') {
+          Log.info('[addon:autoUpdater] 文件已下载完成: ', item.getSavePath());
+          let info = {
+            status: status.downloaded,
+            desc: '下载完成',
+            filePath: item.getSavePath()
+          };
+          this.sendStatusToWindow(info);
+
+          // 提醒用户选择操作
+          dialog.showMessageBox({
+            type: 'info',
+            title: '下载完成',
+            message: '文件已下载完成,请选择操作:',
+            buttons: ['立即安装', '打开目录', '取消']
+          }).then(result => {
+            if (result.response === 0) {
+              // 用户选择“立即安装”,执行安装操作
+              this.install(item.getSavePath());
+            } else if (result.response === 1) {
+              // 用户选择“打开目录”,打开文件所在目录
+              shell.openPath(path.dirname(item.getSavePath()));
+            }
+          });
+        } else {
+          Log.error('[addon:autoUpdater] 下载失败: ', state);
+          let info = {
+            status: status.error,
+            desc: `下载失败: ${state}`
+          };
+          this.sendStatusToWindow(info);
+        }
+
+        win.close(); // 关闭隐藏窗口
+      });
+    });
+  }
+
+  install(filePath) {
+
+    // 启动安装程序并脱离主进程
+    const child = spawn(filePath, [], {
+      detached: true,
+      stdio: 'ignore'
+    });
+
+    child.on('error', (err) => {
+      console.error('启动安装程序失败:', err);
+    });
+
+    // 让安装程序独立运行后,退出当前应用
+    child.unref();
+    app.quit();
+  }
+
+  /**
+   * 向前端发消息
+   */
+  sendStatusToWindow(content = {}) {
+    const textJson = JSON.stringify(content);
+    const channel = 'app.updater';
+    const win = CoreWindow.getMainWindow();
+    win.webContents.send(channel, textJson);
+  }
+
+  /**
+   * 单位转换
+   */
+  bytesChange (limit) {
+    let size = "";
+    if(limit < 0.1 * 1024){
+      size = limit.toFixed(2) + "B";
+    }else if(limit < 0.1 * 1024 * 1024){
+      size = (limit/1024).toFixed(2) + "KB";
+    }else if(limit < 0.1 * 1024 * 1024 * 1024){
+      size = (limit/(1024 * 1024)).toFixed(2) + "MB";
+    }else{
+      size = (limit/(1024 * 1024 * 1024)).toFixed(2) + "GB";
+    }
+
+    let sizeStr = size + "";
+    let index = sizeStr.indexOf(".");
+    let dou = sizeStr.substring(index + 1 , index + 3);
+    if(dou == "00"){
+      return sizeStr.substring(0, index) + sizeStr.substring(index + 3, index + 5);
+    }
+
+    return size;
+  }
+
+}
+
+OTAController.toString = () => '[class OTAController]';
+module.exports = OTAController;

+ 57 - 0
frontend/src/components/UpdateDialog/index.vue

@@ -0,0 +1,57 @@
+<template>
+  <el-dialog
+    :visible.sync="show"
+    :close-on-click-modal="false"
+    title="自动更新中"
+    :show-close="false"
+    @update:visible="close"
+  >
+    <template>
+      <div class="desc line-30 fs-16">{{res.desc}}</div>
+    </template>
+    <slot></slot>
+  </el-dialog>
+</template>
+<script>
+export default {
+  name: 'FullDialog',
+  components: {
+  },
+  props: {
+    show: {
+      type: Boolean,
+      default: false
+    },
+    res:{
+      type: Object,
+      default:()=>{
+        return {}
+      }
+    },
+  },
+  data() {
+    return {
+    }
+  },
+  computed: {
+  },
+  watch: {
+  },
+  destroyed() {
+  },
+  mounted() {
+  },
+  created() {
+
+  },
+  methods: {
+    close() {
+      this.$emit('onClose')
+      this.$emit('update:show', false)
+    },
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+</style>

+ 1 - 0
frontend/src/components/header-bar/assets/gengxin.svg

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1750736576156" class="icon" viewBox="0 0 1025 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="6548" xmlns:xlink="http://www.w3.org/1999/xlink" width="200.1953125" height="200"><path d="M737.600098 289.600098c-46.401367-96-144-161.599609-257.60083-161.599609-151.999512 0-276.800049 118.399902-286.399902 267.199219-112.000488 32.000977-193.599365 129.601074-193.599365 244.800781 0 136.000488 113.599854 247.999512 255.999756 255.999023 0 0 475.199463 0 480 0 158.398926 0 288.000244-135.999023 288.000244-303.999023C1024 430.400879 897.599121 297.601074 737.600098 289.600098L737.600098 289.600098zM681.864746 642.988281l-140.050049 160.469238c-16.470703 15.620606-43.184082 15.620606-59.650147 0l-140.050049-160.469238-0.001465 0c-4.11499-5.342285-6.545898-11.847168-6.545898-18.958008 0-17.593262 15.021729-31.862305 33.605713-31.862305l33.604492 0c9.242188 0 16.79126-7.179199 16.79126-15.93457l0.424561-162.904785 0.045898 0c0-17.446289 14.948486-31.578125 33.448486-31.578125l117.060791 0c18.480225 0 33.448486 14.13916 33.448486 31.578125l0.426025 162.904785c0 8.77832 7.556641 15.93457 16.791504 15.93457l33.601074 0c18.568848 0 33.61499 14.264648 33.61499 31.862305C688.43042 631.127441 685.987305 637.644043 681.864746 642.988281L681.864746 642.988281z" fill="#2c2c2c" p-id="6549"></path></svg>

+ 1 - 0
frontend/src/components/header-bar/assets/qiehuan.svg

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1750736436552" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4561" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M990.08 111.36a32 32 0 0 0-32 32V960H140.16a32 32 0 0 0 0 64h849.92a32 32 0 0 0 32-32V143.36a32 32 0 0 0-32-32z" fill="#2c2c2c" p-id="4562"></path><path d="M393.6 841.6a32 32 0 0 0-32-32H64V64h745.6v300.8a32 32 0 0 0 64 0V32a32 32 0 0 0-32-32H32a32 32 0 0 0-32 32v809.6a32 32 0 0 0 32 32h329.6a32 32 0 0 0 32-32z" fill="#2c2c2c" p-id="4563"></path><path d="M625.28 841.6a32 32 0 0 0 32 32h184.32a32 32 0 0 0 32-32v-183.68a32 32 0 1 0-64 0v106.24L256 210.56h106.24a32 32 0 0 0 0-64H178.56a32 32 0 0 0-32 32v184.96a32 32 0 0 0 64 0V256l553.6 553.6h-106.88a32 32 0 0 0-32 32z" fill="#2c2c2c" p-id="4564"></path></svg>

+ 27 - 2
frontend/src/components/header-bar/index.vue

@@ -35,8 +35,9 @@ import useUserInfo from "@/stores/modules/user";
 import { useRouter} from "vue-router";
 import iconsz from './assets/shezhi@2x.png'
 import iconykq from './assets/yaokong@2x.png'
-import iconMinimize from './assets/suoxiao@2x.png' // 新增
-import iconClose from './assets/guanbi@2x.png' // 新增
+import gengxin from './assets/gengxin.svg'
+/*import iconMinimize from './assets/suoxiao@2x.png' // 新增
+import iconClose from './assets/guanbi@2x.png' // 新增*/
 import icpList from '@/utils/ipc'
 import { getRouterUrl } from '@/utils/appfun'
 import client from "@/stores/modules/client";
@@ -82,6 +83,11 @@ const menuType = reactive({
     name: '初始设备调频设置',
     icon: iconsz,
     click: openDeveloper
+  },
+  ota: {
+    name: '版本更新',
+    icon: gengxin,
+    click: openOTA
   }
 });
 
@@ -161,6 +167,25 @@ function openDeveloper(){
   clientStore.ipc.send(icpList.utils.openMain, params);
 }
 
+
+function openOTA(){
+
+  const { href } = Router.resolve({
+    name: 'ota',
+  })
+
+  clientStore.ipc.removeAllListeners(icpList.utils.openMain);
+  let params = {
+    title: '版本更新',
+    width: 900,
+    height: 700,
+    frame: true,
+    id: "ota",
+    url: getRouterUrl(href)
+  }
+  clientStore.ipc.send(icpList.utils.openMain, params);
+}
+
 function loginOut(){
   useUserInfoStore.loginOut();
   useUserInfoStore.updateLoginShow(true)

+ 3 - 0
frontend/src/router/index.ts

@@ -4,6 +4,8 @@ import { createRouter, createWebHistory, createWebHashHistory, RouteRecordRaw }
 
 import { authGuard } from './plugins/authGuard'
 
+import otaRoutes from "./module/ota";
+
 const routes: RouteRecordRaw[] = [
     {
         path: "/",
@@ -73,6 +75,7 @@ const routes: RouteRecordRaw[] = [
             title: '初始设备调频设置'
         }
     },
+    ...otaRoutes,
 ];
 
 const router = createRouter({

+ 14 - 0
frontend/src/router/module/ota.ts

@@ -0,0 +1,14 @@
+
+
+const otaRoutes = [
+    {
+        path: "/ota",
+        name: "ota",
+        component: () => import("@/views/OTA/index.vue"),
+        meta: {
+            title: '版本更新'
+        }
+    },
+];
+
+export default otaRoutes;

+ 3 - 0
frontend/src/utils/ipc.ts

@@ -44,6 +44,9 @@ const icpList = {
         addLogo: 'controller.generate.addLogo',
         getLogoList: 'controller.generate.getLogoList',
         deleteLogo: 'controller.generate.deleteLogo',
+    },
+    ota:{
+        updateVersion: 'controller.ota.updateVersion'
     }
 
 

+ 204 - 0
frontend/src/views/OTA/index.vue

@@ -0,0 +1,204 @@
+<script setup>
+import { ref, onMounted, computed } from 'vue';
+import axios from 'axios';
+import packageJson from '@/../../package.json';
+import { ElMessageBox } from 'element-plus';
+import client from "@/stores/modules/client";
+import  icpList from '@/utils/ipc'
+import socket from "@/stores/modules/socket";
+import UpdateDialog from '@/components/UpdateDialog'
+
+const currentVersion = ref(packageJson.version);
+const latestVersion = ref('');
+const isLatest = ref(true);
+const versions = ref([]); // 所有版本数据
+
+// 分页相关
+const currentPage = ref(1);
+const pageSize = ref(20);
+const totalItems = ref(0);
+
+// 计算属性:获取当前页的数据
+const paginatedVersions = computed(() => {
+  const start = (currentPage.value - 1) * pageSize.value;
+  const end = start + pageSize.value;
+  return (versions.value || []).slice(start, end).reverse();
+});
+
+// 获取版本信息
+const fetchVersions = async () => {
+  try {
+    // 添加时间戳避免缓存问题
+    const timestamp = new Date().getTime();
+    const response = await axios.get('https://huilimaimg.cnhqt.com/frontend/html/zhihuiyin/version.json', {
+      params: {
+        _t: timestamp
+      }
+    });
+
+    console.log(response.data);
+    // 确保 response.data 是 JSON 数据
+    let data;
+    if (typeof response.data === 'string') {
+      data = JSON.parse(response.data);
+    } else {
+      data = response.data;
+    }
+
+    versions.value = data;
+
+    if (data.length > 0) {
+      const latest = data[data.length - 1];
+      latestVersion.value = latest.version;
+
+      // 比较版本号
+      isLatest.value = compareVersions(currentVersion.value, latest.version) >= 0;
+    }
+
+    // 初始化分页信息
+    totalItems.value = versions.value.length;
+  } catch (error) {
+    console.error('获取版本信息失败:', error);
+    ElMessage.error('无法获取版本信息');
+  }
+};
+
+// 版本号比较函数
+const compareVersions = (v1, v2) => {
+  const parts1 = v1.split('.').map(Number);
+  const parts2 = v2.split('.').map(Number);
+
+  for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) {
+    const num1 = parts1[i] || 0;
+    const num2 = parts2[i] || 0;
+
+    if (num1 > num2) return 1;
+    if (num1 < num2) return -1;
+  }
+
+  return 0;
+};
+
+// 下载最新版本
+const downloadUpdate = () => {
+  downloadSpecificVersion(versions.value[versions.value.length - 1].url)
+};
+
+
+const updateVisible = ref( false)
+const updateResult = ref({})
+
+const clientStore = client();
+const socketStore = socket()
+// 下载特定版本
+const downloadSpecificVersion = (url) => {
+
+  clientStore.ipc.removeAllListeners(icpList.ota.updateVersion);
+  clientStore.ipc.send(icpList.ota.updateVersion,url);
+  clientStore.ipc.on(icpList.ota.updateVersion, async (event, result) => {
+    console.log('==============================')
+    console.log('checkUpdate')
+    console.log(event)
+    console.log(result)
+
+  })
+
+  clientStore.ipc.on('app.updater', async (event, result) => {
+
+    console.log('==============================')
+    console.log('app.updater')
+    console.log(event)
+    console.log(result)
+
+    try {
+      let res =  JSON.parse(result)
+      if([1,3,4].includes(res.status)){
+        updateResult.value =  res
+        updateVisible.value = true
+        console.log(updateResult.value)
+        console.log(updateVisible.value)
+      }
+    }catch (e) {
+      console.log(e)
+    }
+
+
+  })
+
+
+
+};
+
+// 处理分页变化
+const handlePageChange = (page) => {
+  currentPage.value = page;
+};
+
+onMounted(() => {
+  fetchVersions();
+});
+</script>
+
+<template>
+  <el-container>
+
+
+    <el-dialog
+        v-model="updateVisible"
+        :close-on-click-modal="false"
+        title="软件下载中"
+    >
+        <div class="desc line-30 fs-16">{{updateResult.desc}}</div>
+    </el-dialog>
+    <el-main>
+      <div class="version-check-container">
+        <el-card class="current-version-card">
+          <p>当前版本  <span class="fs-14">版本号: {{ currentVersion }}</span></p>
+
+          <p>状态: <span :class="{ 'text-green': isLatest, 'text-red': !isLatest }">{{ isLatest ? '您已经是最新版本' : '发现新版本' }}</span></p>
+          <p v-if="!isLatest">新版本: {{ latestVersion }}</p>
+          <el-button v-if="!isLatest" type="primary" @click="downloadUpdate">下载更新</el-button>
+        </el-card>
+
+        <el-card class="history-versions-card mar-top-10">
+          <h3>历史版本</h3>
+          <!-- 使用计算属性获取分页数据 -->
+          <el-table :data="paginatedVersions" border>
+            <el-table-column prop="version" label="版本号" width="70"></el-table-column>
+            <el-table-column prop="date" label="发布日期" width="100"></el-table-column>
+            <el-table-column label="描述">
+              <template #default="{ row }">
+                <el-tooltip :content="row.describe" placement="top" :show-when="hover" :width="500">
+                  <span class="version-describe">{{ row.describe }}</span>
+                </el-tooltip>
+              </template>
+            </el-table-column>
+            <el-table-column label="操作"  width="80">
+              <template #default="{ row }">
+                <el-button size="small" @click="downloadSpecificVersion(row.url)">下载</el-button>
+              </template>
+            </el-table-column>
+          </el-table>
+          <el-pagination
+            layout="prev, pager, next"
+            :total="totalItems"
+            :page-size="pageSize"
+            v-model:current-page="currentPage"
+            @current-change="handlePageChange"
+            style="margin-top: 15px;"
+          />
+        </el-card>
+      </div>
+    </el-main>
+  </el-container>
+</template>
+
+<style scoped>
+.version-describe {
+  display: inline-block;
+  width: 100%; /* 根据需要调整 */
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+}
+</style>

+ 9 - 1
frontend/src/views/Photography/shot.vue

@@ -197,6 +197,7 @@ const useUserInfoStore = useUserInfo();
 
 import  configInfo  from '@/stores/modules/config';
 const configInfoStore = configInfo();
+import qiehuan from '@/components/header-bar/assets/qiehuan.svg'
 
 const menu = computed(()=>{
   if(configInfoStore.appModel === 2){
@@ -206,13 +207,17 @@ const menu = computed(()=>{
       },
       {
         name:'切换到拍摄模式',
+        icon:qiehuan,
         click(){
           configInfoStore.updateAppModel(1)
           Router.push({
             name:'PhotographyCheck'
           })
         }
-      }
+      },
+      {
+        type:'ota'
+      },
     ]
   }
   if(useUserInfoStore.userInfo.brand_company_code === '1300'){
@@ -222,6 +227,9 @@ const menu = computed(()=>{
       },
       {
         type:'developer'
+      },
+      {
+        type:'ota'
       }
     ]
   }