372 lines
11 KiB
Vue
372 lines
11 KiB
Vue
<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>
|