ai-clone/frontend-ai/App.vue
2026-03-05 14:29:21 +08:00

372 lines
11 KiB
Vue
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script>
import { fetchServicePrices, showPaymentModal, SERVICE_TYPES } from '@/utils/payment.js';
import { API_BASE, API_ENDPOINTS } from '@/config/api.js';
import { cleanExpiredCache, cleanOldestCache } from '@/utils/videoCacheManager.js';
import PermissionManager from '@/utils/permission.js';
export default {
onLaunch(options) {
console.log('App Launch');
// #ifdef MP-WEIXIN
this.setupMpWeixinAuthGuard();
// #endif
// 清理过期的视频缓存
this.cleanVideoCache();
// 预加载应用配置
this.loadAppConfig();
// 预加载服务价格
this.loadServicePrices();
// 全局拦截:遇到 402需要付费/额度不足)时弹出支付弹窗
this.setupPaymentInterceptor();
// 获取小程序用户标识(用于支付)
this.getUserIdentity();
// App环境预请求麦克风权限
this.requestPermissions();
},
onShow() {
console.log('App Show');
},
onHide() {
console.log('App Hide');
},
methods: {
// #ifdef MP-WEIXIN
setupMpWeixinAuthGuard() {
const whiteList = [
'/pages/login/login',
'/pages/settings/privacy-policy',
'/pages/settings/user-agreement',
'/pages/index/index'
];
const protectedList = [
'/pages/history/history',
'/pages/revival/revival-history',
'/pages/revival/revival',
'/pages/phone-call/phone-call',
'/pages/video-call/video-call',
'/pages/video-call-new/video-call-new',
'/pages/video-gen/video-gen',
'/pages/upload-audio/upload-audio',
'/pages/my-works/my-works',
'/pages/profile/edit-profile',
'/pages/settings/settings'
];
const isLoggedIn = () => {
const token = uni.getStorageSync('token');
const userId = uni.getStorageSync('userId');
return !!(token && userId);
};
const normalizeUrlPath = (url) => {
if (!url) return '';
const qIndex = url.indexOf('?');
const path = qIndex >= 0 ? url.slice(0, qIndex) : url;
return path.startsWith('/') ? path : `/${path}`;
};
const buildLoginUrl = (originalUrl) => {
const redirect = originalUrl || '';
return `/pages/login/login?redirect=${encodeURIComponent(redirect)}`;
};
const shouldBlock = (url) => {
const path = normalizeUrlPath(url);
if (!path) return false;
if (whiteList.includes(path)) return false;
if (!protectedList.includes(path)) return false;
return !isLoggedIn();
};
const wrap = (methodName) => {
const original = uni[methodName];
if (typeof original !== 'function') return;
uni[methodName] = (options = {}) => {
try {
const url = typeof options === 'string' ? options : options.url;
if (shouldBlock(url)) {
const loginUrl = buildLoginUrl(url);
return uni.reLaunch({ url: loginUrl });
}
} catch (e) {
// ignore
}
return original.call(uni, options);
};
};
wrap('navigateTo');
wrap('redirectTo');
wrap('reLaunch');
wrap('switchTab');
},
// #endif
// 清理视频缓存
cleanVideoCache() {
try {
const expiredCount = cleanExpiredCache();
const oldCount = cleanOldestCache();
if (expiredCount > 0 || oldCount > 0) {
console.log(`[App] 视频缓存清理完成:过期${expiredCount}个,旧缓存${oldCount}`);
}
} catch (error) {
console.error('[App] 清理视频缓存失败:', error);
}
},
// 加载应用配置
async loadAppConfig() {
try {
// 临时添加:强制清除缓存,测试完成后可删除此行
uni.removeStorageSync('appConfig');
console.log('[App] 缓存已清除,重新加载配置');
const res = await uni.request({
url: `${API_BASE}${API_ENDPOINTS.config.getAppConfig}`,
method: 'GET',
header: {
'Content-Type': 'application/json'
// 不添加认证头,因为这是公开接口
}
});
// 兼容不同平台的返回格式:可能是 [error, res] 或直接是 res
const response = Array.isArray(res) ? res[1] : res;
if (response && response.data && response.data.success && response.data.data) {
const config = response.data.data;
// 保存到本地存储
uni.setStorageSync('appConfig', config);
console.log('[App] 应用配置已加载:', config);
}
} catch (error) {
console.error('[App] 加载应用配置失败:', error);
}
},
async loadServicePrices() {
try {
await fetchServicePrices();
console.log('[App] 服务价格已加载');
} catch (error) {
console.error('[App] 加载服务价格失败:', error);
}
},
// 获取小程序用户标识(用于支付)
async getUserIdentity() {
// #ifdef MP-WEIXIN
// 微信小程序获取openid
const existingOpenid = uni.getStorageSync('wx_openid');
if (existingOpenid) {
console.log('[App] 已有微信openid:', existingOpenid);
return;
}
try {
const loginRes = await uni.login();
if (loginRes[1] && loginRes[1].code) {
const code = loginRes[1].code;
console.log('[App] 微信登录成功code:', code);
// 发送code到后端换取openid
// TODO: 需要后端实现 /api/wechat/login 接口
// const res = await uni.request({
// url: `${API_BASE}/api/wechat/login`,
// method: 'POST',
// data: { code }
// });
// if (res[1] && res[1].data && res[1].data.openid) {
// uni.setStorageSync('wx_openid', res[1].data.openid);
// console.log('[App] 微信openid已保存');
// }
// 临时方案使用模拟openid仅用于测试
console.warn('[App] 使用模拟openid生产环境需要实现后端接口');
uni.setStorageSync('wx_openid', 'test_openid_' + Date.now());
}
} catch (error) {
console.error('[App] 获取微信openid失败:', error);
}
// #endif
// #ifdef MP-ALIPAY
// 支付宝小程序获取userId
const existingUserId = uni.getStorageSync('alipay_user_id');
if (existingUserId) {
console.log('[App] 已有支付宝userId:', existingUserId);
return;
}
try {
my.getAuthCode({
scopes: 'auth_user',
success: async (res) => {
const authCode = res.authCode;
console.log('[App] 支付宝授权成功authCode:', authCode);
// 发送authCode到后端换取userId
// TODO: 需要后端实现 /api/alipay/login 接口
// const loginRes = await uni.request({
// url: `${API_BASE}/api/alipay/login`,
// method: 'POST',
// data: { authCode }
// });
// if (loginRes[1] && loginRes[1].data && loginRes[1].data.userId) {
// uni.setStorageSync('alipay_user_id', loginRes[1].data.userId);
// console.log('[App] 支付宝userId已保存');
// }
// 临时方案使用模拟userId仅用于测试
console.warn('[App] 使用模拟userId生产环境需要实现后端接口');
uni.setStorageSync('alipay_user_id', '2088' + Date.now());
},
fail: (error) => {
console.error('[App] 获取支付宝userId失败:', error);
}
});
} catch (error) {
console.error('[App] 支付宝授权失败:', error);
}
// #endif
},
setupPaymentInterceptor() {
const getServiceTypeFromUrl = (url) => {
const u = String(url || '');
if (u.includes('/api/photo-revival/')) return SERVICE_TYPES.PHOTO_REVIVAL.type;
if (u.includes('/api/voice/')) return SERVICE_TYPES.VOICE_CLONE.type;
if (u.includes('/api/tts/')) return SERVICE_TYPES.TTS_SYNTHESIS.type;
if (u.includes('/api/video-call')) return SERVICE_TYPES.VIDEO_CALL.type;
if (u.includes('/api/conversation/')) return SERVICE_TYPES.CONVERSATION.type;
return '';
};
const ensureLogin = () => {
const token = uni.getStorageSync('token') || '';
const userId = uni.getStorageSync('userId') || '';
return !!(token && userId);
};
const showPay = (serviceType) => {
try {
if (!ensureLogin()) {
uni.showModal({
title: '提示',
content: '请先登录后再继续',
confirmText: '去登录',
cancelText: '取消',
success: (m) => {
if (m.confirm) {
uni.reLaunch({ url: '/pages/login/login' });
}
}
});
return;
}
const st = serviceType || SERVICE_TYPES.PHOTO_REVIVAL.type;
// 重要:支付弹窗需要页面承载(页面里渲染了 <PaymentModal/>)。
// 全局拦截在 App 实例上直接 show 会导致小程序端“无UI”。
let hostVm = this;
try {
const pages = (typeof getCurrentPages === 'function') ? getCurrentPages() : [];
const top = pages && pages.length ? pages[pages.length - 1] : null;
if (top && top.$vm) {
hostVm = top.$vm;
}
} catch (e) {
// ignore
}
showPaymentModal(hostVm, st);
} catch (e) {
console.error('[Payment] 402拦截弹窗失败:', e);
}
};
const handle402 = (options, res) => {
try {
const url = (options && options.url) ? options.url : '';
let parsed = null;
try {
parsed = res && typeof res.data === 'string' ? JSON.parse(res.data) : (res ? res.data : null);
} catch (e) {
parsed = null;
}
const backendServiceType = parsed && (parsed.serviceType || parsed.service_type);
const mapped = backendServiceType === 'CREATE_VOICE' ? SERVICE_TYPES.VOICE_CLONE.type
: backendServiceType === 'PHOTO_REVIVAL' ? SERVICE_TYPES.PHOTO_REVIVAL.type
: backendServiceType === 'VOLCENGINE_VIDEO' ? (SERVICE_TYPES.VOLCENGINE_VIDEO ? SERVICE_TYPES.VOLCENGINE_VIDEO.type : '')
: backendServiceType === 'SYNTHESIZE' ? SERVICE_TYPES.TTS_SYNTHESIS.type
: backendServiceType === 'VIDEO_CALL' ? SERVICE_TYPES.VIDEO_CALL.type
: backendServiceType === 'AI_CALL' ? SERVICE_TYPES.CONVERSATION.type
: '';
const serviceType = mapped || getServiceTypeFromUrl(url);
showPay(serviceType);
} catch (e) {
console.error('[Payment] 402拦截处理失败:', e);
}
};
const intercept = (apiName) => {
let lastOptions = null;
uni.addInterceptor(apiName, {
invoke(options) {
lastOptions = options;
return options;
},
returnValue(res) {
try {
// Promise/async 形式:在 then 里检查 402
if (res && typeof res.then === 'function') {
return res.then((out) => {
try {
const r = Array.isArray(out) ? out[1] : out;
if (r && r.statusCode === 402) {
handle402(lastOptions, r);
}
} catch (e) {
// ignore
}
return out;
});
}
// callback 形式
const r = Array.isArray(res) ? res[1] : res;
if (r && r.statusCode === 402) {
const opt = Array.isArray(res) ? res[0] : lastOptions;
handle402(opt, r);
}
} catch (e) {
// ignore
}
return res;
}
});
};
intercept('request');
intercept('uploadFile');
},
// 请求App权限
async requestPermissions() {
// #ifdef APP-PLUS
try {
console.log('[App] 开始请求App权限');
await PermissionManager.requestPermissionsOnLaunch();
} catch (error) {
console.error('[App] 请求权限失败:', error);
}
// #endif
}
}
}
</script>
<style>
/*每个页面公共css */
@import "@/common/delete-icon.css";
</style>