first commit
This commit is contained in:
92
utils/ability.js
Normal file
92
utils/ability.js
Normal file
@@ -0,0 +1,92 @@
|
||||
import { abilityCheck } from "@/api/system";
|
||||
import adManager from "@/utils/adManager";
|
||||
|
||||
/**
|
||||
* Checks if a user has the ability to perform an action (e.g., download).
|
||||
* Handles common blocking scenarios like "need_share" or "need_ad".
|
||||
*
|
||||
* @param {string} scene - The scene identifier for the ability check (e.g., "wallpaper_download").
|
||||
* @returns {Promise<boolean>} - Returns true if the action can proceed, false otherwise.
|
||||
*/
|
||||
export const checkAbilityAndHandle = async (scene) => {
|
||||
try {
|
||||
const abilityRes = await abilityCheck(scene);
|
||||
if (abilityRes.canUse) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (
|
||||
abilityRes?.blockType === "need_share" &&
|
||||
abilityRes?.message === "分享可继续"
|
||||
) {
|
||||
uni.showToast({
|
||||
title: "分享给好友即可下载",
|
||||
icon: "none",
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
abilityRes?.blockType === "need_ad"
|
||||
) {
|
||||
uni.showModal({
|
||||
title: "积分不足",
|
||||
content: "观看广告可获得50积分,继续下载",
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
adManager.showVideoAd();
|
||||
}
|
||||
},
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
if (abilityRes?.blockType === "need_vip") {
|
||||
uni.showModal({
|
||||
title: "会员激活",
|
||||
content: "会员激活即可继续使用该功能,是否前往开通?",
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
uni.navigateTo({
|
||||
url: "/pages/mine/vip",
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
uni.showToast({
|
||||
title: "您今日下载次数已用完,明日再试",
|
||||
icon: "none",
|
||||
});
|
||||
return false;
|
||||
} catch (e) {
|
||||
console.error("Ability check failed", e);
|
||||
uni.showToast({ title: "系统繁忙,请稍后重试", icon: "none" });
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the display label for an unlock type.
|
||||
* @param {string} unlockType - The unlock type identifier.
|
||||
* @returns {string} - The display label.
|
||||
*/
|
||||
export const getUnlockLabel = (unlockType) => {
|
||||
if (!unlockType) return "解锁";
|
||||
switch (unlockType) {
|
||||
case "sing3":
|
||||
return "连续签到3天";
|
||||
case "sing5":
|
||||
return "连续签到5天";
|
||||
case "sing7":
|
||||
return "连续签到7天";
|
||||
case "ad":
|
||||
return "广告";
|
||||
case "vip":
|
||||
return "VIP";
|
||||
default:
|
||||
return "解锁";
|
||||
}
|
||||
};
|
||||
199
utils/adManager.js
Normal file
199
utils/adManager.js
Normal file
@@ -0,0 +1,199 @@
|
||||
import { watchAdStart, watchAdReward } from "@/api/system.js";
|
||||
import { useUserStore } from "@/stores/user";
|
||||
|
||||
class AdManager {
|
||||
constructor() {
|
||||
this.videoAd = null;
|
||||
this.rewardToken = "";
|
||||
this.isLoaded = false;
|
||||
this.isAdShowing = false; // 新增:标记广告是否正在展示
|
||||
this.adUnitId = "adunit-d7a28e0357d98947"; // Default ID from RewardAd.vue
|
||||
|
||||
// 彻底废除预初始化。
|
||||
// 在微信小程序中,如果在页面未完全就绪时调用 createRewardedVideoAd,
|
||||
// 会导致底层寻找挂载点失败,抛出 `insertTextView:fail` 等严重渲染错误。
|
||||
// 因此改为:只在用户真正点击“看广告”时才进行初始化。
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the ad instance synchronously when needed.
|
||||
* @param {string} adUnitId
|
||||
*/
|
||||
_initSync(adUnitId) {
|
||||
if (this.videoAd && this.adUnitId === adUnitId) {
|
||||
return true; // Already initialized for this ID
|
||||
}
|
||||
|
||||
this.adUnitId = adUnitId || this.adUnitId;
|
||||
|
||||
// #ifdef MP-WEIXIN || MP-ALIPAY || MP-TOUTIAO || MP-QQ
|
||||
if (uni.createRewardedVideoAd) {
|
||||
try {
|
||||
// Destroy old instance if ID changed
|
||||
if (this.videoAd) {
|
||||
if (this.videoAd.destroy) {
|
||||
try {
|
||||
this.videoAd.destroy();
|
||||
} catch (destroyErr) {
|
||||
console.error("Ad Manager: destroy old ad failed", destroyErr);
|
||||
}
|
||||
}
|
||||
this.videoAd = null;
|
||||
}
|
||||
|
||||
// 核心修复:微信小程序官方建议使用单例,且多次调用 createRewardedVideoAd 传入相同的 adUnitId 会返回同一个实例。
|
||||
// 但如果在短时间内销毁又立即创建,底层可能会发生不可预知的渲染报错(如找不到 parent 节点)。
|
||||
// 因此,我们只需安全地获取实例,并重新绑定事件。
|
||||
this.videoAd = uni.createRewardedVideoAd({
|
||||
adUnitId: this.adUnitId
|
||||
});
|
||||
|
||||
if (this.videoAd) {
|
||||
// 清理之前可能存在的残留事件,防止重复触发和内存泄漏导致底层崩溃
|
||||
if (this.videoAd.offLoad) this.videoAd.offLoad();
|
||||
if (this.videoAd.offError) this.videoAd.offError();
|
||||
if (this.videoAd.offClose) this.videoAd.offClose();
|
||||
|
||||
this.videoAd.onLoad(() => {
|
||||
console.log("Ad Manager: Ad Loaded");
|
||||
this.isLoaded = true;
|
||||
});
|
||||
|
||||
this.videoAd.onError((err) => {
|
||||
console.error("Ad Manager: Ad Load Error", err);
|
||||
this.isLoaded = false;
|
||||
});
|
||||
return true;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Ad Manager Init Error", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// #endif
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the rewarded video ad.
|
||||
* Handles the full flow: start session -> show ad -> verify completion -> claim reward -> refresh assets.
|
||||
* @param {string} [adUnitId] - Optional adUnitId to override the default.
|
||||
* @param {string} [providedToken] - Optional token if watchAdStart was already called externally.
|
||||
* @returns {Promise<boolean>} Resolves with true if reward was claimed, false otherwise.
|
||||
*/
|
||||
async showVideoAd(adUnitId, providedToken) {
|
||||
if (this.isAdShowing || this.isStarting) {
|
||||
console.log("Ad Manager: Ad is already starting or showing");
|
||||
return false;
|
||||
}
|
||||
|
||||
this.isStarting = true;
|
||||
|
||||
// 延迟到这一步才初始化,避开页面渲染期
|
||||
const canInit = this._initSync(adUnitId);
|
||||
|
||||
if (!canInit || !this.videoAd) {
|
||||
uni.showToast({ title: "当前环境不支持广告", icon: "none" });
|
||||
this.isStarting = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
// Step 1: Start Ad Session (if not provided)
|
||||
if (providedToken) {
|
||||
this.rewardToken = providedToken;
|
||||
} else {
|
||||
const startRes = await watchAdStart();
|
||||
if (!startRes || !startRes.rewardToken) {
|
||||
uni.showToast({ title: "广告启动失败,请稍后再试", icon: "none" });
|
||||
this.isStarting = false;
|
||||
return false;
|
||||
}
|
||||
this.rewardToken = startRes.rewardToken;
|
||||
}
|
||||
|
||||
// Step 2: Show Ad
|
||||
try {
|
||||
this.isAdShowing = true;
|
||||
this.isStarting = false;
|
||||
await this.videoAd.show();
|
||||
} catch (e) {
|
||||
// Retry load and show
|
||||
try {
|
||||
await this.videoAd.load();
|
||||
await this.videoAd.show();
|
||||
} catch (loadErr) {
|
||||
this.isAdShowing = false;
|
||||
console.error("Ad Manager: Retry load failed", loadErr);
|
||||
throw loadErr;
|
||||
}
|
||||
}
|
||||
|
||||
// Step 3: Wait for Close
|
||||
return new Promise((resolve) => {
|
||||
const onCloseHandler = async (res) => {
|
||||
// 延迟 500ms 释放状态,防止从广告返回页面时(onShow)立即触发 DOM 大量更新,导致底层 insertTextView:fail 崩溃
|
||||
setTimeout(() => {
|
||||
this.isAdShowing = false;
|
||||
}, 500);
|
||||
|
||||
// 安全地解绑事件
|
||||
try {
|
||||
if (this.videoAd && this.videoAd.offClose) {
|
||||
this.videoAd.offClose(onCloseHandler); // Clean up listener
|
||||
}
|
||||
} catch (offErr) {
|
||||
console.error("Ad Manager: offClose error", offErr);
|
||||
}
|
||||
|
||||
if (res && res.isEnded) {
|
||||
// Step 4: Claim Reward
|
||||
// Only claim if token wasn't provided externally (if provided, external component claims it)
|
||||
if (!providedToken) {
|
||||
await this._claimReward(this.rewardToken);
|
||||
}
|
||||
resolve(true);
|
||||
} else {
|
||||
uni.showToast({ title: "观看完整广告才能获取奖励哦", icon: "none" });
|
||||
resolve(false);
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
this.videoAd.onClose(onCloseHandler);
|
||||
} catch (onErr) {
|
||||
console.error("Ad Manager: onClose error", onErr);
|
||||
resolve(false);
|
||||
}
|
||||
});
|
||||
|
||||
} catch (e) {
|
||||
console.error("Ad Manager: Show failed", e);
|
||||
uni.showToast({ title: "广告加载失败,请稍后再试", icon: "none" });
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async _claimReward(token) {
|
||||
try {
|
||||
uni.showLoading({ title: "发放奖励中..." });
|
||||
const res = await watchAdReward(token);
|
||||
if (res) {
|
||||
uni.showToast({
|
||||
title: "获得50积分",
|
||||
icon: "success",
|
||||
});
|
||||
// Refresh user assets
|
||||
const userStore = useUserStore();
|
||||
await userStore.fetchUserAssets();
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Ad Manager: Reward claim failed", e);
|
||||
uni.showToast({ title: "奖励发放失败", icon: "none" });
|
||||
} finally {
|
||||
uni.hideLoading();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new AdManager();
|
||||
127
utils/common.js
Normal file
127
utils/common.js
Normal file
@@ -0,0 +1,127 @@
|
||||
import { getDeviceInfo } from "@/utils/system";
|
||||
import {
|
||||
saveRecord,
|
||||
viewRecord,
|
||||
createShareToken,
|
||||
createTracking,
|
||||
} from "@/api/system";
|
||||
|
||||
export const generateObjectId = (
|
||||
m = Math,
|
||||
d = Date,
|
||||
h = 16,
|
||||
s = (s) => m.floor(s).toString(h),
|
||||
) => s(d.now() / 1000) + " ".repeat(h).replace(/./g, () => s(m.random() * h));
|
||||
|
||||
export const uploadImage = (filePath) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.uploadFile({
|
||||
url: "https://api.ai-meng.com/api/common/upload",
|
||||
filePath: filePath,
|
||||
name: "file",
|
||||
header: {
|
||||
"x-app-id": "69665538a49b8ae3be50fe5d",
|
||||
},
|
||||
success: (res) => {
|
||||
if (res.statusCode < 400) {
|
||||
try {
|
||||
const keyJson = JSON.parse(res.data);
|
||||
if (keyJson?.data?.result === "auditFailed") {
|
||||
reject("图片不符合发布规范,请稍作修改后再试");
|
||||
} else {
|
||||
const url = `https://file.lihailezzc.com/${keyJson?.data.key}`;
|
||||
resolve(url);
|
||||
}
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
} else {
|
||||
reject(new Error("Upload failed"));
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
reject(err);
|
||||
},
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export const saveRecordRequest = async (path, targetId, scene, imageUrl) => {
|
||||
if (!imageUrl) {
|
||||
imageUrl = await uploadImage(path);
|
||||
}
|
||||
const deviceInfo = getDeviceInfo();
|
||||
saveRecord({
|
||||
scene,
|
||||
targetId,
|
||||
imageUrl,
|
||||
deviceInfo,
|
||||
});
|
||||
};
|
||||
|
||||
export const saveViewRequest = async (shareToken, scene, targetId = "") => {
|
||||
const deviceInfo = getDeviceInfo();
|
||||
viewRecord({
|
||||
shareToken,
|
||||
scene,
|
||||
targetId,
|
||||
deviceInfo,
|
||||
});
|
||||
};
|
||||
|
||||
export const saveRemoteImageToLocal = (imageUrl) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.downloadFile({
|
||||
url: imageUrl,
|
||||
success: (res) => {
|
||||
if (res.statusCode === 200) {
|
||||
uni.saveImageToPhotosAlbum({
|
||||
filePath: res.tempFilePath,
|
||||
success: () => {
|
||||
resolve(true);
|
||||
},
|
||||
fail: (err) => {
|
||||
reject(err);
|
||||
},
|
||||
});
|
||||
} else {
|
||||
reject(
|
||||
new Error("Download failed with status code: " + res.statusCode),
|
||||
);
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
reject(err);
|
||||
},
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export const getShareToken = async (scene, targetId = "") => {
|
||||
const deviceInfo = getDeviceInfo();
|
||||
const shareTokenRes = await createShareToken({
|
||||
scene,
|
||||
targetId,
|
||||
...deviceInfo,
|
||||
});
|
||||
return shareTokenRes?.shareToken || "";
|
||||
};
|
||||
|
||||
export const trackRecord = (event) => {
|
||||
const pages = getCurrentPages();
|
||||
// 核心修复:在小程序刚启动或页面尚未压栈时,pages 可能为空数组
|
||||
const currentPage = pages && pages.length > 0 ? pages[pages.length - 1] : null;
|
||||
const route = currentPage ? currentPage.route : "app_launch";
|
||||
|
||||
const deviceInfo = getDeviceInfo();
|
||||
createTracking({
|
||||
...event,
|
||||
page: route,
|
||||
deviceInfo,
|
||||
});
|
||||
};
|
||||
|
||||
export const getThumbUrl = (url) => {
|
||||
if (!url) return "";
|
||||
return `${url}?imageView2/1/w/200/h/200/q/80`;
|
||||
};
|
||||
18
utils/date.js
Normal file
18
utils/date.js
Normal file
@@ -0,0 +1,18 @@
|
||||
export function formatDate(isoString, format = 'YYYY-MM-DD HH:mm:ss') {
|
||||
if (!isoString) return ''
|
||||
const date = new Date(isoString)
|
||||
if (isNaN(date.getTime())) return ''
|
||||
|
||||
const pad = (n) => n.toString().padStart(2, '0')
|
||||
|
||||
const map = {
|
||||
YYYY: date.getFullYear().toString(),
|
||||
MM: pad(date.getMonth() + 1),
|
||||
DD: pad(date.getDate()),
|
||||
HH: pad(date.getHours()),
|
||||
mm: pad(date.getMinutes()),
|
||||
ss: pad(date.getSeconds())
|
||||
}
|
||||
|
||||
return format.replace(/YYYY|MM|DD|HH|mm|ss/g, token => map[token])
|
||||
}
|
||||
53
utils/login.js
Normal file
53
utils/login.js
Normal file
@@ -0,0 +1,53 @@
|
||||
export const wxLogin = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
wx.login({
|
||||
success: (res) => {
|
||||
if (res.code) {
|
||||
resolve(res.code);
|
||||
} else {
|
||||
reject("登录失败:code为空");
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
reject(err);
|
||||
},
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export const alipayLogin = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
// #ifdef MP-ALIPAY
|
||||
my.getAuthCode({
|
||||
scopes: "auth_user",
|
||||
success: (res) => {
|
||||
if (res.authCode) {
|
||||
resolve(res.authCode);
|
||||
} else {
|
||||
reject("登录失败:authCode为空");
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
reject(err);
|
||||
},
|
||||
});
|
||||
// #endif
|
||||
// #ifndef MP-ALIPAY
|
||||
reject("当前非支付宝环境");
|
||||
// #endif
|
||||
});
|
||||
};
|
||||
|
||||
export const wxGetUserProfile = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
wx.getUserProfile({
|
||||
desc: "用于完善用户信息",
|
||||
success: (res) => {
|
||||
resolve(res);
|
||||
},
|
||||
fail: (err) => {
|
||||
reject(err);
|
||||
},
|
||||
});
|
||||
});
|
||||
};
|
||||
143
utils/request.js
Normal file
143
utils/request.js
Normal file
@@ -0,0 +1,143 @@
|
||||
const BASE_URL = "https://api.ai-meng.com";
|
||||
// const BASE_URL = 'http://127.0.0.1:3999'
|
||||
// const BASE_URL = "http://192.168.1.2:3999";
|
||||
// const BASE_URL = "http://192.168.31.253:3999";
|
||||
import { useUserStore } from "@/stores/user";
|
||||
import { getPlatform } from "./system.js";
|
||||
const platform = getPlatform();
|
||||
|
||||
// 环境区分
|
||||
// const BASE_URL =
|
||||
// process.env.NODE_ENV === 'production'
|
||||
// ? 'https://apis.lihailezzc.com'
|
||||
// : 'http://127.0.0.1:3999'
|
||||
// 192.168.2.186
|
||||
// 192.168.31.253
|
||||
// const BASE_URL = 'https://apis.lihailezzc.com'
|
||||
|
||||
let isLoadingCount = 0; // 计数多次请求loading
|
||||
|
||||
function showLoadingFlag(loadingText) {
|
||||
if (isLoadingCount === 0) {
|
||||
uni.showLoading({ title: loadingText });
|
||||
}
|
||||
isLoadingCount++;
|
||||
}
|
||||
|
||||
function hideLoading() {
|
||||
isLoadingCount--;
|
||||
if (isLoadingCount <= 0) {
|
||||
isLoadingCount = 0;
|
||||
uni.hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
function getHeaders() {
|
||||
const userStore = useUserStore();
|
||||
const headers = {
|
||||
"x-app-id": "69665538a49b8ae3be50fe5d",
|
||||
"x-platform": platform,
|
||||
"x-user-id": userStore?.userInfo?.id || "",
|
||||
};
|
||||
if (userStore.token) {
|
||||
headers["Authorization"] = userStore.token;
|
||||
}
|
||||
return headers;
|
||||
}
|
||||
|
||||
function handleError(err, showError = true) {
|
||||
if (!showError) return;
|
||||
let msg = "网络异常,请稍后重试";
|
||||
if (typeof err === "string") {
|
||||
msg = err;
|
||||
} else if (err?.msg) {
|
||||
msg = err.msg;
|
||||
} else if (err?.message) {
|
||||
msg = err.message;
|
||||
}
|
||||
uni.showModal({
|
||||
title: "错误提示",
|
||||
content: msg,
|
||||
showCancel: false,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 请求函数,支持自动加载状态、token、错误弹窗、重试等
|
||||
* @param {Object} config 请求配置
|
||||
* @param {string} config.url 请求路径(必填)
|
||||
* @param {string} [config.method='GET'] 请求方法
|
||||
* @param {Object} [config.data={}] 请求参数
|
||||
* @param {Object} [config.header={}] 额外 headers
|
||||
* @param {boolean} [config.showLoading=true] 是否自动显示加载提示
|
||||
* @param {boolean} [config.showError=true] 是否自动弹错误提示
|
||||
* @param {number} [config.timeout=10000] 请求超时,单位毫秒
|
||||
* @param {number} [config.retry=1] 失败后自动重试次数
|
||||
* @returns {Promise<any>} 返回接口 data
|
||||
*/
|
||||
export function request(config) {
|
||||
if (!config || !config.url) {
|
||||
return Promise.reject(new Error("请求缺少 url 参数"));
|
||||
}
|
||||
|
||||
const {
|
||||
url,
|
||||
method = "GET",
|
||||
data = {},
|
||||
header = {},
|
||||
showLoading = false,
|
||||
showError = true,
|
||||
retry = 1,
|
||||
loadingText = "加载中",
|
||||
} = config;
|
||||
|
||||
const finalHeader = { ...getHeaders(), ...header };
|
||||
const upperMethod = method.toUpperCase();
|
||||
|
||||
let attempt = 0;
|
||||
|
||||
const doRequest = () =>
|
||||
new Promise((resolve, reject) => {
|
||||
if (showLoading) showLoadingFlag(loadingText);
|
||||
|
||||
uni.request({
|
||||
url: BASE_URL + url,
|
||||
method: upperMethod,
|
||||
data,
|
||||
header: finalHeader,
|
||||
success: (res) => {
|
||||
if (showLoading) hideLoading();
|
||||
const { statusCode, data: body } = res;
|
||||
if (
|
||||
(statusCode === 200 || statusCode === 201) &&
|
||||
body?.code === 200
|
||||
) {
|
||||
resolve(body.data);
|
||||
} else {
|
||||
reject({
|
||||
type: "business",
|
||||
msg: body?.msg || "服务器错误",
|
||||
data: body,
|
||||
});
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
if (showLoading) hideLoading();
|
||||
reject({ type: "network", msg: "网络请求失败", error: err });
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
// 自动重试
|
||||
const tryRequest = () =>
|
||||
doRequest().catch((err) => {
|
||||
if (attempt < retry) {
|
||||
attempt++;
|
||||
return tryRequest();
|
||||
}
|
||||
if (showError) handleError(err.msg || err);
|
||||
return Promise.reject(err);
|
||||
});
|
||||
|
||||
return tryRequest();
|
||||
}
|
||||
122
utils/system.js
Normal file
122
utils/system.js
Normal file
@@ -0,0 +1,122 @@
|
||||
function getSafeSystemInfo() {
|
||||
try {
|
||||
// #ifdef MP-WEIXIN
|
||||
// 优先使用原生 wx 新 API,避开 uni 包装层内部对 getSystemInfoSync 的兼容调用
|
||||
if (
|
||||
typeof wx !== "undefined" &&
|
||||
wx.getWindowInfo &&
|
||||
wx.getAppBaseInfo &&
|
||||
wx.getDeviceInfo
|
||||
) {
|
||||
return {
|
||||
...wx.getWindowInfo(),
|
||||
...wx.getAppBaseInfo(),
|
||||
...wx.getDeviceInfo(),
|
||||
};
|
||||
}
|
||||
// #endif
|
||||
|
||||
if (uni.getWindowInfo && uni.getAppBaseInfo && uni.getDeviceInfo) {
|
||||
return {
|
||||
...uni.getWindowInfo(),
|
||||
...uni.getAppBaseInfo(),
|
||||
...uni.getDeviceInfo(),
|
||||
};
|
||||
}
|
||||
} catch (e) {
|
||||
// fall through to legacy fallback
|
||||
}
|
||||
|
||||
return uni.getSystemInfoSync();
|
||||
}
|
||||
|
||||
let SYSTEM = getSafeSystemInfo();
|
||||
|
||||
export const getStatusBarHeight = () => SYSTEM.statusBarHeight || 15;
|
||||
|
||||
export const getTitleBarHeight = () => {
|
||||
// #ifdef MP-ALIPAY
|
||||
return 44;
|
||||
// #endif
|
||||
|
||||
if (uni.getMenuButtonBoundingClientRect) {
|
||||
try {
|
||||
const rect = uni.getMenuButtonBoundingClientRect();
|
||||
if (rect && rect.top && rect.height) {
|
||||
return rect.height + (rect.top - getStatusBarHeight()) * 2;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
return 44;
|
||||
};
|
||||
|
||||
export const getBavBarHeight = () => getStatusBarHeight() + getTitleBarHeight();
|
||||
|
||||
export const getLeftIconLeft = () => {
|
||||
// if(tt?.getMenuButtonBoundingClientRect) {
|
||||
// const { leftIcon: {left, width}} = tt.getMenuButtonBoundingClientRect
|
||||
// return left + parseInt(width) + 5
|
||||
// } else {
|
||||
// return 0
|
||||
// }
|
||||
// #ifdef MP-TOUTIAO
|
||||
const {
|
||||
leftIcon: { left, width },
|
||||
} = tt.getCustomButtonBoundingClientRect();
|
||||
return left + parseInt(width);
|
||||
// #endif
|
||||
// #ifndef MP-TOUTIAO
|
||||
return 0;
|
||||
// #endif
|
||||
};
|
||||
|
||||
export function getPlatform() {
|
||||
return getPlatformProvider().replace("mp-", "");
|
||||
}
|
||||
|
||||
export function getPlatformProvider() {
|
||||
const platform = process.env.UNI_PLATFORM;
|
||||
return platform || "mp-weixin";
|
||||
// const platform = uni.getSystemInfoSync().platform;
|
||||
// console.log(1111111, platform)
|
||||
// // H5 模拟器调试时使用 __wxConfig 环境变量判断
|
||||
// if (typeof __wxConfig !== 'undefined') return 'weixin';
|
||||
// if (platform === 'devtools') {
|
||||
// // 可以根据小程序环境变量判断
|
||||
// // 抖音开发工具会定义 tt 对象
|
||||
// return typeof tt !== 'undefined' ? 'toutiao' : 'weixin';
|
||||
// }
|
||||
|
||||
// return typeof tt !== 'undefined' ? 'toutiao' : 'weixin';
|
||||
}
|
||||
|
||||
export function getDeviceInfo() {
|
||||
const info = getSafeSystemInfo();
|
||||
|
||||
return {
|
||||
platform: info.platform,
|
||||
brand: info.brand,
|
||||
model: info.model,
|
||||
system: info.system,
|
||||
screenWidth: info.screenWidth,
|
||||
screenHeight: info.screenHeight,
|
||||
pixelRatio: info.pixelRatio,
|
||||
language: info.language,
|
||||
version: info.version,
|
||||
SDKVersion: info.SDKVersion,
|
||||
appId: "69665538a49b8ae3be50fe5d",
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否处于朋友圈单页模式
|
||||
*/
|
||||
export function isSinglePageMode() {
|
||||
// #ifdef MP-WEIXIN
|
||||
const launchOptions = uni.getLaunchOptionsSync();
|
||||
return launchOptions.scene === 1154;
|
||||
// #endif
|
||||
return false;
|
||||
}
|
||||
121
utils/track.js
Normal file
121
utils/track.js
Normal file
@@ -0,0 +1,121 @@
|
||||
import { request } from "@/utils/request";
|
||||
|
||||
// 生成并缓存 device_id
|
||||
function getOrCreateDeviceId() {
|
||||
let deviceId = uni.getStorageSync("device_id");
|
||||
if (!deviceId) {
|
||||
deviceId = Date.now() + "_" + Math.random().toString(36).substr(2, 9);
|
||||
uni.setStorageSync("device_id", deviceId);
|
||||
}
|
||||
return deviceId;
|
||||
}
|
||||
|
||||
// 获取网络类型
|
||||
function getNetworkType() {
|
||||
return new Promise((resolve) => {
|
||||
uni.getNetworkType({
|
||||
success: (res) => resolve(res.networkType || "unknown"),
|
||||
fail: () => resolve("unknown"),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 获取系统平台
|
||||
function getSystemInfo() {
|
||||
return new Promise((resolve) => {
|
||||
uni.getSystemInfo({
|
||||
success: (res) => resolve(res),
|
||||
fail: () => resolve({}),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 获取当前页面路径
|
||||
function getCurrentPagePath() {
|
||||
const pages = getCurrentPages();
|
||||
const currentPage = pages[pages.length - 1];
|
||||
return currentPage?.route || "";
|
||||
}
|
||||
|
||||
function getMiniProgramVersion() {
|
||||
const platform = process.env.UNI_PLATFORM;
|
||||
let version = "";
|
||||
let envVersion = "";
|
||||
|
||||
try {
|
||||
if (platform === "mp-weixin") {
|
||||
// 微信小程序
|
||||
const info = wx.getAccountInfoSync();
|
||||
version = info?.miniProgram?.version || "";
|
||||
envVersion = info?.miniProgram?.envVersion || "";
|
||||
} else if (platform === "mp-alipay") {
|
||||
// 支付宝小程序
|
||||
const info = my.getAppInfoSync();
|
||||
version = info?.version || "";
|
||||
} else if (platform === "mp-toutiao" || platform === "mp-jd") {
|
||||
// 抖音/头条/京东小程序
|
||||
const info = tt.getAccountInfoSync?.();
|
||||
version = info?.miniProgram?.version || "";
|
||||
envVersion = info?.miniProgram?.envVersion || "";
|
||||
} else if (platform === "mp-baidu") {
|
||||
// 百度小程序(无标准方法获取版本号)
|
||||
version = ""; // 百度不支持获取版本号
|
||||
} else {
|
||||
version = "";
|
||||
}
|
||||
} catch (e) {
|
||||
version = "";
|
||||
envVersion = "";
|
||||
}
|
||||
|
||||
return {
|
||||
platform, // 当前小程序平台
|
||||
version, // 小程序版本号
|
||||
envVersion, // develop / trial / release(微信等支持)
|
||||
};
|
||||
}
|
||||
|
||||
// 构造埋点对象
|
||||
async function buildEventData(
|
||||
eventName,
|
||||
eventType = "click",
|
||||
customParams = {}
|
||||
) {
|
||||
const deviceId = getOrCreateDeviceId();
|
||||
const systemInfo = await getSystemInfo();
|
||||
const networkType = await getNetworkType();
|
||||
// const location = await getLocation()
|
||||
const appVersion = typeof plus !== "undefined" ? plus?.runtime?.version : "";
|
||||
const { envVersion, platform, version } = getMiniProgramVersion();
|
||||
|
||||
return {
|
||||
userId: uni.getStorageSync("user_id") || null,
|
||||
deviceId: deviceId,
|
||||
eventName: eventName,
|
||||
eventType: eventType,
|
||||
eventTime: new Date(),
|
||||
page: getCurrentPagePath(),
|
||||
elementId: customParams.element_id || null,
|
||||
elementContent: customParams.element_content || null,
|
||||
customParams: customParams,
|
||||
networkType: networkType,
|
||||
os: systemInfo.platform || "unknown",
|
||||
appVersion: version || "unknown",
|
||||
envVersion: envVersion || "",
|
||||
platform: platform || "",
|
||||
};
|
||||
}
|
||||
|
||||
// 发送埋点数据到服务器
|
||||
async function sendTrack(eventName, eventType = "click", customParams = {}) {
|
||||
const eventData = await buildEventData(eventName, eventType, customParams);
|
||||
request({
|
||||
url: "/api/common/tracking/create",
|
||||
method: "POST",
|
||||
data: eventData,
|
||||
});
|
||||
}
|
||||
|
||||
export default {
|
||||
sendTrack,
|
||||
};
|
||||
Reference in New Issue
Block a user