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>
|