zjk
2026-07-31 2617e1786c3fddc43b18df31c8236b97ce4ee60f
feat: 添加应用更新进度显示与安装权限

新增安卓APK安装必需的两个权限至manifest.json
为文件下载工具函数添加进度回调支持,可实时上报下载进度
优化管理员配置面板的版本更新功能:显示实时下载进度百分比与安装状态,修复相对路径包地址解析问题
重构版本更新流程,正确跟踪下载与安装状态并优化提示信息
3个文件已修改
116 ■■■■■ 已修改文件
components/admin/AdminConfigPanel.vue 98 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
manifest.json 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
utils/fs.js 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
components/admin/AdminConfigPanel.vue
@@ -59,7 +59,7 @@
          </view>
          <view class="update-btn" @click="handleVersionUpdate">
            <image :src="updateBtnIcon" class="banner-btn-icon" mode="aspectFit"></image>
            <text class="update-btn-text">{{ isUpdatingVersion ? '下载中...' : '立即更新' }}</text>
            <text class="update-btn-text">{{ updateButtonText }}</text>
          </view>
        </view>
        <view class="update-banner" v-else>
@@ -172,7 +172,7 @@
</template>
<script setup>
import { onMounted, ref } from 'vue';
import { computed, onMounted, ref } from 'vue';
import configPageIcon from '../../static/admin-pages/L2Nja.png';
import machineIcon from '../../static/admin-pages/n83u2.png';
import systemInfoIcon from '../../static/admin-pages/tLgNe.png';
@@ -190,7 +190,7 @@
import { syncEmployeeVerifyList } from '../../api/employee';
import { deactivateVisitMachine } from '../../api/device';
import { getLatestVersion } from '../../api/visitorRecord';
import { clearRequestServerConfig, getRequestServerConfig, setRequestServerConfig } from '../../utils/request';
import { buildBaseUrl, clearRequestServerConfig, getRequestServerConfig, setRequestServerConfig } from '../../utils/request';
import { syncAllPendingRecords } from '../../utils/syncEngine';
import { downloadRemoteFileToLocal } from '../../utils/fs';
@@ -203,12 +203,30 @@
const isLoggingOut = ref(false);
const isManualSyncing = ref(false);
const isUpdatingVersion = ref(false);
const updateDownloadProgress = ref(-1);
const isInstallingVersion = ref(false);
const logoutHoldTimer = ref(null);
const currentVersion = ref('v1.0.0');
const latestVersion = ref('');
const hasNewVersion = ref(false);
const latestVersionInfo = ref(null);
const updateButtonText = computed(() => {
  if (!isUpdatingVersion.value) {
    return '立即更新';
  }
  if (isInstallingVersion.value) {
    return '安装中...';
  }
  if (updateDownloadProgress.value >= 0) {
    return `下载中 ${updateDownloadProgress.value}%`;
  }
  return '下载中...';
});
const normalizeVersionNumber = (version = '') => {
  return String(version || '').trim().replace(/^[vV]/, '');
@@ -366,6 +384,24 @@
  });
};
const resolvePackageDownloadUrl = (packageUrl = '') => {
  const normalizedPackageUrl = String(packageUrl || '').trim();
  if (!normalizedPackageUrl) {
    return '';
  }
  if (/^https?:\/\//.test(normalizedPackageUrl)) {
    return normalizedPackageUrl;
  }
  const baseUrl = buildBaseUrl(getRequestServerConfig());
  if (!baseUrl) {
    return '';
  }
  return `${baseUrl}${normalizedPackageUrl.startsWith('/') ? normalizedPackageUrl : `/${normalizedPackageUrl}`}`;
};
const handleVersionUpdate = async () => {
  if (isUpdatingVersion.value) {
    return;
@@ -373,8 +409,9 @@
  const packageUrl = latestVersionInfo.value?.packageUrl || '';
  const packageName = latestVersionInfo.value?.packageName || 'visitor-update.apk';
  const packageDownloadUrl = resolvePackageDownloadUrl(packageUrl);
  if (!hasNewVersion.value || !packageUrl) {
  if (!hasNewVersion.value || !packageDownloadUrl) {
    uni.showToast({
      title: '未获取到更新包地址',
      icon: 'none',
@@ -392,43 +429,56 @@
  try {
    isUpdatingVersion.value = true;
    uni.showLoading({
      title: '更新包下载中...',
      mask: true,
    });
    updateDownloadProgress.value = 0;
    isInstallingVersion.value = false;
    let lastProgress = -1;
    const fileNamePrefix = String(packageName || 'visitor-update')
      .replace(/\.[^.]+$/, '')
      .replace(/[^a-zA-Z0-9_-]/g, '-') || 'visitor-update';
    const localPackagePath = await downloadRemoteFileToLocal(packageUrl, {
    const localPackagePath = await downloadRemoteFileToLocal(packageDownloadUrl, {
      directoryName: 'apk-updates',
      fileNamePrefix,
      fallbackExtension: 'apk',
      onProgress: ({ progress }) => {
        const safeProgress = Math.max(0, Math.min(100, Number(progress || 0)));
        if (safeProgress === lastProgress) {
          return;
        }
        lastProgress = safeProgress;
        updateDownloadProgress.value = safeProgress;
      },
    });
    uni.hideLoading();
    updateDownloadProgress.value = 100;
    isInstallingVersion.value = true;
    uni.showToast({
      title: '下载完成,正在拉起安装',
      icon: 'none',
    });
    plus.runtime.install(localPackagePath, {}, () => {
      uni.showToast({
        title: '安装包已打开,请按提示完成更新',
        icon: 'none',
      });
    }, (error) => {
      console.error('安装更新包失败:', error);
      uni.showToast({
        title: error?.message || '安装更新包失败',
        icon: 'none',
    await new Promise((resolve, reject) => {
      plus.runtime.install(localPackagePath, {}, () => {
        uni.showToast({
          title: '安装包已打开,请按提示完成更新',
          icon: 'none',
        });
        resolve();
      }, (error) => {
        reject(error);
      });
    });
  } catch (error) {
    console.error('下载更新包失败:', error);
    const actionText = isInstallingVersion.value ? '安装更新包失败' : '下载更新包失败';
    console.error(`${actionText}:`, error);
    uni.showToast({
      title: error?.message || '下载更新包失败',
      title: error?.message || actionText,
      icon: 'none',
    });
  } finally {
    uni.hideLoading();
    isUpdatingVersion.value = false;
    updateDownloadProgress.value = -1;
    isInstallingVersion.value = false;
  }
};
@@ -439,7 +489,7 @@
  try {
    isManualSyncing.value = true;
    // 强制触发员工档案全量同步,绕过缓存有效期
    try {
      await syncEmployeeVerifyList(true);
manifest.json
@@ -36,7 +36,9 @@
                    "<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
                    "<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
                    "<uses-feature android:name=\"android.hardware.camera\"/>",
                    "<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>"
                    "<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>",
                    "<uses-permission android:name=\"android.permission.INSTALL_PACKAGES\"/>",
                    "<uses-permission android:name=\"android.permission.REQUEST_INSTALL_PACKAGES\"/>"
                ]
            },
            "ios" : {
utils/fs.js
@@ -91,6 +91,7 @@
    directoryName = 'downloads',
    fileNamePrefix = 'file',
    fallbackExtension = 'jpg',
    onProgress = null,
  } = options;
  return new Promise((resolve, reject) => {
@@ -101,7 +102,7 @@
    }
    // #ifdef APP-PLUS
    uni.downloadFile({
    const downloadTask = uni.downloadFile({
      url: normalizedRemoteUrl,
      success: (downloadResult) => {
        if (downloadResult.statusCode !== 200 || !downloadResult.tempFilePath) {
@@ -121,6 +122,17 @@
        reject(error);
      },
    });
    if (downloadTask && typeof downloadTask.onProgressUpdate === 'function' && typeof onProgress === 'function') {
      // 统一把原生下载进度透传出去,方便调用方展示更细的 UI 状态。
      downloadTask.onProgressUpdate((progressEvent) => {
        onProgress({
          progress: Number(progressEvent?.progress || 0),
          totalBytesWritten: Number(progressEvent?.totalBytesWritten || 0),
          totalBytesExpectedToWrite: Number(progressEvent?.totalBytesExpectedToWrite || 0),
        });
      });
    }
    // #endif
    // #ifndef APP-PLUS