diff --git a/src/App.vue b/src/App.vue index 5d45794..313a2ea 100644 --- a/src/App.vue +++ b/src/App.vue @@ -27,6 +27,7 @@ } const VERSION_CHECK_INTERVAL = 5 * 60 * 1000 + const VERSION_PROMPT_STORAGE_KEY = 'admin-xm-iot:confirmed-version-update' let versionCheckTimer: number | undefined let currentEntrySignature = '' let upgradePromptVisible = false @@ -78,18 +79,69 @@ } const getLoadedEntrySignature = () => { - return Array.from(document.querySelectorAll('script[src],link[href]')) + return Array.from( + document.querySelectorAll('script[src],link[href]') + ) .map((element) => ('src' in element ? element.src : element.href)) .filter((url) => /\/assets\/.*\.(js|css)(\?|$)/.test(url)) .sort() .join('|') } + const getStringHash = (value: string) => { + let hash = 0 + + for (let i = 0; i < value.length; i++) { + hash = (hash << 5) - hash + value.charCodeAt(i) + hash |= 0 + } + + return Math.abs(hash).toString(36) + } + + const getBrowserFingerprint = () => { + return getStringHash( + [ + navigator.userAgent, + navigator.language, + navigator.platform, + Intl.DateTimeFormat().resolvedOptions().timeZone, + `${screen.width}x${screen.height}x${screen.colorDepth}` + ].join('|') + ) + } + + const getConfirmedVersionKey = (signature: string) => { + return `${getBrowserFingerprint()}:${getStringHash(signature)}` + } + + const getConfirmedVersionUpdate = () => { + try { + return localStorage.getItem(VERSION_PROMPT_STORAGE_KEY) + } catch { + return null + } + } + + const setConfirmedVersionUpdate = (signature: string) => { + try { + localStorage.setItem(VERSION_PROMPT_STORAGE_KEY, getConfirmedVersionKey(signature)) + } catch { + // localStorage may be unavailable in private mode. + } + } + + const reloadWithCacheBuster = () => { + const url = new URL(window.location.href) + url.searchParams.set('_t', Date.now().toString()) + window.location.replace(url.toString()) + } + const checkVersionUpdate = async () => { if (upgradePromptVisible || document.hidden) return try { - const response = await fetch(`${window.location.origin}${import.meta.env.BASE_URL}`, { + const response = await fetch(import.meta.env.BASE_URL, { cache: 'no-store' }) const html = await response.text() @@ -103,14 +155,21 @@ } if (currentEntrySignature !== latestSignature) { + if (getConfirmedVersionUpdate() === getConfirmedVersionKey(latestSignature)) { + currentEntrySignature = latestSignature + return + } + upgradePromptVisible = true await ElMessageBox.alert('系统已发布新版本,请刷新页面后继续使用。', '发现新版本', { - confirmButtonText: '立即刷新', + confirmButtonText: '立即更新', type: 'warning', closeOnClickModal: false, closeOnPressEscape: false }) - window.location.reload() + setConfirmedVersionUpdate(latestSignature) + currentEntrySignature = latestSignature + reloadWithCacheBuster() } } catch (error) { console.warn('检查版本更新失败:', error) diff --git a/src/types/components.d.ts b/src/types/components.d.ts index e4f441c..e69de29 100644 --- a/src/types/components.d.ts +++ b/src/types/components.d.ts @@ -1,162 +0,0 @@ -/* eslint-disable */ -// @ts-nocheck -// Generated by unplugin-vue-components -// Read more: https://github.com/vuejs/core/pull/3399 -export {} - -/* prettier-ignore */ -declare module 'vue' { - export interface GlobalComponents { - AgentSelector: typeof import('./../components/business/AgentSelector.vue')['default'] - ArtBackToTop: typeof import('./../components/core/base/ArtBackToTop.vue')['default'] - ArtBarChart: typeof import('./../components/core/charts/ArtBarChart.vue')['default'] - ArtBarChartCard: typeof import('./../components/core/cards/ArtBarChartCard.vue')['default'] - ArtBasicBanner: typeof import('./../components/core/banners/ArtBasicBanner.vue')['default'] - ArtBreadcrumb: typeof import('./../components/core/layouts/art-breadcrumb/index.vue')['default'] - ArtButtonMore: typeof import('./../components/core/forms/ArtButtonMore.vue')['default'] - ArtButtonTable: typeof import('./../components/core/forms/ArtButtonTable.vue')['default'] - ArtCardBanner: typeof import('./../components/core/banners/ArtCardBanner.vue')['default'] - ArtChatWindow: typeof import('./../components/core/layouts/art-chat-window/index.vue')['default'] - ArtCutterImg: typeof import('./../components/core/media/ArtCutterImg.vue')['default'] - ArtDataListCard: typeof import('./../components/core/cards/ArtDataListCard.vue')['default'] - ArtDataViewer: typeof import('./../components/core/views/ArtDataViewer.vue')['default'] - ArtDonutChartCard: typeof import('./../components/core/cards/ArtDonutChartCard.vue')['default'] - ArtDragVerify: typeof import('./../components/core/forms/ArtDragVerify.vue')['default'] - ArtDualBarCompareChart: typeof import('./../components/core/charts/ArtDualBarCompareChart.vue')['default'] - ArtExcelExport: typeof import('./../components/core/forms/ArtExcelExport.vue')['default'] - ArtExcelImport: typeof import('./../components/core/forms/ArtExcelImport.vue')['default'] - ArtException: typeof import('./../components/core/views/exception/ArtException.vue')['default'] - ArtFastEnter: typeof import('./../components/core/layouts/art-fast-enter/index.vue')['default'] - ArtFestivalTextScroll: typeof import('./../components/core/text-effect/ArtFestivalTextScroll.vue')['default'] - ArtFireworksEffect: typeof import('./../components/core/layouts/art-fireworks-effect/index.vue')['default'] - ArtGlobalSearch: typeof import('./../components/core/layouts/art-global-search/index.vue')['default'] - ArtHBarChart: typeof import('./../components/core/charts/ArtHBarChart.vue')['default'] - ArtHeaderBar: typeof import('./../components/core/layouts/art-header-bar/index.vue')['default'] - ArtHorizontalMenu: typeof import('./../components/core/layouts/art-menus/art-horizontal-menu/index.vue')['default'] - ArtIconSelector: typeof import('./../components/core/base/ArtIconSelector.vue')['default'] - ArtImageCard: typeof import('./../components/core/cards/ArtImageCard.vue')['default'] - ArtKLineChart: typeof import('./../components/core/charts/ArtKLineChart.vue')['default'] - ArtLayouts: typeof import('./../components/core/layouts/art-layouts/index.vue')['default'] - ArtLineChart: typeof import('./../components/core/charts/ArtLineChart.vue')['default'] - ArtLineChartCard: typeof import('./../components/core/cards/ArtLineChartCard.vue')['default'] - ArtLogo: typeof import('./../components/core/base/ArtLogo.vue')['default'] - ArtMapChart: typeof import('./../components/core/charts/ArtMapChart.vue')['default'] - ArtMenuRight: typeof import('./../components/core/others/ArtMenuRight.vue')['default'] - ArtMixedMenu: typeof import('./../components/core/layouts/art-menus/art-mixed-menu/index.vue')['default'] - ArtNotification: typeof import('./../components/core/layouts/art-notification/index.vue')['default'] - ArtPageContent: typeof import('./../components/core/layouts/art-page-content/index.vue')['default'] - ArtProgressCard: typeof import('./../components/core/cards/ArtProgressCard.vue')['default'] - ArtRadarChart: typeof import('./../components/core/charts/ArtRadarChart.vue')['default'] - ArtResultPage: typeof import('./../components/core/views/result/ArtResultPage.vue')['default'] - ArtRingChart: typeof import('./../components/core/charts/ArtRingChart.vue')['default'] - ArtScatterChart: typeof import('./../components/core/charts/ArtScatterChart.vue')['default'] - ArtScreenLock: typeof import('./../components/core/layouts/art-screen-lock/index.vue')['default'] - ArtSearchBar: typeof import('./../components/core/forms/art-search-bar/index.vue')['default'] - ArtSearchDate: typeof import('./../components/core/forms/art-search-bar/widget/art-search-date/index.vue')['default'] - ArtSearchInput: typeof import('./../components/core/forms/art-search-bar/widget/art-search-input/index.vue')['default'] - ArtSearchRadio: typeof import('./../components/core/forms/art-search-bar/widget/art-search-radio/index.vue')['default'] - ArtSearchSelect: typeof import('./../components/core/forms/art-search-bar/widget/art-search-select/index.vue')['default'] - ArtSearchTreeSelect: typeof import('./../components/core/forms/art-search-bar/widget/art-search-tree-select/index.vue')['default'] - ArtSettingsPanel: typeof import('./../components/core/layouts/art-settings-panel/index.vue')['default'] - ArtSidebarMenu: typeof import('./../components/core/layouts/art-menus/art-sidebar-menu/index.vue')['default'] - ArtStatsCard: typeof import('./../components/core/cards/ArtStatsCard.vue')['default'] - ArtTable: typeof import('./../components/core/tables/ArtTable.vue')['default'] - ArtTableFullScreen: typeof import('./../components/core/tables/ArtTableFullScreen.vue')['default'] - ArtTableHeader: typeof import('./../components/core/tables/ArtTableHeader.vue')['default'] - ArtTextScroll: typeof import('./../components/core/text-effect/ArtTextScroll.vue')['default'] - ArtTimelineListCard: typeof import('./../components/core/cards/ArtTimelineListCard.vue')['default'] - ArtVideoPlayer: typeof import('./../components/core/media/ArtVideoPlayer.vue')['default'] - ArtWangEditor: typeof import('./../components/core/forms/ArtWangEditor.vue')['default'] - ArtWatermark: typeof import('./../components/core/others/ArtWatermark.vue')['default'] - ArtWorkTab: typeof import('./../components/core/layouts/art-work-tab/index.vue')['default'] - BasicSettings: typeof import('./../components/core/layouts/art-settings-panel/widget/BasicSettings.vue')['default'] - BatchOperationDialog: typeof import('./../components/business/BatchOperationDialog.vue')['default'] - BoxStyleSettings: typeof import('./../components/core/layouts/art-settings-panel/widget/BoxStyleSettings.vue')['default'] - CardOperationDialog: typeof import('./../components/business/CardOperationDialog.vue')['default'] - CardStatusTag: typeof import('./../components/business/CardStatusTag.vue')['default'] - ChunkErrorBoundary: typeof import('./../components/core/others/ChunkErrorBoundary.vue')['default'] - CodeGeneratorButton: typeof import('./../components/business/CodeGeneratorButton.vue')['default'] - ColorSettings: typeof import('./../components/core/layouts/art-settings-panel/widget/ColorSettings.vue')['default'] - CommentItem: typeof import('./../components/custom/comment-widget/widget/CommentItem.vue')['default'] - CommentWidget: typeof import('./../components/custom/comment-widget/index.vue')['default'] - CommissionDisplay: typeof import('./../components/business/CommissionDisplay.vue')['default'] - ContainerSettings: typeof import('./../components/core/layouts/art-settings-panel/widget/ContainerSettings.vue')['default'] - CreateRefundDialog: typeof import('./../components/business/CreateRefundDialog.vue')['default'] - CustomerAccountDialog: typeof import('./../components/business/CustomerAccountDialog.vue')['default'] - DetailPage: typeof import('./../components/common/DetailPage.vue')['default'] - ElAlert: typeof import('element-plus/es')['ElAlert'] - ElAvatar: typeof import('element-plus/es')['ElAvatar'] - ElButton: typeof import('element-plus/es')['ElButton'] - ElCalendar: typeof import('element-plus/es')['ElCalendar'] - ElCard: typeof import('element-plus/es')['ElCard'] - ElCascader: typeof import('element-plus/es')['ElCascader'] - ElCheckbox: typeof import('element-plus/es')['ElCheckbox'] - ElCol: typeof import('element-plus/es')['ElCol'] - ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider'] - ElDatePicker: typeof import('element-plus/es')['ElDatePicker'] - ElDescriptions: typeof import('element-plus/es')['ElDescriptions'] - ElDescriptionsItem: typeof import('element-plus/es')['ElDescriptionsItem'] - ElDialog: typeof import('element-plus/es')['ElDialog'] - ElDivider: typeof import('element-plus/es')['ElDivider'] - ElDrawer: typeof import('element-plus/es')['ElDrawer'] - ElDropdown: typeof import('element-plus/es')['ElDropdown'] - ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem'] - ElDropdownMenu: typeof import('element-plus/es')['ElDropdownMenu'] - ElEmpty: typeof import('element-plus/es')['ElEmpty'] - ElForm: typeof import('element-plus/es')['ElForm'] - ElFormItem: typeof import('element-plus/es')['ElFormItem'] - ElIcon: typeof import('element-plus/es')['ElIcon'] - ElInput: typeof import('element-plus/es')['ElInput'] - ElInputNumber: typeof import('element-plus/es')['ElInputNumber'] - ElMenu: typeof import('element-plus/es')['ElMenu'] - ElMenuItem: typeof import('element-plus/es')['ElMenuItem'] - ElOption: typeof import('element-plus/es')['ElOption'] - ElPagination: typeof import('element-plus/es')['ElPagination'] - ElPopover: typeof import('element-plus/es')['ElPopover'] - ElProgress: typeof import('element-plus/es')['ElProgress'] - ElRadio: typeof import('element-plus/es')['ElRadio'] - ElRadioButton: typeof import('element-plus/es')['ElRadioButton'] - ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup'] - ElRow: typeof import('element-plus/es')['ElRow'] - ElScrollbar: typeof import('element-plus/es')['ElScrollbar'] - ElSelect: typeof import('element-plus/es')['ElSelect'] - ElSubMenu: typeof import('element-plus/es')['ElSubMenu'] - ElSwitch: typeof import('element-plus/es')['ElSwitch'] - ElTable: typeof import('element-plus/es')['ElTable'] - ElTableColumn: typeof import('element-plus/es')['ElTableColumn'] - ElTabPane: typeof import('element-plus/es')['ElTabPane'] - ElTabs: typeof import('element-plus/es')['ElTabs'] - ElTag: typeof import('element-plus/es')['ElTag'] - ElTimeline: typeof import('element-plus/es')['ElTimeline'] - ElTimelineItem: typeof import('element-plus/es')['ElTimelineItem'] - ElTooltip: typeof import('element-plus/es')['ElTooltip'] - ElTreeSelect: typeof import('element-plus/es')['ElTreeSelect'] - ElUpload: typeof import('element-plus/es')['ElUpload'] - ElWatermark: typeof import('element-plus/es')['ElWatermark'] - HorizontalSubmenu: typeof import('./../components/core/layouts/art-menus/art-horizontal-menu/widget/HorizontalSubmenu.vue')['default'] - ImportDialog: typeof import('./../components/business/ImportDialog.vue')['default'] - LoginLeftView: typeof import('./../components/core/views/login/LoginLeftView.vue')['default'] - MenuLayoutSettings: typeof import('./../components/core/layouts/art-settings-panel/widget/MenuLayoutSettings.vue')['default'] - MenuStyleSettings: typeof import('./../components/core/layouts/art-settings-panel/widget/MenuStyleSettings.vue')['default'] - OperationLogsDialog: typeof import('./../components/business/OperationLogsDialog.vue')['default'] - OperatorSelect: typeof import('./../components/business/OperatorSelect.vue')['default'] - PackageSelector: typeof import('./../components/business/PackageSelector.vue')['default'] - PaymentVoucherDialog: typeof import('./../components/business/PaymentVoucherDialog.vue')['default'] - RealnamePolicyDialog: typeof import('./../components/device/RealnamePolicyDialog.vue')['default'] - RouterLink: typeof import('vue-router')['RouterLink'] - RouterView: typeof import('vue-router')['RouterView'] - SectionTitle: typeof import('./../components/core/layouts/art-settings-panel/widget/SectionTitle.vue')['default'] - SettingDrawer: typeof import('./../components/core/layouts/art-settings-panel/widget/SettingDrawer.vue')['default'] - SettingHeader: typeof import('./../components/core/layouts/art-settings-panel/widget/SettingHeader.vue')['default'] - SettingItem: typeof import('./../components/core/layouts/art-settings-panel/widget/SettingItem.vue')['default'] - SidebarSubmenu: typeof import('./../components/core/layouts/art-menus/art-sidebar-menu/widget/SidebarSubmenu.vue')['default'] - SpeedLimitDialog: typeof import('./../components/device/SpeedLimitDialog.vue')['default'] - SwitchCardDialog: typeof import('./../components/device/SwitchCardDialog.vue')['default'] - TableContextMenuHint: typeof import('./../components/core/others/TableContextMenuHint.vue')['default'] - ThemeSettings: typeof import('./../components/core/layouts/art-settings-panel/widget/ThemeSettings.vue')['default'] - UpdateRealnameStatusDialog: typeof import('./../components/business/UpdateRealnameStatusDialog.vue')['default'] - } - export interface ComponentCustomProperties { - vLoading: typeof import('element-plus/es')['ElLoadingDirective'] - } -}