FEAT => 本次更新项目为:
更新模块完成
This commit is contained in:
@@ -44,25 +44,6 @@
|
|||||||
this.iframeUrl = queryString ? `${this.baseUrl}?${queryString}` : this.baseUrl;
|
this.iframeUrl = queryString ? `${this.baseUrl}?${queryString}` : this.baseUrl;
|
||||||
},
|
},
|
||||||
// 发送消息到 iframe(通过URL传参)
|
// 发送消息到 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() {
|
async sendBaseConfig() {
|
||||||
const message = {
|
const message = {
|
||||||
type: TYPE_EMUE.CONFIG,
|
type: TYPE_EMUE.CONFIG,
|
||||||
@@ -71,6 +52,7 @@
|
|||||||
appId: '1234567890',
|
appId: '1234567890',
|
||||||
appName: '存客宝',
|
appName: '存客宝',
|
||||||
appVersion: '1.0.0',
|
appVersion: '1.0.0',
|
||||||
|
isAppMode:true
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -91,9 +73,26 @@
|
|||||||
break;
|
break;
|
||||||
case TYPE_EMUE.FUNCTION:
|
case TYPE_EMUE.FUNCTION:
|
||||||
console.log('[App]ReceiveMessage=>\n' + JSON.stringify(ResDetail.data));
|
console.log('[App]ReceiveMessage=>\n' + JSON.stringify(ResDetail.data));
|
||||||
|
if (ResDetail.data.action === 'clearCache') {
|
||||||
|
this.clearCache();
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
clearCache() {
|
||||||
|
// 清除 webview 缓存
|
||||||
|
if (this.$refs.webviewRef) {
|
||||||
|
// 重新加载 webview
|
||||||
|
this.$refs.webviewRef.reload();
|
||||||
|
}
|
||||||
|
// 清除 webview 缓存数据
|
||||||
|
uni.clearStorage({
|
||||||
|
success: () => {
|
||||||
|
console.log('Webview 缓存已清除');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import AppRouter from "@/router";
|
import AppRouter from "@/router";
|
||||||
|
import UpdateNotification from "@/components/UpdateNotification";
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<AppRouter />
|
<AppRouter />
|
||||||
|
<UpdateNotification position="top" autoReload={false} showToast={true} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
180
nkebao/src/components/UpdateNotification/index.tsx
Normal file
180
nkebao/src/components/UpdateNotification/index.tsx
Normal file
@@ -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<UpdateNotificationProps> = ({
|
||||||
|
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 (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "fixed",
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
zIndex: 99999,
|
||||||
|
background: "linear-gradient(135deg, #667eea 0%, #764ba2 100%)",
|
||||||
|
color: "white",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
padding: "20px",
|
||||||
|
textAlign: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* 背景装饰 */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
top: "10%",
|
||||||
|
left: "50%",
|
||||||
|
transform: "translateX(-50%)",
|
||||||
|
fontSize: "120px",
|
||||||
|
opacity: 0.1,
|
||||||
|
animation: "float 3s ease-in-out infinite",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<RocketOutlined />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 主要内容 */}
|
||||||
|
<div style={{ position: "relative", zIndex: 1 }}>
|
||||||
|
{/* 图标 */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
fontSize: "80px",
|
||||||
|
marginBottom: "20px",
|
||||||
|
animation: "pulse 2s ease-in-out infinite",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CloudDownloadOutlined />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 标题 */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
fontSize: "28px",
|
||||||
|
fontWeight: "bold",
|
||||||
|
marginBottom: "12px",
|
||||||
|
textShadow: "0 2px 4px rgba(0,0,0,0.3)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
发现新版本
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 描述 */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
fontSize: "16px",
|
||||||
|
opacity: 0.9,
|
||||||
|
marginBottom: "40px",
|
||||||
|
lineHeight: "1.5",
|
||||||
|
maxWidth: "300px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
为了给您提供更好的体验,请更新到最新版本
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 更新按钮 */}
|
||||||
|
<Button
|
||||||
|
size="large"
|
||||||
|
style={{
|
||||||
|
background: "rgba(255,255,255,0.2)",
|
||||||
|
border: "2px solid rgba(255,255,255,0.3)",
|
||||||
|
color: "white",
|
||||||
|
fontSize: "18px",
|
||||||
|
fontWeight: "bold",
|
||||||
|
padding: "12px 40px",
|
||||||
|
borderRadius: "50px",
|
||||||
|
backdropFilter: "blur(10px)",
|
||||||
|
boxShadow: "0 8px 32px rgba(0,0,0,0.3)",
|
||||||
|
transition: "all 0.3s ease",
|
||||||
|
}}
|
||||||
|
onClick={handleReload}
|
||||||
|
>
|
||||||
|
<ReloadOutlined style={{ marginRight: "8px" }} />
|
||||||
|
立即更新
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{/* 提示文字 */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
fontSize: "12px",
|
||||||
|
opacity: 0.7,
|
||||||
|
marginTop: "20px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
更新将自动重启应用
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 动画样式 */}
|
||||||
|
<style>
|
||||||
|
{`
|
||||||
|
@keyframes float {
|
||||||
|
0%, 100% { transform: translateX(-50%) translateY(0px); }
|
||||||
|
50% { transform: translateX(-50%) translateY(-20px); }
|
||||||
|
}
|
||||||
|
@keyframes pulse {
|
||||||
|
0%, 100% { transform: scale(1); }
|
||||||
|
50% { transform: scale(1.05); }
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
</style>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default UpdateNotification;
|
||||||
@@ -4,7 +4,13 @@ import Layout from "@/components/Layout/Layout";
|
|||||||
import NavCommon from "@/components/NavCommon";
|
import NavCommon from "@/components/NavCommon";
|
||||||
import { Input } from "antd";
|
import { Input } from "antd";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import { useSettingsStore } from "@/store/module/settings";
|
||||||
|
import {
|
||||||
|
sendMessageToParent,
|
||||||
|
parseUrlMessage,
|
||||||
|
Message,
|
||||||
|
TYPE_EMUE,
|
||||||
|
} from "@/utils/postApp";
|
||||||
// 声明全局的 uni 对象
|
// 声明全局的 uni 对象
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
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 IframeDebugPage: React.FC = () => {
|
||||||
|
const { setSettings } = useSettingsStore();
|
||||||
const [receivedMessages, setReceivedMessages] = useState<string[]>([]);
|
const [receivedMessages, setReceivedMessages] = useState<string[]>([]);
|
||||||
const [messageId, setMessageId] = useState(0);
|
const [messageId, setMessageId] = useState(0);
|
||||||
const [inputMessage, setInputMessage] = useState("");
|
const [inputMessage, setInputMessage] = useState("");
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
// 解析 URL 参数中的消息
|
// 解析 URL 参数中的消息
|
||||||
const parseUrlMessage = () => {
|
parseUrlMessage().then(message => {
|
||||||
const search = window.location.search.substring(1);
|
if (message) {
|
||||||
let messageParam = null;
|
handleReceivedMessage(message);
|
||||||
|
|
||||||
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));
|
|
||||||
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(() => {
|
useEffect(() => {
|
||||||
parseUrlMessage();
|
parseUrlMessage();
|
||||||
@@ -71,35 +40,19 @@ const IframeDebugPage: React.FC = () => {
|
|||||||
const handleReceivedMessage = (message: Message) => {
|
const handleReceivedMessage = (message: Message) => {
|
||||||
const messageText = `[${new Date().toLocaleTimeString()}] 收到: ${JSON.stringify(message)}`;
|
const messageText = `[${new Date().toLocaleTimeString()}] 收到: ${JSON.stringify(message)}`;
|
||||||
setReceivedMessages(prev => [...prev, messageText]);
|
setReceivedMessages(prev => [...prev, messageText]);
|
||||||
console.log("message.type", message.type);
|
|
||||||
if ([TYPE_EMUE.CONFIG].includes(message.type)) {
|
if ([TYPE_EMUE.CONFIG].includes(message.type)) {
|
||||||
localStorage.setItem("paddingTop", message.data.paddingTop);
|
const { paddingTop, appId, appName, appVersion } = message.data;
|
||||||
localStorage.setItem("isAppMode", "true");
|
setSettings({
|
||||||
|
paddingTop,
|
||||||
|
appId,
|
||||||
|
appName,
|
||||||
|
appVersion,
|
||||||
|
isAppMode: true,
|
||||||
|
});
|
||||||
navigate("/");
|
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
|
// 发送自定义消息到 App
|
||||||
const sendCustomMessage = () => {
|
const sendCustomMessage = () => {
|
||||||
if (!inputMessage.trim()) return;
|
if (!inputMessage.trim()) return;
|
||||||
@@ -107,17 +60,14 @@ const IframeDebugPage: React.FC = () => {
|
|||||||
const newMessageId = messageId + 1;
|
const newMessageId = messageId + 1;
|
||||||
setMessageId(newMessageId);
|
setMessageId(newMessageId);
|
||||||
|
|
||||||
const message: Message = {
|
const message = {
|
||||||
type: TYPE_EMUE.DATA, // 数据交互
|
id: newMessageId,
|
||||||
data: {
|
content: inputMessage,
|
||||||
id: newMessageId,
|
source: "存客宝消息源",
|
||||||
content: inputMessage,
|
timestamp: Date.now(),
|
||||||
source: "存客宝消息源",
|
|
||||||
timestamp: Date.now(),
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
sendMessageToParent(message);
|
sendMessageToParent(message, TYPE_EMUE.DATA);
|
||||||
setInputMessage("");
|
setInputMessage("");
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -126,33 +76,27 @@ const IframeDebugPage: React.FC = () => {
|
|||||||
const newMessageId = messageId + 1;
|
const newMessageId = messageId + 1;
|
||||||
setMessageId(newMessageId);
|
setMessageId(newMessageId);
|
||||||
|
|
||||||
const message: Message = {
|
const message = {
|
||||||
type: TYPE_EMUE.DATA, // 数据交互
|
id: newMessageId,
|
||||||
data: {
|
action: "ping",
|
||||||
id: newMessageId,
|
content: `存客宝测试消息 ${newMessageId}`,
|
||||||
action: "ping",
|
random: Math.random(),
|
||||||
content: `存客宝测试消息 ${newMessageId}`,
|
|
||||||
random: Math.random(),
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
sendMessageToParent(message);
|
sendMessageToParent(message, TYPE_EMUE.DATA);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 发送App功能调用消息
|
// 发送App功能调用消息
|
||||||
const sendAppFunctionCall = () => {
|
const sendAppFunctionCall = () => {
|
||||||
const message: Message = {
|
const message = {
|
||||||
type: 1, // App功能调用
|
action: "showToast",
|
||||||
data: {
|
params: {
|
||||||
action: "showToast",
|
title: "来自H5的功能调用",
|
||||||
params: {
|
icon: "success",
|
||||||
title: "来自H5的功能调用",
|
|
||||||
icon: "success",
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
sendMessageToParent(message);
|
sendMessageToParent(message, TYPE_EMUE.FUNCTION);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 清空消息列表
|
// 清空消息列表
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import { useNavigate, useSearchParams } from "react-router-dom";
|
|
||||||
import { Form, Input, Button, Toast, Checkbox } from "antd-mobile";
|
import { Form, Input, Button, Toast, Checkbox } from "antd-mobile";
|
||||||
import {
|
import {
|
||||||
EyeInvisibleOutline,
|
EyeInvisibleOutline,
|
||||||
@@ -9,8 +9,6 @@ import {
|
|||||||
import { useUserStore } from "@/store/module/user";
|
import { useUserStore } from "@/store/module/user";
|
||||||
import { loginWithPassword, loginWithCode, sendVerificationCode } from "./api";
|
import { loginWithPassword, loginWithCode, sendVerificationCode } from "./api";
|
||||||
import style from "./login.module.scss";
|
import style from "./login.module.scss";
|
||||||
import Layout from "@/components/Layout/Layout";
|
|
||||||
import NavCommon from "@/components/NavCommon";
|
|
||||||
|
|
||||||
const Login: React.FC = () => {
|
const Login: React.FC = () => {
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
@@ -20,8 +18,6 @@ const Login: React.FC = () => {
|
|||||||
const [showPassword, setShowPassword] = useState(false);
|
const [showPassword, setShowPassword] = useState(false);
|
||||||
const [agreeToTerms, setAgreeToTerms] = useState(false);
|
const [agreeToTerms, setAgreeToTerms] = useState(false);
|
||||||
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const [searchParams] = useSearchParams();
|
|
||||||
const { login } = useUserStore();
|
const { login } = useUserStore();
|
||||||
|
|
||||||
// 倒计时效果
|
// 倒计时效果
|
||||||
@@ -32,16 +28,6 @@ const Login: React.FC = () => {
|
|||||||
}
|
}
|
||||||
}, [countdown]);
|
}, [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 handleSendVerificationCode = async () => {
|
||||||
const account = form.getFieldValue("account");
|
const account = form.getFieldValue("account");
|
||||||
@@ -95,24 +81,12 @@ const Login: React.FC = () => {
|
|||||||
} else {
|
} else {
|
||||||
response = await loginWithCode(loginParams);
|
response = await loginWithCode(loginParams);
|
||||||
}
|
}
|
||||||
console.log(response, "response");
|
|
||||||
|
|
||||||
// 获取设备总数
|
// 获取设备总数
|
||||||
const deviceTotal = response.deviceTotal || 0;
|
const deviceTotal = response.deviceTotal || 0;
|
||||||
console.log(deviceTotal, "deviceTotal");
|
|
||||||
|
|
||||||
// 更新状态管理(token会自动存储到localStorage,用户信息存储在状态管理中)
|
// 更新状态管理(token会自动存储到localStorage,用户信息存储在状态管理中)
|
||||||
login(response.token, response.member, deviceTotal);
|
login(response.token, response.member, deviceTotal);
|
||||||
|
|
||||||
Toast.show({ content: "登录成功", position: "top" });
|
|
||||||
|
|
||||||
// 根据设备数量判断跳转
|
|
||||||
if (deviceTotal > 0) {
|
|
||||||
navigate("/");
|
|
||||||
} else {
|
|
||||||
// 没有设备,跳转到引导页面
|
|
||||||
navigate("/guide");
|
|
||||||
}
|
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
// 错误已在request中处理,这里不需要额外处理
|
// 错误已在request中处理,这里不需要额外处理
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import {
|
|||||||
getDashboard,
|
getDashboard,
|
||||||
} from "./api";
|
} from "./api";
|
||||||
import style from "./index.module.scss";
|
import style from "./index.module.scss";
|
||||||
|
import UpdateNotification from "@/components/UpdateNotification";
|
||||||
|
|
||||||
interface DashboardData {
|
interface DashboardData {
|
||||||
deviceNum?: number;
|
deviceNum?: number;
|
||||||
@@ -253,6 +254,7 @@ const Home: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<UpdateNotification position="top" autoReload={false} showToast={true} />
|
||||||
</Layout>
|
</Layout>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -250,7 +250,7 @@ const Devices: React.FC = () => {
|
|||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
footer={
|
footer={
|
||||||
<div style={{ padding: 16, textAlign: "center", background: "#fff" }}>
|
<div className="pagination-container">
|
||||||
<Pagination
|
<Pagination
|
||||||
current={page}
|
current={page}
|
||||||
pageSize={20}
|
pageSize={20}
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ const About: React.FC = () => {
|
|||||||
<div className={style["app-info"]}>
|
<div className={style["app-info"]}>
|
||||||
<div className={style["app-logo"]}>
|
<div className={style["app-logo"]}>
|
||||||
<div className={style["logo-placeholder"]}>
|
<div className={style["logo-placeholder"]}>
|
||||||
<img src="/public/logo.png" alt="logo" />
|
<img src="/logo.png" alt="logo" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={style["app-details"]}>
|
<div className={style["app-details"]}>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { useNavigate } from "react-router-dom";
|
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 {
|
import {
|
||||||
UserOutlined,
|
UserOutlined,
|
||||||
SafetyOutlined,
|
SafetyOutlined,
|
||||||
@@ -8,14 +8,16 @@ import {
|
|||||||
LogoutOutlined,
|
LogoutOutlined,
|
||||||
SettingOutlined,
|
SettingOutlined,
|
||||||
LockOutlined,
|
LockOutlined,
|
||||||
HeartOutlined,
|
ReloadOutlined,
|
||||||
StarOutlined,
|
|
||||||
} from "@ant-design/icons";
|
} from "@ant-design/icons";
|
||||||
import Layout from "@/components/Layout/Layout";
|
import Layout from "@/components/Layout/Layout";
|
||||||
import { useUserStore } from "@/store/module/user";
|
import { useUserStore } from "@/store/module/user";
|
||||||
import { useSettingsStore } from "@/store/module/settings";
|
import { useSettingsStore } from "@/store/module/settings";
|
||||||
import style from "./index.module.scss";
|
import style from "./index.module.scss";
|
||||||
import NavCommon from "@/components/NavCommon";
|
import NavCommon from "@/components/NavCommon";
|
||||||
|
import { sendMessageToParent, TYPE_EMUE } from "@/utils/postApp";
|
||||||
|
import { updateChecker } from "@/utils/updateChecker";
|
||||||
|
|
||||||
interface SettingItem {
|
interface SettingItem {
|
||||||
id: string;
|
id: string;
|
||||||
title: string;
|
title: string;
|
||||||
@@ -32,7 +34,7 @@ interface SettingItem {
|
|||||||
const Setting: React.FC = () => {
|
const Setting: React.FC = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { user, logout } = useUserStore();
|
const { user, logout } = useUserStore();
|
||||||
const { settings, updateSetting } = useSettingsStore();
|
const { settings } = useSettingsStore();
|
||||||
const [showLogoutDialog, setShowLogoutDialog] = useState(false);
|
const [showLogoutDialog, setShowLogoutDialog] = useState(false);
|
||||||
const [avatarError, setAvatarError] = useState(false);
|
const [avatarError, setAvatarError] = useState(false);
|
||||||
|
|
||||||
@@ -57,13 +59,30 @@ const Setting: React.FC = () => {
|
|||||||
Dialog.confirm({
|
Dialog.confirm({
|
||||||
content: "确定要清除缓存吗?这将清除所有本地数据。",
|
content: "确定要清除缓存吗?这将清除所有本地数据。",
|
||||||
onConfirm: () => {
|
onConfirm: () => {
|
||||||
localStorage.clear();
|
sendMessageToParent(
|
||||||
sessionStorage.clear();
|
{
|
||||||
|
action: "clearCache",
|
||||||
|
},
|
||||||
|
TYPE_EMUE.FUNCTION,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 在设置页面添加手动检查更新功能
|
||||||
|
const handleCheckUpdate = () => {
|
||||||
|
updateChecker.checkForUpdate().then(result => {
|
||||||
|
if (result.hasUpdate) {
|
||||||
Toast.show({
|
Toast.show({
|
||||||
content: "缓存已清除",
|
content: "发现新版本,请刷新页面",
|
||||||
position: "top",
|
position: "top",
|
||||||
});
|
});
|
||||||
},
|
} else {
|
||||||
|
Toast.show({
|
||||||
|
content: "当前已是最新版本",
|
||||||
|
position: "top",
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -114,6 +133,15 @@ const Setting: React.FC = () => {
|
|||||||
color: "var(--primary-color)",
|
color: "var(--primary-color)",
|
||||||
badge: "2.3MB",
|
badge: "2.3MB",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: "checkUpdate",
|
||||||
|
title: "检查更新",
|
||||||
|
description: "检查应用是否有新版本",
|
||||||
|
icon: <ReloadOutlined />,
|
||||||
|
type: "button",
|
||||||
|
onClick: handleCheckUpdate,
|
||||||
|
color: "var(--primary-color)",
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -258,12 +286,14 @@ const Setting: React.FC = () => {
|
|||||||
<div className={style["version-info"]}>
|
<div className={style["version-info"]}>
|
||||||
<div className={style["version-card"]}>
|
<div className={style["version-card"]}>
|
||||||
<div className={style["app-logo"]}>
|
<div className={style["app-logo"]}>
|
||||||
<img src="/public/logo.png" alt="" />
|
<img src="/logo.png" alt="" />
|
||||||
</div>
|
</div>
|
||||||
<div className={style["version-details"]}>
|
<div className={style["version-details"]}>
|
||||||
<div className={style["app-name"]}>存客宝</div>
|
<div className={style["app-name"]}>存客宝</div>
|
||||||
<div className={style["version-text"]}>版本 3.0.0</div>
|
<div className={style["version-text"]}>
|
||||||
<div className={style["build-info"]}>Build 2025-7-30</div>
|
版本 {settings.appVersion}
|
||||||
|
</div>
|
||||||
|
<div className={style["build-info"]}>Build 2025-08-04</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={style["copyright"]}>
|
<div className={style["copyright"]}>
|
||||||
|
|||||||
@@ -65,23 +65,6 @@ const ScenarioList: React.FC = () => {
|
|||||||
const [total, setTotal] = useState(0);
|
const [total, setTotal] = useState(0);
|
||||||
const pageSize = 20;
|
const pageSize = 20;
|
||||||
|
|
||||||
// 获取渠道中文名称
|
|
||||||
const getChannelName = (channel: string) => {
|
|
||||||
const channelMap: Record<string, string> = {
|
|
||||||
douyin: "抖音直播获客",
|
|
||||||
kuaishou: "快手直播获客",
|
|
||||||
xiaohongshu: "小红书种草获客",
|
|
||||||
weibo: "微博话题获客",
|
|
||||||
haibao: "海报扫码获客",
|
|
||||||
phone: "电话号码获客",
|
|
||||||
gongzhonghao: "公众号引流获客",
|
|
||||||
weixinqun: "微信群裂变获客",
|
|
||||||
payment: "付款码获客",
|
|
||||||
api: "API接口获客",
|
|
||||||
};
|
|
||||||
return channelMap[channel] || `${channel}获客`;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 获取计划列表数据
|
// 获取计划列表数据
|
||||||
const fetchPlanList = async (page: number, isLoadMore: boolean = false) => {
|
const fetchPlanList = async (page: number, isLoadMore: boolean = false) => {
|
||||||
if (!scenarioId) return;
|
if (!scenarioId) return;
|
||||||
@@ -409,7 +392,7 @@ const ScenarioList: React.FC = () => {
|
|||||||
}
|
}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
footer={
|
footer={
|
||||||
<div className={style["pagination-container"]}>
|
<div className="pagination-container">
|
||||||
<Pagination
|
<Pagination
|
||||||
total={total}
|
total={total}
|
||||||
pageSize={pageSize}
|
pageSize={pageSize}
|
||||||
|
|||||||
@@ -1,17 +1,11 @@
|
|||||||
import { createPersistStore } from "@/store/createPersistStore";
|
import { createPersistStore } from "@/store/createPersistStore";
|
||||||
|
|
||||||
export interface AppSettings {
|
export interface AppSettings {
|
||||||
// 应用设置
|
paddingTop: number;
|
||||||
language: string;
|
appId: string;
|
||||||
timezone: string;
|
appName: string;
|
||||||
|
appVersion: string;
|
||||||
// 隐私设置
|
isAppMode: boolean;
|
||||||
analyticsEnabled: boolean;
|
|
||||||
crashReportEnabled: boolean;
|
|
||||||
|
|
||||||
// 功能设置
|
|
||||||
autoSave: boolean;
|
|
||||||
showTutorial: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SettingsState {
|
interface SettingsState {
|
||||||
@@ -26,12 +20,11 @@ interface SettingsState {
|
|||||||
|
|
||||||
// 默认设置
|
// 默认设置
|
||||||
const defaultSettings: AppSettings = {
|
const defaultSettings: AppSettings = {
|
||||||
language: "zh-CN",
|
paddingTop: 0,
|
||||||
timezone: "Asia/Shanghai",
|
appId: "",
|
||||||
analyticsEnabled: true,
|
appName: "",
|
||||||
crashReportEnabled: true,
|
appVersion: "",
|
||||||
autoSave: true,
|
isAppMode: false,
|
||||||
showTutorial: true,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useSettingsStore = createPersistStore<SettingsState>(
|
export const useSettingsStore = createPersistStore<SettingsState>(
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { createPersistStore } from "@/store/createPersistStore";
|
import { createPersistStore } from "@/store/createPersistStore";
|
||||||
|
import { Toast } from "antd-mobile";
|
||||||
|
|
||||||
export interface User {
|
export interface User {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -60,6 +61,16 @@ export const useUserStore = createPersistStore<UserState>(
|
|||||||
deviceTotal: deviceTotal,
|
deviceTotal: deviceTotal,
|
||||||
};
|
};
|
||||||
set({ user, token, isLoggedIn: true });
|
set({ user, token, isLoggedIn: true });
|
||||||
|
|
||||||
|
Toast.show({ content: "登录成功", position: "top" });
|
||||||
|
|
||||||
|
// 根据设备数量判断跳转
|
||||||
|
if (deviceTotal > 0) {
|
||||||
|
window.location.href = "/";
|
||||||
|
} else {
|
||||||
|
// 没有设备,跳转到引导页面
|
||||||
|
window.location.href = "/guide";
|
||||||
|
}
|
||||||
},
|
},
|
||||||
logout: () => {
|
logout: () => {
|
||||||
// 清除localStorage中的token
|
// 清除localStorage中的token
|
||||||
|
|||||||
@@ -264,3 +264,43 @@ button {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 6px;
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Modal } from "antd-mobile";
|
import { Modal } from "antd-mobile";
|
||||||
|
import { getSetting } from "@/store/module/settings";
|
||||||
/**
|
/**
|
||||||
* 通用js调用弹窗,Promise风格
|
* 通用js调用弹窗,Promise风格
|
||||||
* @param content 弹窗内容
|
* @param content 弹窗内容
|
||||||
@@ -49,7 +49,7 @@ export function getSafeAreaHeight() {
|
|||||||
// 2. 设备检测
|
// 2. 设备检测
|
||||||
const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent);
|
const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent);
|
||||||
const isAndroid = /Android/.test(navigator.userAgent);
|
const isAndroid = /Android/.test(navigator.userAgent);
|
||||||
const isAppMode = Boolean(localStorage.getItem("isAppMode"));
|
const isAppMode = getSetting("isAppMode");
|
||||||
if (isIOS && isAppMode) {
|
if (isIOS && isAppMode) {
|
||||||
// iOS 设备
|
// iOS 设备
|
||||||
const isIPhoneX = window.screen.height >= 812;
|
const isIPhoneX = window.screen.height >= 812;
|
||||||
|
|||||||
72
nkebao/src/utils/postApp.ts
Normal file
72
nkebao/src/utils/postApp.ts
Normal file
@@ -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<any> => {
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
};
|
||||||
217
nkebao/src/utils/updateChecker.ts
Normal file
217
nkebao/src/utils/updateChecker.ts
Normal file
@@ -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<UpdateInfo> {
|
||||||
|
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();
|
||||||
@@ -38,5 +38,13 @@ export default defineConfig({
|
|||||||
minify: "esbuild",
|
minify: "esbuild",
|
||||||
// 启用源码映射(可选,生产环境可以关闭)
|
// 启用源码映射(可选,生产环境可以关闭)
|
||||||
sourcemap: false,
|
sourcemap: false,
|
||||||
|
// 生成manifest文件
|
||||||
|
manifest: true,
|
||||||
|
},
|
||||||
|
define: {
|
||||||
|
// 注入版本信息
|
||||||
|
"import.meta.env.VITE_APP_VERSION": JSON.stringify(
|
||||||
|
process.env.npm_package_version,
|
||||||
|
),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user