new Vue({ el: '#app', data() { return { currentPage: 1, // 当前页码 pageSize: 20, // 每页显示的条目数 total_trade_count:0, pageButtonCount: 5, accessToken: '', newSymbols: [], positions: [], previousPositions: [], trades: [], globalData: {}, timer: null, hideSmallPositions: true, coinInfo: {}, gateBalance: {}, binancePositions: [], isRefreshing: false, tooltips: [], // 用于存储tooltip实例 top10_symbols: {}, headers:{}, tradeLoading:true, logs:[], total_log_count:0, currentLogPage:1, totalLogPages:0, logLoading:true, baseUrl:'', symbolName:'', funding_data:{} } }, computed: { // 计算分页后的数据 paginatedData() { const start = (this.currentPage - 1) * this.pageSize; const end = start + this.pageSize; return this.data.slice(start, end); }, // 总页数 totalPages() { return Math.ceil(this.total_trade_count / this.pageSize); }, visiblePageNumbers() { const half = Math.floor(this.pageButtonCount / 2); let start = Math.max(2, this.currentPage - half); let end = Math.min(this.totalPages-1, this.currentPage + half); // 调整范围以确保显示固定数量的按钮 if (end - start + 1 < this.pageButtonCount) { if (start === 1) { end = Math.min(start + this.pageButtonCount - 1, this.totalPages); } else { start = Math.max(end - this.pageButtonCount + 1, 1); } } return Array.from({ length: end - start }, (_, i) => start + i +1); }, filteredPositions() { let positions = [...this.positions]; // 如果启用了隐藏小额持仓 if (this.hideSmallPositions) { positions = positions.filter(pos => { const value = parseFloat(pos.total_value.replace(' ', '')); return value >= 5; }); } return positions; } }, methods: { handleTradeSearch(){ this.fetchTrades() }, async changePage(page) { if (page > 0 && page <= this.totalPages) { this.currentPage = page; await this.fetchTrades() } }, async changeLogPage(page) { if (page > 0 && page <= this.totalLogPages) { this.currentLogPage = page; await this.fetchLogs() } }, getLevelClass(level){ switch(level){ case 'INFO': return 'bg-info' case 'WARN': return 'bg-warning' case 'ERROR': return 'bg-danger' default: return 'bg-secondary' } }, initTooltips() { // 先销毁现有的tooltips // this.destroyTooltips(); // 创建新的tooltips this.$nextTick(() => { const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]'); this.tooltips = Array.from(tooltipTriggerList).map(el => new bootstrap.Tooltip(el) ); }); }, destroyTooltips() { // 销毁所有现有的tooltips if (this.tooltips.length) { this.tooltips.forEach(tooltip => tooltip.dispose()); this.tooltips = []; } }, getPriceDiff(a, b){ return parseFloat(((a-b) / b * 100).toFixed(2)) }, async fetchTrades(isFresh=true){ if(isFresh){ this.tradeLoading = true } const tradesRes = await axios.get(`${this.baseUrl}/api/trades`, {headers:this.headers,params:{page:this.currentPage,symbol:this.symbolName.trim()}}).catch(() => null) if (tradesRes){ const tradeData = this.decryptData(tradesRes.data).data || []; this.trades = tradeData.trades this.total_trade_count = tradeData.total_count }else{ if(this.currentPage!==1){ this.currentPage -= 1; } } this.tradeLoading = false }, async fetchLogs(isFresh=true){ if(isFresh){ this.logLoading = true } const logsRes = await axios.get(`${this.baseUrl}/api/logs`, {headers:this.headers,params:{page:this.currentLogPage}}).catch(() => null) if (logsRes){ const logData = this.decryptData(logsRes.data).data || []; this.logs = logData.logs this.total_log_count = logData.total_count this.totalLogPages = Math.ceil(this.total_log_count / this.pageSize); }else{ if(this.currentLogPage!==1){ this.currentLogPage -= 1; } } this.logLoading = false }, async refreshData() { if (!this.accessToken) { return; } if (this.isRefreshing) return; this.previousPositions = [...this.positions]; this.isRefreshing = true; try { const [positionsRes, globalDataRes, gateBalanceRes, binancePositionsRes] = await Promise.all([ axios.get(`${this.baseUrl}/api/positions`, {headers:this.headers}).catch(() => null), axios.get(`${this.baseUrl}/api/global_data`, {headers:this.headers}).catch(() => null), axios.get(`${this.baseUrl}/api/gate_balance`, {headers:this.headers}).catch(() => null), axios.get(`${this.baseUrl}/api/binance_positions`, {headers:this.headers}).catch(() => null), ]); if(this.currentPage===1){ await this.fetchTrades(false) } if(this.currentLogPage===1){ await this.fetchLogs(false) } if (positionsRes) this.positions = this.decryptData(positionsRes.data).data || []; if (globalDataRes) this.globalData = this.decryptData(globalDataRes.data).data || {}; if (gateBalanceRes) this.gateBalance = this.decryptData(gateBalanceRes.data).data || {}; if (binancePositionsRes) this.binancePositions = this.decryptData(binancePositionsRes.data).data || {}; for (let pos of this.positions) { if (!this.previousPositions.some(p => p.symbol === pos.symbol)) { this.newSymbols.push(pos.symbol); } } // 更新完数据后重新初始化tooltips this.initTooltips(); } catch (error) { console.error('Error fetching data:', error); } finally { this.isRefreshing = false; } }, startAutoRefresh() { this.timer = setInterval(this.refreshData, 20000); }, stopAutoRefresh() { if (this.timer) { clearInterval(this.timer); } }, handleImageError(event, symbol) { // 图片加载失败时使用默认图标 event.target.src = 'https://cdn.jsdelivr.net/gh/spothq/cryptocurrency-icons@master/32/color/generic.png'; }, formatDuration(input) { if (!input) return '0分钟'; let seconds; if (typeof input === 'number' && input > 1000000000) { // 如果是时间戳(当前持仓) seconds = Math.floor(Date.now() / 1000) - input; } else { // 如果是秒数(历史交易) seconds = input; } const hours = Math.floor(seconds / 3600); const minutes = Math.floor((seconds % 3600) / 60); if (hours > 0) { return `${hours}时${minutes}分钟`; } else { return `${minutes}分钟`; } }, isNewPosition(symbol) { return this.newSymbols.includes(symbol); }, decryptData(encryptedData) { const key = CryptoJS.enc.Utf8.parse(this.accessToken.slice(16, 32)); const iv = CryptoJS.enc.Utf8.parse(this.accessToken.slice(0, 16)); try { // Base64解码 const decodeBase64 = CryptoJS.enc.Base64.parse(encryptedData); // 解密 const decrypted = CryptoJS.AES.decrypt( {ciphertext: decodeBase64}, key, { iv: iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 } ); // 转换为字符串并解析JSON const decryptedStr = decrypted.toString(CryptoJS.enc.Utf8); return JSON.parse(decryptedStr); } catch (error) { console.error('Decryption error:', error); return null; } }, }, mounted() { const parsedUrl = new URL(window.location.href); this.baseUrl = parsedUrl.origin; const accessToken = parsedUrl.searchParams.get('access_token'); if (!accessToken) { window.location.href = 'about:blank'; } if (accessToken) { this.accessToken = accessToken; this.headers = { 'Authorization': `Bearer ${this.accessToken}` } this.refreshData(); this.startAutoRefresh(); } }, beforeDestroy() { this.stopAutoRefresh(); this.destroyTooltips(); // 组件销毁时清理tooltips } });