diff --git a/ckApp/pages/index/index.vue b/ckApp/pages/index/index.vue index 7d0341d2..50698c36 100644 --- a/ckApp/pages/index/index.vue +++ b/ckApp/pages/index/index.vue @@ -44,25 +44,6 @@ this.iframeUrl = queryString ? `${this.baseUrl}?${queryString}` : this.baseUrl; }, // 发送消息到 iframe(通过URL传参) - async sendMessageToIframe() { - const paddingTop = await getTopSafeAreaHeightAsync(); - this.messageId++; - const message = { - type: TYPE_EMUE.DATA, // 数据类型:0数据交互 1App功能调用 - data: { - id: this.messageId, - content: `Hello,我是 App 发送的消息 ${this.messageId}`, - timestamp: Date.now(), - paddingTop: paddingTop - } - }; - - // 将消息添加到URL参数中 - this.urlParams.message = encodeURIComponent(JSON.stringify(message)); - this.buildIframeUrl(); - console.log('[App]SendMessage=>\n' + JSON.stringify(message)); - }, - // 发送消息到 iframe(通过URL传参) async sendBaseConfig() { const message = { type: TYPE_EMUE.CONFIG, @@ -71,6 +52,7 @@ appId: '1234567890', appName: '存客宝', appVersion: '1.0.0', + isAppMode:true } }; @@ -91,9 +73,26 @@ break; case TYPE_EMUE.FUNCTION: console.log('[App]ReceiveMessage=>\n' + JSON.stringify(ResDetail.data)); + if (ResDetail.data.action === 'clearCache') { + this.clearCache(); + } break; } + }, + clearCache() { + // 清除 webview 缓存 + if (this.$refs.webviewRef) { + // 重新加载 webview + this.$refs.webviewRef.reload(); + } + // 清除 webview 缓存数据 + uni.clearStorage({ + success: () => { + console.log('Webview 缓存已清除'); + } + }); + } } } diff --git a/nkebao/src/App.tsx b/nkebao/src/App.tsx index bfee5515..01bf3208 100644 --- a/nkebao/src/App.tsx +++ b/nkebao/src/App.tsx @@ -1,9 +1,12 @@ import React from "react"; import AppRouter from "@/router"; +import UpdateNotification from "@/components/UpdateNotification"; + function App() { return ( <> + ); } diff --git a/nkebao/src/components/UpdateNotification/index.tsx b/nkebao/src/components/UpdateNotification/index.tsx new file mode 100644 index 00000000..86480615 --- /dev/null +++ b/nkebao/src/components/UpdateNotification/index.tsx @@ -0,0 +1,180 @@ +import React, { useState, useEffect } from "react"; +import { Button } from "antd-mobile"; +import { updateChecker } from "@/utils/updateChecker"; +import { + ReloadOutlined, + CloudDownloadOutlined, + RocketOutlined, +} from "@ant-design/icons"; + +interface UpdateNotificationProps { + position?: "top" | "bottom"; + autoReload?: boolean; + showToast?: boolean; +} + +const UpdateNotification: React.FC = ({ + position = "top", + autoReload = false, + showToast = true, +}) => { + const [hasUpdate, setHasUpdate] = useState(false); + const [isVisible, setIsVisible] = useState(false); + + useEffect(() => { + // 注册更新检测回调 + const handleUpdate = (info: { hasUpdate: boolean }) => { + if (info.hasUpdate) { + setHasUpdate(true); + setIsVisible(true); + + if (autoReload) { + // 自动刷新 + setTimeout(() => { + updateChecker.forceReload(); + }, 3000); + } + } + }; + + updateChecker.onUpdate(handleUpdate); + + // 启动更新检测 + updateChecker.start(); + + return () => { + updateChecker.offUpdate(handleUpdate); + updateChecker.stop(); + }; + }, [autoReload, showToast]); + const handleReload = () => { + updateChecker.forceReload(); + }; + + // if (!isVisible || !hasUpdate) { + // return null; + // } + + return ( +
+ {/* 背景装饰 */} +
+ +
+ + {/* 主要内容 */} +
+ {/* 图标 */} +
+ +
+ + {/* 标题 */} +
+ 发现新版本 +
+ + {/* 描述 */} +
+ 为了给您提供更好的体验,请更新到最新版本 +
+ + {/* 更新按钮 */} + + + {/* 提示文字 */} +
+ 更新将自动重启应用 +
+
+ + {/* 动画样式 */} + +
+ ); +}; + +export default UpdateNotification; diff --git a/nkebao/src/pages/iframe/init.tsx b/nkebao/src/pages/iframe/init.tsx index 51185a38..b9e1fda0 100644 --- a/nkebao/src/pages/iframe/init.tsx +++ b/nkebao/src/pages/iframe/init.tsx @@ -4,7 +4,13 @@ import Layout from "@/components/Layout/Layout"; import NavCommon from "@/components/NavCommon"; import { Input } from "antd"; import { useNavigate } from "react-router-dom"; - +import { useSettingsStore } from "@/store/module/settings"; +import { + sendMessageToParent, + parseUrlMessage, + Message, + TYPE_EMUE, +} from "@/utils/postApp"; // 声明全局的 uni 对象 declare global { interface Window { @@ -12,55 +18,18 @@ declare global { } } -interface Message { - type: number; // 数据类型:0数据交互 1App功能调用 - data: any; -} - -const TYPE_EMUE = { - CONNECT: 0, - DATA: 1, - FUNCTION: 2, - CONFIG: 3, -}; const IframeDebugPage: React.FC = () => { + const { setSettings } = useSettingsStore(); const [receivedMessages, setReceivedMessages] = useState([]); const [messageId, setMessageId] = useState(0); const [inputMessage, setInputMessage] = useState(""); const navigate = useNavigate(); // 解析 URL 参数中的消息 - const parseUrlMessage = () => { - const search = window.location.search.substring(1); - let messageParam = null; - - if (search) { - const pairs = search.split("&"); - for (const pair of pairs) { - const [key, value] = pair.split("="); - if (key === "message" && value) { - messageParam = decodeURIComponent(value); - break; - } - } + parseUrlMessage().then(message => { + if (message) { + handleReceivedMessage(message); } - - if (messageParam) { - try { - const message = JSON.parse(decodeURIComponent(messageParam)); - console.log("[存客宝]ReceiveMessage=>\n" + JSON.stringify(message)); - handleReceivedMessage(message); - // 清除URL中的message参数 - const newUrl = - window.location.pathname + - window.location.search - .replace(/[?&]message=[^&]*/, "") - .replace(/^&/, "?"); - window.history.replaceState({}, "", newUrl); - } catch (e) { - console.error("解析URL消息失败:", e); - } - } - }; + }); useEffect(() => { parseUrlMessage(); @@ -71,35 +40,19 @@ const IframeDebugPage: React.FC = () => { const handleReceivedMessage = (message: Message) => { const messageText = `[${new Date().toLocaleTimeString()}] 收到: ${JSON.stringify(message)}`; setReceivedMessages(prev => [...prev, messageText]); - console.log("message.type", message.type); if ([TYPE_EMUE.CONFIG].includes(message.type)) { - localStorage.setItem("paddingTop", message.data.paddingTop); - localStorage.setItem("isAppMode", "true"); + const { paddingTop, appId, appName, appVersion } = message.data; + setSettings({ + paddingTop, + appId, + appName, + appVersion, + isAppMode: true, + }); navigate("/"); } }; - // 向 App 发送消息 - const sendMessageToParent = (message: Message) => { - if (window.uni && window.uni.postMessage) { - try { - window.uni.postMessage({ - data: message, - }); - console.log("[存客宝]SendMessage=>\n" + JSON.stringify(message)); - } catch (e) { - console.error( - "[存客宝]SendMessage=>\n" + JSON.stringify(message) + "发送失败:", - e, - ); - } - } else { - console.error( - "[存客宝]SendMessage=>\n" + JSON.stringify(message) + "无法发送消息", - ); - } - }; - // 发送自定义消息到 App const sendCustomMessage = () => { if (!inputMessage.trim()) return; @@ -107,17 +60,14 @@ const IframeDebugPage: React.FC = () => { const newMessageId = messageId + 1; setMessageId(newMessageId); - const message: Message = { - type: TYPE_EMUE.DATA, // 数据交互 - data: { - id: newMessageId, - content: inputMessage, - source: "存客宝消息源", - timestamp: Date.now(), - }, + const message = { + id: newMessageId, + content: inputMessage, + source: "存客宝消息源", + timestamp: Date.now(), }; - sendMessageToParent(message); + sendMessageToParent(message, TYPE_EMUE.DATA); setInputMessage(""); }; @@ -126,33 +76,27 @@ const IframeDebugPage: React.FC = () => { const newMessageId = messageId + 1; setMessageId(newMessageId); - const message: Message = { - type: TYPE_EMUE.DATA, // 数据交互 - data: { - id: newMessageId, - action: "ping", - content: `存客宝测试消息 ${newMessageId}`, - random: Math.random(), - }, + const message = { + id: newMessageId, + action: "ping", + content: `存客宝测试消息 ${newMessageId}`, + random: Math.random(), }; - sendMessageToParent(message); + sendMessageToParent(message, TYPE_EMUE.DATA); }; // 发送App功能调用消息 const sendAppFunctionCall = () => { - const message: Message = { - type: 1, // App功能调用 - data: { - action: "showToast", - params: { - title: "来自H5的功能调用", - icon: "success", - }, + const message = { + action: "showToast", + params: { + title: "来自H5的功能调用", + icon: "success", }, }; - sendMessageToParent(message); + sendMessageToParent(message, TYPE_EMUE.FUNCTION); }; // 清空消息列表 diff --git a/nkebao/src/pages/login/login.tsx b/nkebao/src/pages/login/login.tsx index c9d83114..6617e26b 100644 --- a/nkebao/src/pages/login/login.tsx +++ b/nkebao/src/pages/login/login.tsx @@ -1,5 +1,5 @@ import React, { useState, useEffect } from "react"; -import { useNavigate, useSearchParams } from "react-router-dom"; + import { Form, Input, Button, Toast, Checkbox } from "antd-mobile"; import { EyeInvisibleOutline, @@ -9,8 +9,6 @@ import { import { useUserStore } from "@/store/module/user"; import { loginWithPassword, loginWithCode, sendVerificationCode } from "./api"; import style from "./login.module.scss"; -import Layout from "@/components/Layout/Layout"; -import NavCommon from "@/components/NavCommon"; const Login: React.FC = () => { const [form] = Form.useForm(); @@ -20,8 +18,6 @@ const Login: React.FC = () => { const [showPassword, setShowPassword] = useState(false); const [agreeToTerms, setAgreeToTerms] = useState(false); - const navigate = useNavigate(); - const [searchParams] = useSearchParams(); const { login } = useUserStore(); // 倒计时效果 @@ -32,16 +28,6 @@ const Login: React.FC = () => { } }, [countdown]); - // 检查URL是否为登录页面 - const isLoginPage = (url: string) => { - try { - const urlObj = new URL(url, window.location.origin); - return urlObj.pathname === "/login" || urlObj.pathname.endsWith("/login"); - } catch { - return false; - } - }; - // 发送验证码 const handleSendVerificationCode = async () => { const account = form.getFieldValue("account"); @@ -95,24 +81,12 @@ const Login: React.FC = () => { } else { response = await loginWithCode(loginParams); } - console.log(response, "response"); // 获取设备总数 const deviceTotal = response.deviceTotal || 0; - console.log(deviceTotal, "deviceTotal"); // 更新状态管理(token会自动存储到localStorage,用户信息存储在状态管理中) login(response.token, response.member, deviceTotal); - - Toast.show({ content: "登录成功", position: "top" }); - - // 根据设备数量判断跳转 - if (deviceTotal > 0) { - navigate("/"); - } else { - // 没有设备,跳转到引导页面 - navigate("/guide"); - } } catch (error: any) { // 错误已在request中处理,这里不需要额外处理 } finally { diff --git a/nkebao/src/pages/mobile/home/index.tsx b/nkebao/src/pages/mobile/home/index.tsx index 3293455c..60244b37 100644 --- a/nkebao/src/pages/mobile/home/index.tsx +++ b/nkebao/src/pages/mobile/home/index.tsx @@ -18,6 +18,7 @@ import { getDashboard, } from "./api"; import style from "./index.module.scss"; +import UpdateNotification from "@/components/UpdateNotification"; interface DashboardData { deviceNum?: number; @@ -253,6 +254,7 @@ const Home: React.FC = () => { + ); }; diff --git a/nkebao/src/pages/mobile/mine/devices/index.tsx b/nkebao/src/pages/mobile/mine/devices/index.tsx index 444cfea3..6778bc7c 100644 --- a/nkebao/src/pages/mobile/mine/devices/index.tsx +++ b/nkebao/src/pages/mobile/mine/devices/index.tsx @@ -250,7 +250,7 @@ const Devices: React.FC = () => { } footer={ -
+
{
- logo + logo
diff --git a/nkebao/src/pages/mobile/mine/setting/index.tsx b/nkebao/src/pages/mobile/mine/setting/index.tsx index 8090bdc2..a15424bf 100644 --- a/nkebao/src/pages/mobile/mine/setting/index.tsx +++ b/nkebao/src/pages/mobile/mine/setting/index.tsx @@ -1,6 +1,6 @@ import React, { useState } from "react"; import { useNavigate } from "react-router-dom"; -import { NavBar, List, Switch, Button, Dialog, Toast, Card } from "antd-mobile"; +import { List, Switch, Button, Dialog, Toast, Card } from "antd-mobile"; import { UserOutlined, SafetyOutlined, @@ -8,14 +8,16 @@ import { LogoutOutlined, SettingOutlined, LockOutlined, - HeartOutlined, - StarOutlined, + ReloadOutlined, } from "@ant-design/icons"; import Layout from "@/components/Layout/Layout"; import { useUserStore } from "@/store/module/user"; import { useSettingsStore } from "@/store/module/settings"; import style from "./index.module.scss"; import NavCommon from "@/components/NavCommon"; +import { sendMessageToParent, TYPE_EMUE } from "@/utils/postApp"; +import { updateChecker } from "@/utils/updateChecker"; + interface SettingItem { id: string; title: string; @@ -32,7 +34,7 @@ interface SettingItem { const Setting: React.FC = () => { const navigate = useNavigate(); const { user, logout } = useUserStore(); - const { settings, updateSetting } = useSettingsStore(); + const { settings } = useSettingsStore(); const [showLogoutDialog, setShowLogoutDialog] = useState(false); const [avatarError, setAvatarError] = useState(false); @@ -57,13 +59,30 @@ const Setting: React.FC = () => { Dialog.confirm({ content: "确定要清除缓存吗?这将清除所有本地数据。", onConfirm: () => { - localStorage.clear(); - sessionStorage.clear(); + sendMessageToParent( + { + action: "clearCache", + }, + TYPE_EMUE.FUNCTION, + ); + }, + }); + }; + + // 在设置页面添加手动检查更新功能 + const handleCheckUpdate = () => { + updateChecker.checkForUpdate().then(result => { + if (result.hasUpdate) { Toast.show({ - content: "缓存已清除", + content: "发现新版本,请刷新页面", position: "top", }); - }, + } else { + Toast.show({ + content: "当前已是最新版本", + position: "top", + }); + } }); }; @@ -114,6 +133,15 @@ const Setting: React.FC = () => { color: "var(--primary-color)", badge: "2.3MB", }, + { + id: "checkUpdate", + title: "检查更新", + description: "检查应用是否有新版本", + icon: , + type: "button", + onClick: handleCheckUpdate, + color: "var(--primary-color)", + }, ], }, { @@ -258,12 +286,14 @@ const Setting: React.FC = () => {
- +
存客宝
-
版本 3.0.0
-
Build 2025-7-30
+
+ 版本 {settings.appVersion} +
+
Build 2025-08-04
diff --git a/nkebao/src/pages/mobile/scenarios/plan/list/index.tsx b/nkebao/src/pages/mobile/scenarios/plan/list/index.tsx index cac548d8..230bf532 100644 --- a/nkebao/src/pages/mobile/scenarios/plan/list/index.tsx +++ b/nkebao/src/pages/mobile/scenarios/plan/list/index.tsx @@ -65,23 +65,6 @@ const ScenarioList: React.FC = () => { const [total, setTotal] = useState(0); const pageSize = 20; - // 获取渠道中文名称 - const getChannelName = (channel: string) => { - const channelMap: Record = { - douyin: "抖音直播获客", - kuaishou: "快手直播获客", - xiaohongshu: "小红书种草获客", - weibo: "微博话题获客", - haibao: "海报扫码获客", - phone: "电话号码获客", - gongzhonghao: "公众号引流获客", - weixinqun: "微信群裂变获客", - payment: "付款码获客", - api: "API接口获客", - }; - return channelMap[channel] || `${channel}获客`; - }; - // 获取计划列表数据 const fetchPlanList = async (page: number, isLoadMore: boolean = false) => { if (!scenarioId) return; @@ -409,7 +392,7 @@ const ScenarioList: React.FC = () => { } loading={loading} footer={ -
+
( diff --git a/nkebao/src/store/module/user.ts b/nkebao/src/store/module/user.ts index ae4f66d2..07838d87 100644 --- a/nkebao/src/store/module/user.ts +++ b/nkebao/src/store/module/user.ts @@ -1,4 +1,5 @@ import { createPersistStore } from "@/store/createPersistStore"; +import { Toast } from "antd-mobile"; export interface User { id: number; @@ -60,6 +61,16 @@ export const useUserStore = createPersistStore( deviceTotal: deviceTotal, }; set({ user, token, isLoggedIn: true }); + + Toast.show({ content: "登录成功", position: "top" }); + + // 根据设备数量判断跳转 + if (deviceTotal > 0) { + window.location.href = "/"; + } else { + // 没有设备,跳转到引导页面 + window.location.href = "/guide"; + } }, logout: () => { // 清除localStorage中的token diff --git a/nkebao/src/styles/global.scss b/nkebao/src/styles/global.scss index ca4e01d0..6e4995b9 100644 --- a/nkebao/src/styles/global.scss +++ b/nkebao/src/styles/global.scss @@ -264,3 +264,43 @@ button { align-items: center; gap: 6px; } +.pagination-container { + display: flex; + justify-content: center; + padding: 14px 0; + background: white; + border-radius: 12px; + margin-top: 16px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + + :global(.ant-pagination) { + .ant-pagination-item { + border-radius: 6px; + border: 1px solid #d9d9d9; + + &:hover { + border-color: var(--primary-color); + } + + &.ant-pagination-item-active { + background: var(--primary-color); + border-color: var(--primary-color); + + a { + color: white; + } + } + } + + .ant-pagination-prev, + .ant-pagination-next { + border-radius: 6px; + border: 1px solid #d9d9d9; + + &:hover { + border-color: var(--primary-color); + color: var(--primary-color); + } + } + } +} diff --git a/nkebao/src/utils/common.ts b/nkebao/src/utils/common.ts index ebba36f1..995b9c05 100644 --- a/nkebao/src/utils/common.ts +++ b/nkebao/src/utils/common.ts @@ -1,5 +1,5 @@ import { Modal } from "antd-mobile"; - +import { getSetting } from "@/store/module/settings"; /** * 通用js调用弹窗,Promise风格 * @param content 弹窗内容 @@ -49,7 +49,7 @@ export function getSafeAreaHeight() { // 2. 设备检测 const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent); const isAndroid = /Android/.test(navigator.userAgent); - const isAppMode = Boolean(localStorage.getItem("isAppMode")); + const isAppMode = getSetting("isAppMode"); if (isIOS && isAppMode) { // iOS 设备 const isIPhoneX = window.screen.height >= 812; diff --git a/nkebao/src/utils/postApp.ts b/nkebao/src/utils/postApp.ts new file mode 100644 index 00000000..0ed8ab2d --- /dev/null +++ b/nkebao/src/utils/postApp.ts @@ -0,0 +1,72 @@ +export interface Message { + type: number; // 数据类型:0数据交互 1App功能调用 + data: any; +} +export const TYPE_EMUE = { + CONNECT: 0, + DATA: 1, + FUNCTION: 2, + CONFIG: 3, +}; +// 向 App 发送消息 +export const sendMessageToParent = (message: any, type: number) => { + const params: Message = { + type: type, + data: message, + }; + + if (window.uni && window.uni.postMessage) { + try { + window.uni.postMessage({ + data: params, + }); + console.log("[存客宝]SendMessage=>\n" + JSON.stringify(params)); + } catch (e) { + console.error( + "[存客宝]SendMessage=>\n" + JSON.stringify(params) + "发送失败:", + e, + ); + } + } else { + console.error( + "[存客宝]SendMessage=>\n" + JSON.stringify(params) + "无法发送消息", + ); + } +}; +// 解析 URL 参数中的消息 +export const parseUrlMessage = (): Promise => { + return new Promise((resolve, reject) => { + const search = window.location.search.substring(1); + let messageParam = null; + + if (search) { + const pairs = search.split("&"); + for (const pair of pairs) { + const [key, value] = pair.split("="); + if (key === "message" && value) { + messageParam = decodeURIComponent(value); + break; + } + } + } + + if (messageParam) { + try { + const message = JSON.parse(decodeURIComponent(messageParam)); + console.log("[存客宝]ReceiveMessage=>\n" + JSON.stringify(message)); + resolve(message); + // 清除URL中的message参数 + const newUrl = + window.location.pathname + + window.location.search + .replace(/[?&]message=[^&]*/, "") + .replace(/^&/, "?"); + window.history.replaceState({}, "", newUrl); + } catch (e) { + console.error("解析URL消息失败:", e); + reject(e); + } + } + reject(null); + }); +}; diff --git a/nkebao/src/utils/updateChecker.ts b/nkebao/src/utils/updateChecker.ts new file mode 100644 index 00000000..aca1700c --- /dev/null +++ b/nkebao/src/utils/updateChecker.ts @@ -0,0 +1,217 @@ +/** + * 应用更新检测工具 + */ + +interface UpdateInfo { + hasUpdate: boolean; + version?: string; + timestamp?: number; +} + +class UpdateChecker { + private currentVersion: string; + private checkInterval: number = 1000; // 1秒检查一次(用于测试) + private intervalId: NodeJS.Timeout | null = null; + private updateCallbacks: ((info: UpdateInfo) => void)[] = []; + private currentHashes: string[] = []; + + constructor() { + // 从package.json获取版本号 + this.currentVersion = import.meta.env.VITE_APP_VERSION || "1.0.0"; + // 初始化当前哈希值 + this.initCurrentHashes(); + } + + /** + * 初始化当前哈希值 + */ + private initCurrentHashes() { + // 从当前页面的资源中提取哈希值 + const scripts = document.querySelectorAll("script[src]"); + const links = document.querySelectorAll("link[href]"); + + const scriptHashes = Array.from(scripts) + .map(script => script.getAttribute("src")) + .filter( + src => src && (src.includes("assets/") || src.includes("/assets/")), + ) + .map(src => { + // 修改正则表达式,匹配包含字母、数字和下划线的哈希值 + const match = src?.match(/[a-zA-Z0-9_-]{8,}/); + return match ? match[0] : ""; + }) + .filter(hash => hash); + + const linkHashes = Array.from(links) + .map(link => link.getAttribute("href")) + .filter( + href => href && (href.includes("assets/") || href.includes("/assets/")), + ) + .map(href => { + // 修改正则表达式,匹配包含字母、数字和下划线的哈希值 + const match = href?.match(/[a-zA-Z0-9_-]{8,}/); + return match ? match[0] : ""; + }) + .filter(hash => hash); + + this.currentHashes = [...new Set([...scriptHashes, ...linkHashes])]; + } + + /** + * 开始检测更新 + */ + start() { + if (this.intervalId) { + return; + } + + // 立即检查一次 + this.checkForUpdate(); + + // 设置定时检查 + this.intervalId = setInterval(() => { + this.checkForUpdate(); + }, this.checkInterval); + } + + /** + * 停止检测更新 + */ + stop() { + if (this.intervalId) { + clearInterval(this.intervalId); + this.intervalId = null; + } + } + + /** + * 检查更新 + */ + async checkForUpdate(): Promise { + try { + // 获取新的manifest文件 + let manifestResponse; + let manifestPath = "/.vite/manifest.json"; + + try { + manifestResponse = await fetch(manifestPath, { + cache: "no-cache", + headers: { + "Cache-Control": "no-cache", + Pragma: "no-cache", + }, + }); + } catch (error) { + // 如果.vite路径失败,尝试根路径 + manifestPath = "/manifest.json"; + manifestResponse = await fetch(manifestPath, { + cache: "no-cache", + headers: { + "Cache-Control": "no-cache", + Pragma: "no-cache", + }, + }); + } + + if (!manifestResponse.ok) { + return { hasUpdate: false }; + } + + const manifest = await manifestResponse.json(); + + // 从Vite manifest中提取文件哈希 + const newHashes: string[] = []; + + Object.values(manifest).forEach((entry: any) => { + if (entry.file && entry.file.includes("assets/")) { + // console.log("处理manifest entry file:", entry.file); + // 修改正则表达式,匹配包含字母、数字和下划线的哈希值 + const match = entry.file.match(/[a-zA-Z0-9_-]{8,}/); + if (match) { + const hash = match[0]; + newHashes.push(hash); + } + } + // 也检查CSS文件 + if (entry.css) { + entry.css.forEach((cssFile: string) => { + if (cssFile.includes("assets/")) { + // console.log("处理manifest entry css:", cssFile); + // 修改正则表达式,匹配包含字母、数字和下划线的哈希值 + const match = cssFile.match(/[a-zA-Z0-9_-]{8,}/); + if (match) { + const hash = match[0]; + // console.log("提取的manifest css哈希:", hash); + newHashes.push(hash); + } + } + }); + } + }); + + // 去重新哈希值数组 + const uniqueNewHashes = [...new Set(newHashes)]; + + // 比较哈希值 + const hasUpdate = this.compareHashes(this.currentHashes, uniqueNewHashes); + + const updateInfo: UpdateInfo = { + hasUpdate, + version: manifest.version || this.currentVersion, + timestamp: Date.now(), + }; + + // 通知所有回调 + this.updateCallbacks.forEach(callback => callback(updateInfo)); + + return updateInfo; + } catch (error) { + return { hasUpdate: false }; + } + } + + /** + * 比较哈希值 + */ + private compareHashes(current: string[], newHashes: string[]): boolean { + if (current.length !== newHashes.length) { + return true; + } + + // 对两个数组进行排序后比较,忽略顺序 + const sortedCurrent = [...current].sort(); + const sortedNewHashes = [...newHashes].sort(); + + const hasUpdate = sortedCurrent.some((hash, index) => { + return hash !== sortedNewHashes[index]; + }); + + return hasUpdate; + } + + /** + * 注册更新回调 + */ + onUpdate(callback: (info: UpdateInfo) => void) { + this.updateCallbacks.push(callback); + } + + /** + * 移除更新回调 + */ + offUpdate(callback: (info: UpdateInfo) => void) { + const index = this.updateCallbacks.indexOf(callback); + if (index > -1) { + this.updateCallbacks.splice(index, 1); + } + } + + /** + * 强制刷新页面 + */ + forceReload() { + window.location.reload(); + } +} + +export const updateChecker = new UpdateChecker(); diff --git a/nkebao/vite.config.ts b/nkebao/vite.config.ts index eb0ce0a3..d03fd648 100644 --- a/nkebao/vite.config.ts +++ b/nkebao/vite.config.ts @@ -38,5 +38,13 @@ export default defineConfig({ minify: "esbuild", // 启用源码映射(可选,生产环境可以关闭) sourcemap: false, + // 生成manifest文件 + manifest: true, + }, + define: { + // 注入版本信息 + "import.meta.env.VITE_APP_VERSION": JSON.stringify( + process.env.npm_package_version, + ), }, });