diff --git a/App.vue b/App.vue index f714d78..98a46aa 100644 --- a/App.vue +++ b/App.vue @@ -1,71 +1,32 @@ \ No newline at end of file +/* 每个页面公共css */ +page { + background-color: #F3F4F6; + font-family: 'Inter', 'Noto Sans SC', sans-serif; + --brand-color: #064e3b; + --brand-light: #10B981; +} + +/* 通用工具类 */ +.bg-white { background-color: #ffffff; } +.rounded-2xl { border-radius: 32rpx; } +.shadow-sm { box-shadow: 0 2rpx 6rpx rgba(0,0,0,0.05); } +.flex-row { display: flex; flex-direction: row; } +.flex-col { display: flex; flex-direction: column; } +.items-center { align-items: center; } +.justify-between { justify-content: space-between; } + diff --git a/main.js b/main.js index c1caf36..1366a39 100644 --- a/main.js +++ b/main.js @@ -13,8 +13,17 @@ app.$mount() // #ifdef VUE3 import { createSSRApp } from 'vue' +import api from './utils/api' + +console.log('🚀 应用启动,导入api模块') + export function createApp() { const app = createSSRApp(App) + + // 全局注册api + app.config.globalProperties.$api = api + console.log('✅ api已全局注册为 $api') + return { app } diff --git a/pages/index/index.vue b/pages/index/index.vue index 8172ffc..4e148a2 100644 --- a/pages/index/index.vue +++ b/pages/index/index.vue @@ -18,17 +18,17 @@ ¥ - {{ assetData.totalValue.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) }} + {{ (assetData.totalValue || 0).toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) }} 今日账面变动 - {{ assetData.todayProfit >= 0 ? '+' : '' }}¥{{ assetData.todayProfit.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) }} + {{ (assetData.todayProfit || 0) >= 0 ? '+' : '' }}¥{{ (assetData.todayProfit || 0).toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) }} 历史总变动 - {{ assetData.totalReturnRate >= 0 ? '+' : '' }}{{ assetData.totalReturnRate }}% + {{ (assetData.totalReturnRate || 0) >= 0 ? '+' : '' }}{{ assetData.totalReturnRate || 0 }}% @@ -75,11 +75,11 @@ 当前估值 - ¥ {{ holding.value.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) }} + ¥ {{ (holding.value || 0).toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) }} 历史总变动 - {{ holding.returnRate >= 0 ? '+' : '' }}{{ holding.returnRate }}% + {{ (holding.returnRate || 0) >= 0 ? '+' : '' }}{{ holding.returnRate || 0 }}% @@ -107,9 +107,11 @@ const holdings = ref([]); // 从后端API获取资产数据的函数 const fetchAssetData = async () => { try { + console.log('开始获取资产数据...'); const response = await api.assets.getAssetData(); if (response.code === 200) { assetData.value = response.data; + console.log('资产数据获取成功'); } } catch (error) { console.error('获取资产数据失败:', error); @@ -119,17 +121,23 @@ const fetchAssetData = async () => { // 从后端API获取持仓组合数据的函数 const fetchHoldingsData = async () => { try { + console.log('开始获取持仓数据...'); const response = await api.assets.getHoldings(); if (response.code === 200) { - holdings.value = response.data; + // 处理响应数据结构,获取 items 数组 + holdings.value = response.data.items || []; + console.log('持仓数据获取成功,items数量:', holdings.value.length); + console.log('持仓数据:', holdings.value); } } catch (error) { console.error('获取持仓数据失败:', error); } }; -// 页面加载时获取数据 +// 页面加载时检查登录状态 onMounted(async () => { + console.log('首页加载,开始加载数据...'); + await Promise.all([ fetchAssetData(), fetchHoldingsData() @@ -143,10 +151,6 @@ const goConfig = () => { const goDetail = (holdingId) => { uni.navigateTo({ url: `/pages/detail/detail?id=${holdingId}` }); }; - -const goStrategies = () => { - uni.switchTab({ url: '/pages/strategies/strategies' }); -}; \ No newline at end of file diff --git a/utils/api.js b/utils/api.js index b5cecc9..32d3346 100644 --- a/utils/api.js +++ b/utils/api.js @@ -2,12 +2,225 @@ * API工具类 - 统一封装后端API请求 */ +console.log('📦 api.js 模块加载成功') + // API基础URL -const BASE_URL = 'https://api.assetmanager.com'; +const BASE_URL = 'https://localhost:7040/'; // 请求超时时间 const TIMEOUT = 10000; +// 登录锁,防止并发登录 +let loginLock = null; + +/** + * 获取微信登录码 + * @returns {Promise} - 返回微信登录码 + */ +const getWechatCode = () => { + return new Promise((resolve, reject) => { + uni.login({ + provider: 'weixin', + success: (res) => { + if (res.code) { + console.log('📱 微信登录码获取成功:', res.code); + resolve(res.code); + } else { + console.error('📱 微信登录码获取失败:', res.errMsg); + reject(new Error(`微信登录码获取失败: ${res.errMsg}`)); + } + }, + fail: (err) => { + console.error('📱 微信登录失败:', err); + reject(new Error(`微信登录失败: ${err.errMsg}`)); + } + }); + }); +}; + +/** + * 执行微信登录 + * @returns {Promise} - 返回登录结果 + */ +const doWechatLogin = async () => { + try { + console.log('🔐 开始执行微信登录'); + const code = await getWechatCode(); + console.log('🔐 开始调用后端登录接口:', code); + + const res = await new Promise((resolve, reject) => { + uni.request({ + url: `${BASE_URL}api/auth/wechat-login`, + method: 'POST', + data: { code }, + header: { + 'Content-Type': 'application/json' + }, + timeout: TIMEOUT, + success: resolve, + fail: reject + }); + }); + + console.log('🔐 后端登录接口响应:', res); + + if (res.statusCode === 200 && res.data?.code === 200 && res.data?.data?.token) { + console.log('✅ 微信登录成功,获取到token和userId'); + uni.setStorageSync('token', res.data.data.token); + // 存储userId到本地存储 + const userId = res.data.data.userId || res.data.data.user?.userId; + if (userId) { + uni.setStorageSync('userId', userId); + console.log('✅ userId已存储:', userId); + // 验证存储是否成功 + const storedUserId = uni.getStorageSync('userId'); + console.log('✅ 验证userId存储结果:', storedUserId); + } else { + console.warn('⚠️ 登录响应中未找到userId'); + console.log('登录响应数据:', res.data.data); + } + return res.data; + } else { + console.error('❌ 微信登录失败:', res.data?.message || '登录失败'); + throw new Error(res.data?.message || '微信登录失败'); + } + } catch (error) { + console.error('❌ 微信登录异常:', error); + throw error; + } +}; + +/** + * 带重试机制的请求方法 + * @param {string} url - 请求URL + * @param {string} method - 请求方法 + * @param {object} data - 请求数据 + * @param {object} headers - 请求头 + * @param {number} retryCount - 重试次数 + * @returns {Promise} - 返回Promise对象 + */ +const requestWithRetry = async (url, method = 'GET', data = {}, headers = {}, retryCount = 0) => { + try { + // 获取存储的token + let token = uni.getStorageSync('token'); + + // 如果没有token,先执行微信登录(使用登录锁防止并发) + if (!token) { + if (loginLock) { + console.log('🔒 等待其他请求完成登录...'); + await loginLock; + token = uni.getStorageSync('token'); + } else { + console.log('🔒 未检测到token,开始微信登录...'); + loginLock = doWechatLogin(); + await loginLock; + loginLock = null; + token = uni.getStorageSync('token'); + console.log('🔒 微信登录成功,获取到token'); + } + } + + // 正确处理URL拼接,避免双斜杠 + let fullUrl; + if (BASE_URL.endsWith('/') && url.startsWith('/')) { + fullUrl = BASE_URL + url.substring(1); + } else if (!BASE_URL.endsWith('/') && !url.startsWith('/')) { + fullUrl = BASE_URL + '/' + url; + } else { + fullUrl = BASE_URL + url; + } + + // 请求开始日志 + console.log('🚀 API 请求开始:', { + method, + url: fullUrl, + data: method === 'GET' ? null : data, + hasToken: !!token, + retryCount, + timestamp: new Date().toISOString() + }); + + const res = await new Promise((resolve, reject) => { + uni.request({ + url: fullUrl, + method, + data, + header: { + 'Content-Type': 'application/json', + 'Authorization': token ? `Bearer ${token}` : '', + ...headers + }, + timeout: TIMEOUT, + success: resolve, + fail: reject + }); + }); + + // 响应日志 + console.log('✅ API 请求成功:', { + method, + url: fullUrl, + statusCode: res.statusCode, + responseTime: new Date().toISOString(), + data: res.data + }); + + if (res.statusCode === 200) { + return res.data; + } else if (res.statusCode === 401) { + // 未授权,清除token和userId并重新登录(使用登录锁防止并发) + console.log('🔒 登录已过期,开始重新登录...'); + uni.removeStorageSync('token'); + uni.removeStorageSync('userId'); + + // 重新登录后重试请求 + if (retryCount < 3) { + if (loginLock) { + console.log('🔒 等待其他请求完成登录...'); + await loginLock; + } else { + loginLock = doWechatLogin(); + await loginLock; + loginLock = null; + } + console.log('🔒 重新登录成功,开始重试请求...'); + return await requestWithRetry(url, method, data, headers, retryCount + 1); + } else { + console.error('❌ 达到最大重试次数'); + uni.showToast({ title: '系统异常,请稍后重试', icon: 'none', duration: 2000 }); + throw new Error('登录已过期,重试次数超限'); + } + } else { + console.log('❌ API 请求失败:', { + statusCode: res.statusCode, + message: res.data?.message || `请求失败: ${res.statusCode}` + }); + throw new Error(`请求失败: ${res.statusCode}`); + } + } catch (error) { + // 请求失败日志 + console.log('❌ API 请求失败:', { + url, + error, + retryCount, + timestamp: new Date().toISOString() + }); + + // 如果是网络错误且未达到最大重试次数,尝试重试 + if (retryCount < 3 && error.message && error.message.includes('网络请求失败')) { + console.log('🔄 网络错误,开始重试...'); + return await requestWithRetry(url, method, data, headers, retryCount + 1); + } + + // 达到最大重试次数,提示用户 + if (retryCount >= 2) { + uni.showToast({ title: '系统异常,请稍后重试', icon: 'none', duration: 2000 }); + } + + throw error; + } +}; + /** * 基础请求方法 * @param {string} url - 请求URL @@ -17,39 +230,7 @@ const TIMEOUT = 10000; * @returns {Promise} - 返回Promise对象 */ const request = (url, method = 'GET', data = {}, headers = {}) => { - // 获取存储的token - const token = uni.getStorageSync('token'); - return new Promise((resolve, reject) => { - uni.request({ - url: BASE_URL + url, - method, - data, - header: { - 'Content-Type': 'application/json', - 'Authorization': token ? `Bearer ${token}` : '', - ...headers - }, - timeout: TIMEOUT, - success: (res) => { - if (res.statusCode === 200) { - resolve(res.data); - } else if (res.statusCode === 401) { - // 未授权,清除token并重新登录 - uni.removeStorageSync('token'); - uni.removeStorageSync('userInfo'); - console.log('登录已过期,请重新登录'); - reject(new Error('登录已过期')); - } else { - reject(new Error(`请求失败: ${res.statusCode}`)); - } - }, - fail: (err) => { - // 当实际API不可用时,使用模拟数据 - console.log('API请求失败,使用模拟数据:', err); - resolve(getMockData(url, method, data)); - } - }); - }); + return requestWithRetry(url, method, data, headers); }; /** @@ -60,7 +241,6 @@ const request = (url, method = 'GET', data = {}, headers = {}) => { * @returns {Promise} - 返回Promise对象 */ export const get = (url, params = {}, headers = {}) => { - // 构建查询字符串 const queryString = Object.keys(params) .map(key => `${key}=${encodeURIComponent(params[key])}`) .join('&'); @@ -79,291 +259,46 @@ export const post = (url, data = {}, headers = {}) => { return request(url, 'POST', data, headers); }; -/** - * PUT请求 - * @param {string} url - 请求URL - * @param {object} data - 请求数据 - * @param {object} headers - 请求头 - * @returns {Promise} - 返回Promise对象 - */ -export const put = (url, data = {}, headers = {}) => { - return request(url, 'PUT', data, headers); -}; - -/** - * DELETE请求 - * @param {string} url - 请求URL - * @param {object} params - 请求参数 - * @param {object} headers - 请求头 - * @returns {Promise} - 返回Promise对象 - */ -export const del = (url, params = {}, headers = {}) => { - const queryString = Object.keys(params) - .map(key => `${key}=${encodeURIComponent(params[key])}`) - .join('&'); - const fullUrl = queryString ? `${url}?${queryString}` : url; - return request(fullUrl, 'DELETE', {}, headers); -}; - -/** - * 模拟数据 - * @param {string} url - 请求URL - * @param {string} method - 请求方法 - * @param {object} data - 请求数据 - * @returns {object} - 返回模拟数据 - */ -const getMockData = (url, method, data) => { - // 模拟API延迟 - return new Promise(resolve => { - setTimeout(() => { - // 资产相关模拟数据 - if (url.includes('/assets')) { - resolve({ - code: 200, - data: { - totalValue: 1284592.40, - currency: 'CNY', - todayProfit: 12482.00, - todayProfitCurrency: 'CNY', - totalReturnRate: 24.82 - }, - message: 'success' - }); - } - - // 持仓相关模拟数据 - if (url.includes('/holdings')) { - resolve({ - code: 200, - data: [ - { - id: 'hfea-001', - name: '美股全天候杠杆', - tags: 'HFEA · 季度调仓', - status: '监控中', - statusType: 'green', - iconChar: 'H', - iconBgClass: 'bg-green-100', - iconTextClass: 'text-green-700', - value: 156240.00, - currency: 'USD', - returnRate: 42.82, - returnType: 'positive' - }, - { - id: 'ma-002', - name: '纳指双均线趋势', - tags: '趋势跟踪 · 日线', - status: '等待信号', - statusType: 'gray', - iconChar: 'T', - iconBgClass: 'bg-blue-100', - iconTextClass: 'text-blue-700', - value: 412500.00, - currency: 'USD', - returnRate: -1.79, - returnType: 'negative' - }, - { - id: 'hk-003', - name: '港股价值投资', - tags: '价值投资 · 蓝筹', - status: '持有中', - statusType: 'green', - iconChar: 'H', - iconBgClass: 'bg-green-100', - iconTextClass: 'text-green-700', - value: 896000.00, - currency: 'HKD', - returnRate: 12.56, - returnType: 'positive' - } - ], - message: 'success' - }); - } - - // 策略相关模拟数据 - if (url.includes('/strategies')) { - resolve({ - code: 200, - data: [ - { - id: 'hfea', - iconChar: 'H', - title: 'HFEA 风险平价逻辑', - tag: '高风险 · 高预期收益', - desc: '针对杠杆ETF平衡的对冲策略,核心逻辑为 TMF (3x长债) 与 UPRO (3x标普) 的季度平衡。', - bgClass: 'bg-emerald-900', - tagClass: 'text-emerald-700', - tags: ['季调', '止损机制'], - btnText: '配置参数', - btnClass: 'btn-primary' - }, - { - id: 'ma', - iconChar: 'T', - title: '双均线趋势跟随', - tag: '中风险 · 低回撤要求', - desc: '利用快线(EMA10)上穿慢线(EMA60)捕捉强势波段,金叉买入,死叉离场。', - bgClass: 'bg-blue-600', - tagClass: 'text-blue-700', - tags: ['日线', '左侧止盈'], - btnText: '预览模型', - btnClass: 'btn-secondary' - }, - { - id: 'chandelier', - iconChar: 'S', - title: '吊灯止损策略', - tag: '风险控制 · 辅助', - desc: '基于 ATR 波动率计算的动态止损线,锁住利润,防止回撤过大。', - bgClass: 'bg-orange-500', - tagClass: 'text-orange-700', - tags: ['ATR', '动态止盈'], - btnText: '配置参数', - btnClass: 'btn-secondary' - } - ], - message: 'success' - }); - } - - // 用户信息模拟数据 - if (url.includes('/user/info')) { - resolve({ - code: 200, - data: { - userName: '首席策略员 0x42', - memberLevel: '全球实验室 Pro', - runningDays: 412 - }, - message: 'success' - }); - } - - // 用户统计数据模拟数据 - if (url.includes('/user/stats')) { - resolve({ - code: 200, - data: { - signalsCaptured: 1248, - winRate: 58.4 - }, - message: 'success' - }); - } - - // 应用信息模拟数据 - if (url.includes('/app/info')) { - resolve({ - code: 200, - data: { - version: 'v2.4.0', - currentDate: new Date().toISOString().split('T')[0] - }, - message: 'success' - }); - } - - // 微信登录模拟数据 - if (url.includes('/api/auth/wechat/login')) { - resolve({ - code: 200, - data: { - token: 'mock-wechat-token-123456', - userInfo: { - id: 'user-001', - openid: 'mock-openid-123456', - nickname: '微信用户', - avatar: 'https://via.placeholder.com/100', - memberLevel: '全球实验室 Pro', - runningDays: 412 - } - }, - message: 'success' - }); - } - - // 微信绑定模拟数据 - if (url.includes('/api/auth/wechat/bind')) { - resolve({ - code: 200, - data: { - token: 'mock-wechat-token-789012', - userInfo: { - id: 'user-001', - openid: 'mock-openid-123456', - email: 'user@example.com', - nickname: '微信用户', - avatar: 'https://via.placeholder.com/100', - memberLevel: '全球实验室 Pro', - runningDays: 412 - } - }, - message: 'success' - }); - } - - // 默认返回 - resolve({ - code: 200, - data: null, - message: 'success' - }); - }, 500); - }); -}; - /** * API接口封装 */ +console.log('📦 api.js 开始导出api对象') export const api = { - // 资产相关API assets: { - // 获取总资产数据 - getAssetData: () => get('/assets'), - // 获取持仓组合数据 - getHoldings: () => get('/holdings') + getAssetData: () => { + const userId = uni.getStorageSync('userId'); + console.log('📤 发起 getAssetData 请求,userId:', userId); + return get('/api/v1/portfolio/assets', { userId }); + }, + getHoldings: () => { + const userId = uni.getStorageSync('userId'); + console.log('📤 发起 getHoldings 请求,userId:', userId); + return get('/api/v1/portfolio', { userId }); + } }, - // 策略相关API strategies: { - // 获取策略列表 - getStrategies: () => get('/strategies'), - // 获取单个策略详情 - getStrategyDetail: (id) => get(`/strategies/${id}`), - // 创建新策略 - createStrategy: (data) => post('/strategies', data), - // 更新策略 - updateStrategy: (id, data) => put(`/strategies/${id}`, data), - // 删除策略 - deleteStrategy: (id) => del(`/strategies/${id}`) + getStrategies: () => { + const userId = uni.getStorageSync('userId'); + console.log('📤 发起 getStrategies 请求,userId:', userId); + return get('/api/v1/strategies', { userId }); + } }, - // 用户相关API user: { - // 获取用户信息 - getUserInfo: () => get('/user/info'), - // 获取用户统计数据 - getUserStats: () => get('/user/stats'), - // 更新用户信息 - updateUserInfo: (data) => put('/user/info', data) - }, - - // 认证相关API - auth: { - // 微信登录 - wechatLogin: (code) => post('/api/auth/wechat/login', { code }), - // 微信绑定 - wechatBind: (data) => post('/api/auth/wechat/bind', data) - }, - - // 应用相关API - app: { - // 获取应用信息 - getAppInfo: () => get('/app/info') + getUserInfo: () => { + const userId = uni.getStorageSync('userId'); + console.log('📤 发起 getUserInfo 请求,userId:', userId); + return get('/api/user/info', { userId }); + }, + getUserStats: () => { + const userId = uni.getStorageSync('userId'); + console.log('📤 发起 getUserStats 请求,userId:', userId); + return get('/api/user/stats', { userId }); + } } }; -export default api; \ No newline at end of file +console.log('📦 api.js 导出默认api对象') + +export default api;