Przeglądaj źródła

Reapply "12/4完成工作"

This reverts commit fcac748180b57dde761bf9a17cca0c9a26621cff.
Hexinkui 3 tygodni temu
rodzic
commit
b08f6f636b
3 zmienionych plików z 313 dodań i 202 usunięć
  1. 4 4
      src/api/index.js
  2. 24 80
      src/views/index/components/HotCoin.vue
  3. 285 118
      src/views/market/Index.vue

+ 4 - 4
src/api/index.js

@@ -20,7 +20,7 @@ export function GetCoins() {
 //获取K线
 //获取K线
 export function GetCandlestickChart(id) {
 export function GetCandlestickChart(id) {
   return request({
   return request({
-    url: `/finance/trading_pair/get_kline/${id?.symbol}`,
+    url: `/finance/trading_pair/${id?.symbol}/get_kline/`,
     method: 'get',
     method: 'get',
     params: {
     params: {
         interval: id.period,
         interval: id.period,
@@ -33,11 +33,11 @@ export function GetCandlestickChart(id) {
 //交易对
 //交易对
 export function TradingPair(id) {
 export function TradingPair(id) {
     return request({
     return request({
-    url: `/finance/trading_pair/${id?.symbol}`,
+    url: `/finance/trading_pair/`,
     method: 'get',
     method: 'get',
     params: {
     params: {
-        interval: id.period,
-        limit: 150
+        pageSize: id.pageSize,
+        pageNum: id.pageNum,
     }
     }
   })
   })
 }
 }

+ 24 - 80
src/views/index/components/HotCoin.vue

@@ -37,8 +37,8 @@
       </div>
       </div>
     </div>
     </div>
     <div class="coin-body" >
     <div class="coin-body" >
-     <div v-show="item.change_rate" class="body-item" v-for="(item, index) in coinList" :key="index">
-      <div class="item-left" @click="router.push({ path: '/marketDetails', query: { id: item.id,type: item.symbol.toLowerCase()} })">
+     <div class="body-item" v-for="(item, index) in coinList" :key="item.id || index">
+      <div  class="item-left" @click="router.push({ path: '/marketDetails', query: { id: item.id, type: item.symbol.toLowerCase()} })">
         <div class="coin-img" >
         <div class="coin-img" >
           <img :src="item.logo" alt="" />
           <img :src="item.logo" alt="" />
         </div>
         </div>
@@ -48,19 +48,20 @@
         </div>
         </div>
         <div class="coin-echars"></div>
         <div class="coin-echars"></div>
         <div class="coin-price">
         <div class="coin-price">
-          <div class="upper-price pf500 fs14 fc2C3131">{{ formatPrice(item.price) }}</div>
-          <div class="letter-price pf400 fs10 fcA9A9A9">≈ ${{ formatPrice(item.price) }}</div>
+          <div class="upper-price pf500 fs14 fc2C3131">{{ formatPrice(item.current_price) }}</div>
+          <div class="letter-price pf400 fs10 fcA9A9A9">≈ ${{ formatPrice(item.current_price) }}</div>
         </div>
         </div>
       </div>
       </div>
-      <div class="item-right pf500 fs12 fcFFFFFF" :class="getChangeColor(item.change_rate)">{{ formatChange(item.change_rate) }}</div>
+      <div  class="item-right pf500 fs12 fcFFFFFF" :class="getChangeColor(item.trend)">{{ formatChange(item.trend) }}</div>
     </div>
     </div>
     </div>
     </div>
   </div>
   </div>
 </template>
 </template>
+
 <script setup>
 <script setup>
 import { GetCoins } from '@/api/index'
 import { GetCoins } from '@/api/index'
 import { ref, onMounted, onUnmounted } from 'vue'
 import { ref, onMounted, onUnmounted } from 'vue'
- import { useRoute, useRouter } from "vue-router";
+import { useRoute, useRouter } from "vue-router";
 
 
 const router = useRouter();
 const router = useRouter();
 
 
@@ -74,24 +75,22 @@ let isUnmounted = false
 const formatSymbol = (symbol) => symbol ? symbol.replace('USDT', '') : ''
 const formatSymbol = (symbol) => symbol ? symbol.replace('USDT', '') : ''
 const formatPrice = (price) => price ? parseFloat(price).toFixed(2) : '0.00'
 const formatPrice = (price) => price ? parseFloat(price).toFixed(2) : '0.00'
 
 
-// 格式化涨跌幅:+9.01%
+// 格式化涨跌幅
 const formatChange = (val) => {
 const formatChange = (val) => {
   if (!val) return '+0.00%'
   if (!val) return '+0.00%'
   const num = parseFloat(val)
   const num = parseFloat(val)
-  // 正数加 + 号,负数自带 - 号
   return (num > 0 ? '+' : '') + num.toFixed(2) + '%'
   return (num > 0 ? '+' : '') + num.toFixed(2) + '%'
 }
 }
 
 
-// 获取颜色:涨绿跌红
+// 获取颜色
 const getChangeColor = (val) => {
 const getChangeColor = (val) => {
-  if (!val) return 'bg-gray' // 没有数据时显示灰色
+  if (!val) return 'bg-gray'
   return parseFloat(val) >= 0 ? 'bg-green' : 'bg-red'
   return parseFloat(val) >= 0 ? 'bg-green' : 'bg-red'
 }
 }
 
 
 // --- 1. WebSocket 核心逻辑 ---
 // --- 1. WebSocket 核心逻辑 ---
 
 
 const initWebSocket = () => {
 const initWebSocket = () => {
-  // 🔒【防死循环】清理旧连接
   if (socket) {
   if (socket) {
     socket.onclose = null
     socket.onclose = null
     socket.close()
     socket.close()
@@ -100,37 +99,25 @@ const initWebSocket = () => {
 
 
   if (coinList.value.length === 0) return
   if (coinList.value.length === 0) return
 
 
-  // 🔒【参数生成】
-  // 列表里是 BTCUSDT (大写) -> 转成 btcusdt (小写) -> 用 / 拼接
-  // 结果: "btcusdt/ethusdt/bnbusdt..."
   const symbolsParam = coinList.value
   const symbolsParam = coinList.value
     .map(item => item.symbol.toLowerCase())
     .map(item => item.symbol.toLowerCase())
     .join('/')
     .join('/')
 
 
   const query = `?symbol=${symbolsParam}`
   const query = `?symbol=${symbolsParam}`
 
 
-  // 2. 确定地址
-  // 暂时直连真实 IP,排除本地代理干扰
-  // const host = '63.141.230.43:57676'
-  // const host = 'http://localhost:8080'
-  // 等调试通了,以后上线前可以改回这样:
   const host = process.env.NODE_ENV === 'production'
   const host = process.env.NODE_ENV === 'production'
     ? 'backend.66linknow.com'
     ? 'backend.66linknow.com'
-    : 'localhost:8080' // 开发环境走代理
-    const wsUrl = `ws://${host}/ws/kline/${query}`
-
-  // console.log('🚀 开始连接:', wsUrl)
+    : 'localhost:8080'
+  const wsUrl = `ws://${host}/ws/kline/${query}`
 
 
   try {
   try {
     socket = new WebSocket(wsUrl)
     socket = new WebSocket(wsUrl)
   } catch (err) {
   } catch (err) {
-    // console.error('WS 初始化失败:', err)
     reconnect()
     reconnect()
     return
     return
   }
   }
 
 
   socket.onopen = () => {
   socket.onopen = () => {
-    // console.log('✅ 连接成功')
     startHeartbeat()
     startHeartbeat()
     if (reconnectTimer) clearTimeout(reconnectTimer)
     if (reconnectTimer) clearTimeout(reconnectTimer)
   }
   }
@@ -139,8 +126,6 @@ const initWebSocket = () => {
     if (event.data === 'pong' || event.data === 'ping') return
     if (event.data === 'pong' || event.data === 'ping') return
     try {
     try {
       const msg = JSON.parse(event.data)
       const msg = JSON.parse(event.data)
-
-      // 兼容两种数据结构 (有时候后端会包一层 data)
       if (msg.data) {
       if (msg.data) {
         updateCoinData(msg.data)
         updateCoinData(msg.data)
       } else {
       } else {
@@ -154,9 +139,6 @@ const initWebSocket = () => {
   }
   }
 
 
   socket.onclose = (e) => {
   socket.onclose = (e) => {
-    // console.log(`⚠️ 断开 (Code: ${e.code})`)
-    // console.log('关闭原因:', e)
-    // console.log('是否正常关闭:', e.wasClean)
     if (e.code === 1000) return
     if (e.code === 1000) return
     socket = null
     socket = null
     reconnect()
     reconnect()
@@ -165,25 +147,21 @@ const initWebSocket = () => {
 
 
 // --- 2. 更新数据 (核心适配) ---
 // --- 2. 更新数据 (核心适配) ---
 const updateCoinData = (ticker) => {
 const updateCoinData = (ticker) => {
-  // ticker 是 WS 推送的数据:
-  // { s: "ASTERUSDT", c: "1.016", P: "9.013", ... }
+  // WS 推送格式: { s: "XRPUSDT", c: "2.18", P: "9.01" }
 
 
   if (!ticker || !ticker.s) return
   if (!ticker || !ticker.s) return
 
 
-  // 1. 找到列表里对应的币
-  // 列表里是 "BTCUSDT",WS 推送里 s 也是 "BTCUSDT"
-  // 统一转大写对比,确保匹配
   const targetCoin = coinList.value.find(item =>
   const targetCoin = coinList.value.find(item =>
     item.symbol.toUpperCase() === ticker.s.toUpperCase()
     item.symbol.toUpperCase() === ticker.s.toUpperCase()
   )
   )
 
 
   if (targetCoin) {
   if (targetCoin) {
-    // 2. 更新价格 (c = current price)
-    if (ticker.c) targetCoin.price = ticker.c
+    // 【修改点4】: WebSocket 更新时,赋值给新的字段名
+    // c = current price -> 赋值给 current_price
+    if (ticker.c) targetCoin.current_price = ticker.c
 
 
-    // 3. 更新涨跌幅 (P = percentage change)
-    // 把 WS 里的 P 字段赋值给列表项的 change_rate
-    if (ticker.P) targetCoin.change_rate = ticker.P
+    // P = percentage change -> 赋值给 trend
+    if (ticker.P) {targetCoin.trend = ticker.P; targetCoin.p = ticker.P}
   }
   }
 }
 }
 
 
@@ -211,11 +189,10 @@ const startHeartbeat = () => {
 
 
 onMounted(async () => {
 onMounted(async () => {
   try {
   try {
-    // 1. 先拿列表 (只有价格,没有涨跌幅)
     const res = await GetCoins()
     const res = await GetCoins()
+    // 注意:确保 res 已经是数组,或者取 res.data
     coinList.value = Array.isArray(res) ? res : (res.data || [])
     coinList.value = Array.isArray(res) ? res : (res.data || [])
 
 
-    // 2. 再连 WS (获取实时数据)
     if (coinList.value.length > 0) {
     if (coinList.value.length > 0) {
       initWebSocket()
       initWebSocket()
     }
     }
@@ -235,7 +212,9 @@ onUnmounted(() => {
   }
   }
 })
 })
 </script>
 </script>
+
 <style lang="less" scoped>
 <style lang="less" scoped>
+/* 样式保持不变,省略以节省空间 */
   .hot-coin {
   .hot-coin {
     margin-top: 20px;
     margin-top: 20px;
     width: 346px;
     width: 346px;
@@ -383,43 +362,8 @@ onUnmounted(() => {
     }
     }
   }
   }
 
 
-//
-//  //xin
-//.body-item {
-//  display: flex;
-//  justify-content: space-between;
-//  align-items: center;
-//  padding: 10px 15px;
-//  border-bottom: 1px solid #f5f5f5;
-//}
-//.item-left {
-//  display: flex;
-//  align-items: center;
-//  gap: 10px;
-//}
-//.coin-img img {
-//  width: 32px;
-//  height: 32px;
-//  border-radius: 50%;
-//  object-fit: cover;
-//}
-///* .upper-name { font-weight: bold; font-size: 15px; color: #333; }
-//.letter-name { font-size: 12px; color: #999; }
-//.upper-price { font-weight: bold; font-size: 15px; margin-left: 10px; color: #333; } */
-//
-///* 右侧涨跌幅按钮 */
-//.item-right {
-//  padding: 6px 12px;
-//  border-radius: 4px;
-//  /* color: white; */
-//  /* font-weight: 500;
-//  font-size: 13px; */
-//  min-width: 75px;
-//  text-align: center;
-//  transition: background-color 0.3s;
-//}
 /* 颜色配置 */
 /* 颜色配置 */
-.bg-green { background-color: #2EBD85; }
-.bg-red { background-color: #F6465D; }
+.bg-green { background-color: #2EBD85!important; }
+.bg-red { background-color: #F6465D!important; }
 .bg-gray { background-color: #C0C0C0; }
 .bg-gray { background-color: #C0C0C0; }
-</style>
+</style>

+ 285 - 118
src/views/market/Index.vue

@@ -1,5 +1,10 @@
 <script setup>
 <script setup>
 import { ref, computed, watch, onMounted, onUnmounted } from 'vue';
 import { ref, computed, watch, onMounted, onUnmounted } from 'vue';
+// 引入您的真实接口函数
+import { TradingPair } from '@/api/index.js';
+import { useRoute, useRouter } from "vue-router";
+
+const router = useRouter();
 
 
 // --- 1. 状态管理 ---
 // --- 1. 状态管理 ---
 const tabs = [
 const tabs = [
@@ -17,7 +22,14 @@ const finished = ref(false);
 const scrollContainer = ref(null);
 const scrollContainer = ref(null);
 const PAGE_SIZE = 15; // 定义分页大小
 const PAGE_SIZE = 15; // 定义分页大小
 
 
-// --- 2. 图表与图标工具函数 (保持不变) ---
+// --- 2. WebSocket 核心变量 ---
+const socket = ref(null);
+let reconnectTimer = null; // 重连定时器
+let heartbeatTimer = null; // 心跳定时器
+let isManualClose = false; // 标记是否为手动关闭(切换Tab时),避免触发重连
+let lastSymbols = [];      // 记录最后一次订阅的币种,用于重连
+
+// --- 3. 图表与图标工具函数 ---
 const generateChartPaths = (isUp) => {
 const generateChartPaths = (isUp) => {
   const width = 60; const height = 24; const pointCount = 15; const step = width / (pointCount - 1);
   const width = 60; const height = 24; const pointCount = 15; const step = width / (pointCount - 1);
   let points = []; let y = isUp ? (height * 0.8) : (height * 0.2);
   let points = []; let y = isUp ? (height * 0.8) : (height * 0.2);
@@ -32,136 +44,206 @@ const generateChartPaths = (isUp) => {
   return { linePath, fillPath };
   return { linePath, fillPath };
 };
 };
 
 
-const icons = {
+// 本地 SVG 图标映射 (作为兜底备用)
+const localIcons = {
   BTC: '<path fill="#F7931A" d="M22.6 13.4c.4-2.6-1.6-4-4.3-4.9l.9-3.5-2.1-.5-.9 3.5c-.5-.1-1.1-.3-1.7-.4l.9-3.5-2.1-.5-.9 3.5c-.4-.1-1-.2-1.5-.3l-3-.7-.6 2.4s1.6.4 1.6.4c.9.2 1 .8 1 1.2l-1 4c.1 0 .1 0 .1.1l-1.4 5.6c0 .3-.3.8-1 .6 0 0-1.6-.4-1.6-.4l-1.1 2.6 2.8.7c.5.1 1 .3 1.5.4l-.9 3.6 2.1.5.9-3.6c.6.2 1.1.3 1.7.5l-.9 3.6 2.1.5.9-3.5c3.7.7 6.4.4 7.6-2.9.9-2.6-.1-4.1-1.9-5.1 1.4-.3 2.4-1.2 2.7-3z"/>',
   BTC: '<path fill="#F7931A" d="M22.6 13.4c.4-2.6-1.6-4-4.3-4.9l.9-3.5-2.1-.5-.9 3.5c-.5-.1-1.1-.3-1.7-.4l.9-3.5-2.1-.5-.9 3.5c-.4-.1-1-.2-1.5-.3l-3-.7-.6 2.4s1.6.4 1.6.4c.9.2 1 .8 1 1.2l-1 4c.1 0 .1 0 .1.1l-1.4 5.6c0 .3-.3.8-1 .6 0 0-1.6-.4-1.6-.4l-1.1 2.6 2.8.7c.5.1 1 .3 1.5.4l-.9 3.6 2.1.5.9-3.6c.6.2 1.1.3 1.7.5l-.9 3.6 2.1.5.9-3.5c3.7.7 6.4.4 7.6-2.9.9-2.6-.1-4.1-1.9-5.1 1.4-.3 2.4-1.2 2.7-3z"/>',
   ETH: '<path fill="#627EEA" d="M11.9 20.3L6.1 16.9l5.8-2.6 5.8 2.6-5.8 3.4zm0-9.6l5.8 2.6-5.8 8.1-5.8-8.1 5.8-2.6zM12 2l5.8 9.6L12 14 6.2 11.6 12 2z"/>',
   ETH: '<path fill="#627EEA" d="M11.9 20.3L6.1 16.9l5.8-2.6 5.8 2.6-5.8 3.4zm0-9.6l5.8 2.6-5.8 8.1-5.8-8.1 5.8-2.6zM12 2l5.8 9.6L12 14 6.2 11.6 12 2z"/>',
   BNB: '<path fill="#F3BA2F" d="M4.6 12l2.3-2.3L9.2 12l-2.3 2.3L4.6 12zM12 4.6l2.3 2.3-2.3 2.3-2.3-2.3L12 4.6zm7.4 7.4l-2.3 2.3 2.3 2.3 2.3-2.3-2.3-2.3zm-7.4 7.4l-2.3-2.3 2.3-2.3 2.3 2.3-2.3 2.3zm4.6-7.4l2.3-2.3-2.3-2.3-2.3 2.3 2.3 2.3zM12 14.8l-2.3-2.3 2.3-2.3 2.3 2.3L12 14.8zM9.7 7.4L12 5.1l2.3 2.3L12 9.7 9.7 7.4z"/>',
   BNB: '<path fill="#F3BA2F" d="M4.6 12l2.3-2.3L9.2 12l-2.3 2.3L4.6 12zM12 4.6l2.3 2.3-2.3 2.3-2.3-2.3L12 4.6zm7.4 7.4l-2.3 2.3 2.3 2.3 2.3-2.3-2.3-2.3zm-7.4 7.4l-2.3-2.3 2.3-2.3 2.3 2.3-2.3 2.3zm4.6-7.4l2.3-2.3-2.3-2.3-2.3 2.3 2.3 2.3zM12 14.8l-2.3-2.3 2.3-2.3 2.3 2.3L12 14.8zM9.7 7.4L12 5.1l2.3 2.3L12 9.7 9.7 7.4z"/>',
-  USDT: '<path fill="#26A17B" d="M14.5 10.4c.2-.2.3-.3.3-.3s-.1 0-.3 0c-.5.1-1.2.1-1.9.1-.1-2.9 0-2.9 0-2.9h3.1V5.7h-3.1V3H11v2.7H8V7.3h3v2.9c0 .1 0 .2-.1.2-2.8 0-5.1.8-5.1 1.7 0 1 2.3 1.7 5.2 1.7s5.2-.8 5.2-1.7c0-.7-1.3-1.2-3.4-1.5z"/>'
+  USDT: '<path fill="#26A17B" d="M14.5 10.4c.2-.2.3-.3.3-.3s-.1 0-.3 0c-.5.1-1.2.1-1.9.1-.1-2.9 0-2.9 0-2.9h3.1V5.7h-3.1V3H11v2.7H8V7.3h3v2.9c0 .1 0 .2-.1.2-2.8 0-5.1.8-5.1 1.7 0 1 2.3 1.7 5.2 1.7s5.2-.8 5.2-1.7c0-.7-1.3-1.2-3.4-1.5z"/>',
+  XRP: '<path fill="#23292F" d="M12 24c6.627 0 12-5.373 12-12S18.627 0 12 0 0 5.373 0 12s5.373 12 12 12z" /><path fill="#FFF" d="M9.82 12.01L4.65 6.84a.856.856 0 011.21-1.21l5.17 5.17a1.14 1.14 0 010 1.62L5.86 17.59a.856.856 0 01-1.21-1.21l5.17-4.37zM14.18 12.01l5.17 5.17a.856.856 0 01-1.21 1.21l-5.17-5.17a1.14 1.14 0 010-1.62l5.17-5.17a.856.856 0 011.21 1.21l-5.17 4.37z"/>',
+  SOL: '<path fill="#00FFA3" d="M3.5 16.5l2.1-3.6 14.9 0L18.4 16.5 3.5 16.5zM5.6 7.5L3.5 11.1 18.4 11.1 20.5 7.5 5.6 7.5zM20.5 20.1l-2.1 3.6L3.5 23.7 5.6 20.1 20.5 20.1z"/>',
+  DOGE: '<path fill="#C2A633" d="M12,2C6.48,2,2,6.48,2,12s4.48,10,10,10s10-4.48,10-10S17.52,2,12,2z M12,18c-3.31,0-6-2.69-6-6s2.69-6,6-6s6,2.69,6,6S15.31,18,12,18z"/>'
 };
 };
 
 
-// --- 3. 模拟 WebSocket 服务 ---
-const mockSocket = {
-  activeSubs: new Set(),
-  interval: null,
-  subscribe(ids, callback) {
-    ids.forEach(id => this.activeSubs.add(id));
-    if (!this.interval) {
-      this.interval = setInterval(() => {
-        const subArray = Array.from(this.activeSubs);
-        if (subArray.length === 0) return;
-        const updates = [];
-        const updateCount = Math.floor(Math.random() * 5) + 1;
-        for(let i=0; i<updateCount; i++) {
-          const randomId = subArray[Math.floor(Math.random() * subArray.length)];
-          const isUp = Math.random() > 0.5;
-          updates.push({
-            id: randomId,
-            price: 40000 + Math.random() * 1000,
-            change: (isUp ? 1 : -1) * (Math.random() * 5).toFixed(2),
-            isUp: isUp
-          });
-        }
-        callback(updates);
-      }, 500);
-    }
-  },
-  unsubscribeAll() {
-    this.activeSubs.clear();
-    if (this.interval) {
-      clearInterval(this.interval);
-      this.interval = null;
+// --- 4. 生产级 WebSocket 管理 ---
+
+// 启动心跳:防止连接因长时间无数据而被断开
+const startHeartbeat = () => {
+  clearInterval(heartbeatTimer);
+  heartbeatTimer = setInterval(() => {
+    if (socket.value && socket.value.readyState === WebSocket.OPEN) {
+      // 发送 ping 消息,具体内容看后端要求,通常是字符串 'ping' 或 JSON '{ "op": "ping" }'
+      socket.value.send("ping");
     }
     }
-  }
+  }, 15000); // 建议 15-30 秒一次
 };
 };
 
 
-// --- 4. 模拟 REST API ---
-const mockFetchData = (tab, pageNum) => {
-  return new Promise((resolve) => {
-    setTimeout(() => {
-      // 为了测试效果,改为只返回 10 条数据(小于 PAGE_SIZE 15)
-      // 这样第一页加载完就会立刻显示“没有更多了”
-      const count = 10;
-
-      // 如果不是第一页,就直接返回空,确保结束
-      if (pageNum > 1) { resolve([]); return; }
-
-      const newItems = Array.from({ length: count }).map((_, i) => {
-        const id = `${tab}-${(pageNum - 1) * PAGE_SIZE + i + 1}`;
-        const isUp = Math.random() > 0.5;
-        const paths = generateChartPaths(isUp);
-
-        let symbolPrefix = 'ABC';
-        if (tab === 'fav') symbolPrefix = 'FAV';
-        if (tab === 'spot') symbolPrefix = 'BTC';
-        if (tab === 'futures') symbolPrefix = 'ETH';
-
-        let realSymbol = symbolPrefix + ((pageNum - 1) * PAGE_SIZE + i + 1);
-        if (i % 5 === 0) realSymbol = 'BTC';
-        if (i % 5 === 1) realSymbol = 'ETH';
-
-        return {
-          id: id,
-          symbol: realSymbol,
-          name: `${realSymbol} Network`,
-          price: 40000 + Math.random() * 5000,
-          cny: 280000 + Math.random() * 30000,
-          change: (isUp ? 1 : -1) * (Math.random() * 10).toFixed(2),
-          svgIcon: icons[realSymbol],
-          chartLine: paths.linePath,
-          chartFill: paths.fillPath,
-          chartColor: isUp ? '#2EBD85' : '#F6465D',
-          btnClass: isUp ? 'btn-green' : 'btn-red'
-        };
-      });
-      resolve(newItems);
-    }, 500);
-  });
+const stopHeartbeat = () => {
+  clearInterval(heartbeatTimer);
 };
 };
 
 
-// --- 5. 核心逻辑:数据加载与订阅 ---
-
-const handleSocketUpdate = (updates) => {
-  updates.forEach(update => {
-    const item = listData.value.find(i => i.id === update.id);
-    if (item) {
-      item.price = update.price;
-      item.change = update.change;
-      item.cny = update.price * 7.2;
-      const isUp = update.change >= 0;
-      item.btnClass = isUp ? 'btn-green' : 'btn-red';
-      item.chartColor = isUp ? '#2EBD85' : '#F6465D';
-    }
-  });
+const connectWebSocket = (symbols) => {
+  // 如果是重连调用(不传参数),则使用上次的币种
+  if (!symbols && lastSymbols.length > 0) {
+    symbols = lastSymbols;
+  } else if (symbols) {
+    lastSymbols = symbols;
+  } else {
+    return; // 无币种可订阅
+  }
+
+  // 关闭旧连接
+  isManualClose = true; // 标记为手动关闭,防止立刻触发 onclose 重连
+  if (socket.value) {
+    socket.value.close();
+  }
+  clearTimeout(reconnectTimer);
+  stopHeartbeat();
+
+  // 构造参数
+  const symbolStr = symbols.map(s => s.toLowerCase()).join('/');
+  if (!symbolStr) return;
+
+  const wsUrl = `ws://localhost:8080/ws/kline/?symbol=${symbolStr}`;
+  console.log('Connecting WS:', wsUrl);
+
+  try {
+    isManualClose = false; // 重置标记,准备开始新连接
+    socket.value = new WebSocket(wsUrl);
+
+    socket.value.onopen = () => {
+      console.log('WS Connected');
+      startHeartbeat(); // 连接成功,开启心跳
+    };
+
+    socket.value.onmessage = (event) => {
+      try {
+        const msg = JSON.parse(event.data);
+        // 如果收到 pong 消息,可以忽略
+        if (msg === 'pong' || msg.op === 'pong') return;
+
+        const data = msg.data || msg;
+        const symbol = data.s || data.symbol;
+        if (!symbol) return;
+
+        const item = listData.value.find(i =>
+          i.name && i.name.toUpperCase() === symbol.toUpperCase()
+        );
+
+        if (item) {
+          // 1. 实时更新价格
+          let newPrice = data.c || data.p || (data.k ? data.k.c : null);
+          if (newPrice) {
+            item.price = newPrice;
+            item.cny = (parseFloat(newPrice) * 7.25).toFixed(2);
+          }
+
+          // 2. 实时更新涨跌幅
+          let newChange = data.P || data.trend;
+          if (newChange) {
+            item.change = parseFloat(newChange).toFixed(2);
+            const isUp = parseFloat(newChange) >= 0;
+            item.btnClass = isUp ? 'btn-green' : 'btn-red';
+            item.chartColor = isUp ? '#2EBD85' : '#F6465D';
+          }
+        }
+      } catch (e) {
+        // console.error("WS Message Error:", e);
+      }
+    };
+
+    socket.value.onclose = (e) => {
+      stopHeartbeat();
+      // 如果不是手动切换Tab导致的关闭,则尝试重连
+      if (!isManualClose) {
+        console.log('WS Disconnected unexpectedly. Reconnecting in 3s...');
+        reconnectTimer = setTimeout(() => {
+          connectWebSocket(); // 尝试重连
+        }, 3000);
+      }
+    };
+
+    socket.value.onerror = (err) => {
+      console.warn("WS Connection Error:", err);
+      // error 后通常会自动触发 close,逻辑交给 onclose 处理
+    };
+  } catch (e) {
+    console.error("WS Create Error:", e);
+    // 创建失败也尝试重连
+    reconnectTimer = setTimeout(() => {
+      connectWebSocket();
+    }, 5000);
+  }
 };
 };
 
 
+// --- 5. 真实接口请求封装 ---
+const fetchData = async (tab, pageNum) => {
+  try {
+    if (tab !== 'spot') return [];
+    const res = await TradingPair({
+      type: tab,
+      pageNum: pageNum,
+      pageSize: PAGE_SIZE
+    });
+
+    if (!res || !res.list) return [];
+
+    return res.list.map(item => {
+      const trendVal = parseFloat(item.trend || 0);
+      const isUp = trendVal >= 0;
+      const paths = generateChartPaths(isUp);
+      const iconKey = item.base_coin;
+
+      return {
+        id: item.id,
+        name: item.symbol,
+        symbol: item.name,
+        price: item.current_price,
+        cny: (parseFloat(item.current_price) * 7.25).toFixed(2),
+        change: trendVal.toFixed(2),
+        iconUrl: item.logo,
+        svgIcon: localIcons[iconKey] || null,
+        base_coin_char: item.base_coin ? item.base_coin[0] : '?',
+        chartLine: paths.linePath,
+        chartFill: paths.fillPath,
+        chartColor: isUp ? '#2EBD85' : '#F6465D',
+        btnClass: isUp ? 'btn-green' : 'btn-red'
+      };
+    });
+  } catch (error) {
+    console.error("加载数据失败:", error);
+    return [];
+  }
+};
+
+// --- 6. 核心逻辑:数据加载 ---
 const onLoad = async () => {
 const onLoad = async () => {
   if (loading.value || finished.value) return;
   if (loading.value || finished.value) return;
 
 
   loading.value = true;
   loading.value = true;
-  const data = await mockFetchData(currentTab.value, page.value);
 
 
-  // 核心优化逻辑:
+  const data = await fetchData(currentTab.value, page.value);
+
+  // console.log(`Tab: ${currentTab.value}, Page: ${page.value}, Loaded: ${data.length}`);
+
   if (data.length === 0) {
   if (data.length === 0) {
     finished.value = true;
     finished.value = true;
   } else {
   } else {
     listData.value.push(...data);
     listData.value.push(...data);
     page.value++;
     page.value++;
 
 
-    // 如果返回的数据条数小于分页大小,说明已经是最后一页了
-    // 直接标记完成,这样不需要再发一次空请求
     if (data.length < PAGE_SIZE) {
     if (data.length < PAGE_SIZE) {
       finished.value = true;
       finished.value = true;
     }
     }
-
-    const newIds = data.map(item => item.id);
-    mockSocket.subscribe(newIds, handleSocketUpdate);
+    console.log(listData.value,'llll');
+    const allSymbols = listData.value.map(item => item.name);
+    if (allSymbols.length > 0) {
+      connectWebSocket(allSymbols);
+    }
   }
   }
   loading.value = false;
   loading.value = false;
 };
 };
 
 
 // 监听 Tab 切换
 // 监听 Tab 切换
 watch(currentTab, () => {
 watch(currentTab, () => {
-  mockSocket.unsubscribeAll();
+  isManualClose = true; // 切换前标记手动关闭
+  if (socket.value) {
+    socket.value.close();
+    socket.value = null;
+  }
+  // 清理重连定时器,避免在切换Tab期间触发上一个Tab的重连
+  clearTimeout(reconnectTimer);
+
   listData.value = [];
   listData.value = [];
   page.value = 1;
   page.value = 1;
   finished.value = false;
   finished.value = false;
@@ -172,16 +254,46 @@ watch(currentTab, () => {
 
 
 const handleScroll = (e) => {
 const handleScroll = (e) => {
   const { scrollTop, clientHeight, scrollHeight } = e.target;
   const { scrollTop, clientHeight, scrollHeight } = e.target;
-  if (scrollTop + clientHeight >= scrollHeight - 50) {
+  if (scrollTop + clientHeight >= scrollHeight - 100) {
     onLoad();
     onLoad();
   }
   }
 };
 };
 
 
-onMounted(() => onLoad());
-onUnmounted(() => mockSocket.unsubscribeAll());
+// 处理页面可见性变化(切后台再回来)
+const handleVisibilityChange = () => {
+  if (document.visibilityState === 'visible') {
+    // 如果回来发现连接断了,立即重连
+    if (!socket.value || socket.value.readyState === WebSocket.CLOSED) {
+      console.log('Page visible, checking WS connection...');
+      connectWebSocket();
+    }
+  }
+};
 
 
-const formatPrice = (val) => val.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
-const formatCNY = (val) => val.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
+onMounted(() => {
+  onLoad();
+  document.addEventListener('visibilitychange', handleVisibilityChange);
+});
+
+onUnmounted(() => {
+  document.removeEventListener('visibilitychange', handleVisibilityChange);
+  isManualClose = true;
+  if (socket.value) socket.value.close();
+  clearTimeout(reconnectTimer);
+  stopHeartbeat();
+});
+
+// 格式化工具函数
+const formatPrice = (val) => {
+  if(!val) return '0.00';
+  const num = parseFloat(val);
+  return num.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 8 });
+};
+const formatCNY = (val) => {
+  if(!val) return '0.00';
+  const num = parseFloat(val);
+  return num.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
+};
 </script>
 </script>
 
 
 <template>
 <template>
@@ -218,10 +330,29 @@ const formatCNY = (val) => val.toLocaleString('en-US', { minimumFractionDigits:
         </div>
         </div>
       </div>
       </div>
 
 
+      <!-- 表头增加排序图标 -->
       <div class="table-header">
       <div class="table-header">
-        <div class="col col-left">交易对</div>
-        <div class="col col-center">最新价</div>
-        <div class="col col-right">今日涨跌幅</div>
+        <div class="col col-left">
+          交易对
+          <div class="sort-box">
+             <div class="sort-up"></div>
+             <div class="sort-down"></div>
+          </div>
+        </div>
+        <div class="col col-center">
+          最新价
+          <div class="sort-box">
+             <div class="sort-up"></div>
+             <div class="sort-down"></div>
+          </div>
+        </div>
+        <div class="col col-right">
+          今日涨跌幅
+          <div class="sort-box">
+             <div class="sort-up"></div>
+             <div class="sort-down"></div>
+          </div>
+        </div>
       </div>
       </div>
     </div>
     </div>
 
 
@@ -234,11 +365,13 @@ const formatCNY = (val) => val.toLocaleString('en-US', { minimumFractionDigits:
         v-for="coin in listData"
         v-for="coin in listData"
         :key="coin.id"
         :key="coin.id"
         class="list-item"
         class="list-item"
+        @click="router.push({ path: '/marketDetails', query: { id: coin.id, type: coin.name.toLowerCase()} })"
       >
       >
         <div class="col col-left coin-info">
         <div class="col col-left coin-info">
           <div class="coin-icon-wrapper">
           <div class="coin-icon-wrapper">
-            <svg v-if="coin.svgIcon" viewBox="0 0 32 32" class="real-icon" v-html="coin.svgIcon"></svg>
-            <div v-else class="placeholder-icon">{{ coin.symbol[0] }}</div>
+            <img v-if="coin.iconUrl" :src="coin.iconUrl" class="real-icon-img" alt="icon" />
+            <svg v-else-if="coin.svgIcon" viewBox="0 0 32 32" class="real-icon" v-html="coin.svgIcon"></svg>
+            <div v-else class="placeholder-icon">{{ coin.base_coin_char }}</div>
           </div>
           </div>
           <div class="text-group">
           <div class="text-group">
             <div class="name-row">{{ coin.name }}</div>
             <div class="name-row">{{ coin.name }}</div>
@@ -266,16 +399,18 @@ const formatCNY = (val) => val.toLocaleString('en-US', { minimumFractionDigits:
         </div>
         </div>
       </div>
       </div>
 
 
-      <!-- 底部状态区:使用 v-show 或 v-if 确保渲染 -->
+      <!-- 底部状态区 -->
       <div class="loading-state">
       <div class="loading-state">
         <div v-if="loading" class="spinner-container">
         <div v-if="loading" class="spinner-container">
           <div class="spinner"></div>
           <div class="spinner"></div>
           <span class="loading-text">加载中...</span>
           <span class="loading-text">加载中...</span>
         </div>
         </div>
-        <!-- 这里确保 finished 为 true 时一定会显示 -->
-        <div v-else-if="finished" class="no-more-text">
+        <div v-else-if="finished && listData.length > 0" class="no-more-text">
           - 没有更多了 -
           - 没有更多了 -
         </div>
         </div>
+        <div v-else-if="finished && listData.length === 0" class="empty-state">
+          暂无数据
+        </div>
       </div>
       </div>
     </div>
     </div>
   </div>
   </div>
@@ -353,13 +488,36 @@ const formatCNY = (val) => val.toLocaleString('en-US', { minimumFractionDigits:
 .col { display: flex; }
 .col { display: flex; }
 .col-left { width: 38%; justify-content: flex-start; align-items: center; }
 .col-left { width: 38%; justify-content: flex-start; align-items: center; }
 .col-center { width: 25%; justify-content: center; align-items: center; margin-left: 10px; }
 .col-center { width: 25%; justify-content: center; align-items: center; margin-left: 10px; }
+/* 修正右侧列样式,支持排序图标 */
 .col-right { width: 37%; justify-content: flex-end; align-items: center; }
 .col-right { width: 37%; justify-content: flex-end; align-items: center; }
 
 
+/* 排序小三角 */
+.sort-box {
+  display: flex;
+  flex-direction: column;
+  margin-left: 4px;
+  gap: 2px;
+  cursor: pointer;
+}
+.sort-up {
+  width: 0;
+  height: 0;
+  border-left: 3px solid transparent;
+  border-right: 3px solid transparent;
+  border-bottom: 4px solid #B7BDC6;
+}
+.sort-down {
+  width: 0;
+  height: 0;
+  border-left: 3px solid transparent;
+  border-right: 3px solid transparent;
+  border-top: 4px solid #B7BDC6;
+}
+
 .list-container {
 .list-container {
   flex: 1;
   flex: 1;
   overflow-y: auto;
   overflow-y: auto;
-  /* 增加底部 Padding,确保“没有更多了”不被遮挡 */
-  padding: 0 15px 60px;
+  padding: 0 15px 80px;
 }
 }
 
 
 .list-item {
 .list-item {
@@ -371,8 +529,10 @@ const formatCNY = (val) => val.toLocaleString('en-US', { minimumFractionDigits:
 
 
 .coin-icon-wrapper {
 .coin-icon-wrapper {
   width: 28px; height: 28px; margin-right: 8px; flex-shrink: 0;
   width: 28px; height: 28px; margin-right: 8px; flex-shrink: 0;
+  display: flex; align-items: center; justify-content: center;
 }
 }
 .real-icon { width: 100%; height: 100%; }
 .real-icon { width: 100%; height: 100%; }
+.real-icon-img { width: 100%; height: 100%; border-radius: 50%; object-fit: cover; }
 .placeholder-icon {
 .placeholder-icon {
   width: 100%; height: 100%; background: #F0F3F5; border-radius: 50%;
   width: 100%; height: 100%; background: #F0F3F5; border-radius: 50%;
   color: #707A8A; display: flex; align-items: center; justify-content: center;
   color: #707A8A; display: flex; align-items: center; justify-content: center;
@@ -407,7 +567,7 @@ const formatCNY = (val) => val.toLocaleString('en-US', { minimumFractionDigits:
   display: flex;
   display: flex;
   justify-content: center;
   justify-content: center;
   align-items: center;
   align-items: center;
-  min-height: 80px; /* 增加高度确保可见 */
+  min-height: 80px;
   width: 100%;
   width: 100%;
 }
 }
 
 
@@ -431,11 +591,18 @@ const formatCNY = (val) => val.toLocaleString('en-US', { minimumFractionDigits:
   color: #999;
   color: #999;
 }
 }
 
 
-/* 优化后的“没有更多了”样式:深灰色文字 */
 .no-more-text {
 .no-more-text {
-  color: #999;
-  font-size: 12px;
+  color: #555;
+  font-size: 13px;
   padding: 8px 16px;
   padding: 8px 16px;
+  background-color: #f0f0f0;
+  border-radius: 20px;
+}
+
+.empty-state {
+  color: #999;
+  font-size: 14px;
+  padding-top: 40px;
 }
 }
 
 
 @keyframes spin {
 @keyframes spin {