Bladeren bron

1. 热门币种列表(HotCoin)核心功能开发
【实时数据】WebSocket 核心链路打通: 完成了前端与行情 WebSocket 服务的对接,实现了从 HTTP 静态列表到 WS 实时数据的平滑切换。
【数据处理】数据动态适配与渲染:
实现了动态生成订阅参数逻辑(自动聚合列表币种,转换为后端要求的 BTCUSDT,ETHUSDT 格式)。
完成了 Ticker 数据映射,实现了价格跳动和涨跌幅颜色的实时更新(涨绿/跌红)。
2. 前端基础建设与工程化配置
【SVG 方案】图标组件化改造: 解决了 Vue 3 + Webpack 环境下 SVG 加载失真及控制难题。舍弃了 img 标签引入方式,通过配置 Loader 将 SVG 转换为 Vue 组件,实现了图标无损缩放及 CSS 变色控制。
【网络层】Axios 封装与跨域处理:
完成了 request.js 的统一封装,集成了 Token 携带及全局错误处理拦截器。
配置 vue.config.js 开发环境代理(Proxy),解决了本地开发调用后端接口时的跨域(CORS)及 404 路径重写问题。
3. 稳定性与健壮性优化(重点攻克)
WebSocket 连接治理:
修复连接死循环: 排查并修复了因参数格式(大小写/分隔符)错误导致后端断开,进而触发前端无限重连的 Bug。
优化重连机制: 重构了断线重连逻辑,增加了对正常关闭(Code 1000)的拦截,防止组件销毁后仍在后台重连。
心跳保活: 实现了标准的 Ping/Pong 心跳检测,防止连接因长时间空闲被 Nginx 断开。
资源释放: 优化了组件生命周期管理,确保在 onUnmounted 时正确销毁 Socket 实例及清除所有定时器,防止内存泄漏。

Hexinkui 4 weken geleden
bovenliggende
commit
c61dc542de

+ 2 - 0
.env.development

@@ -0,0 +1,2 @@
+# 这是你本地开发时,接口请求的地址
+VUE_APP_BASE_API = '/api'

+ 2 - 0
.env.production

@@ -0,0 +1,2 @@
+# 这是你上线后的接口地址
+VUE_APP_BASE_API = 'https://api.your-trading-site.com'

+ 3 - 3
package.json

@@ -17,9 +17,9 @@
     "vuex": "^4.0.0"
   },
   "overrides": {
-      "resolutions": {
-        "postcss": "^8.4.31"
-      }
+    "resolutions": {
+      "postcss": "^8.4.31"
+    }
   },
   "devDependencies": {
     "@vue/cli-plugin-router": "~5.0.0",

+ 0 - 1
public/index.html

@@ -4,7 +4,6 @@
     <meta charset="utf-8">
     <meta http-equiv="X-UA-Compatible" content="IE=edge">
     <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no, viewport-fit=cover">
-    <link rel="preload" as="image" href="src/assets/img/index/Rectangle 1.png">
     <title><%= htmlWebpackPlugin.options.title %></title>
   </head>
   <body>

+ 18 - 0
src/api/index.js

@@ -0,0 +1,18 @@
+import request from '@/utils/request'
+
+// 登录接口
+export function login(data) {
+  return request({
+    url: '/user/login',
+    method: 'post',
+    data
+  })
+}
+
+// 获取币币
+export function GetCoins() {
+  return request({
+    url: '/finance/trading_pair/get_display_coin/',
+    method: 'get'
+  })
+}

+ 0 - 7
src/api/user.js

@@ -1,7 +0,0 @@
-import { $get, $post } from "../utils/request.js";
-
-export default {
-  electronicMedicine(data) {
-    return $post("/xsts/electronic_medicine", data);
-  },
-};

+ 4 - 0
src/assets/icon/index/gg.svg

@@ -0,0 +1,4 @@
+<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
+<circle cx="16" cy="16" r="16" fill="#EFAA42"/>
+<path d="M13.0475 22.6001V24.2957C13.0475 24.4829 13.1994 24.6348 13.3866 24.6348H14.7432C14.8331 24.6348 14.9194 24.5991 14.983 24.5355C15.0466 24.4719 15.0823 24.3857 15.0823 24.2957V22.6001H15.7606V24.2957C15.7606 24.4829 15.9125 24.6348 16.0997 24.6348H17.4562C17.5462 24.6348 17.6324 24.5991 17.696 24.5355C17.7596 24.4719 17.7954 24.3857 17.7954 24.2957V22.6001H17.9093C20.6115 22.6001 22.5432 21.1988 22.5432 18.7747C22.5432 16.7371 21.1772 15.6234 19.5778 15.4647V15.3454C20.8937 15.0171 21.8609 14.0241 21.8609 12.3746C21.8609 10.2964 20.3009 9.03479 17.9202 9.03479H17.7954V7.33913C17.7954 7.24919 17.7596 7.16293 17.696 7.09933C17.6324 7.03573 17.5462 7 17.4562 7H16.0997C16.0098 7 15.9235 7.03573 15.8599 7.09933C15.7963 7.16293 15.7606 7.24919 15.7606 7.33913V9.03479H14.9833V7.33913C14.9833 7.24919 14.9476 7.16293 14.884 7.09933C14.8204 7.03573 14.7341 7 14.6441 7H13.3866C13.2967 7 13.2104 7.03573 13.1468 7.09933C13.0832 7.16293 13.0475 7.24919 13.0475 7.33913V9.03479L10.3372 9.04971C10.2472 9.04971 10.161 9.08544 10.0974 9.14904C10.0338 9.21264 9.99805 9.2989 9.99805 9.38884V10.7304C9.99805 10.9163 10.1473 11.0696 10.3345 11.0696L11.3586 11.0628C11.6273 11.0646 11.8843 11.1726 12.0737 11.3632C12.263 11.5538 12.3693 11.8115 12.3693 12.0802V19.5479C12.3693 19.8177 12.2621 20.0765 12.0713 20.2673C11.8805 20.4581 11.6217 20.5653 11.3519 20.5653L10.3372 20.5802C10.2472 20.5802 10.161 20.6159 10.0974 20.6795C10.0338 20.7431 9.99805 20.8294 9.99805 20.9193V22.2758C9.99805 22.463 10.15 22.615 10.3372 22.615L13.0475 22.6001ZM14.9833 11.0519H17.3152C18.5442 11.0519 19.2658 11.7275 19.2658 12.8317C19.2658 14.0132 18.4858 14.6793 16.7196 14.6793H14.9833V11.0519ZM14.9833 16.5472H17.4793C19.0217 16.5472 19.8614 17.334 19.8614 18.6146C19.8614 19.9073 19.0122 20.5815 16.934 20.5815H14.9833V16.5486V16.5472Z" fill="white"/>
+</svg>

+ 77 - 49
src/utils/request.js

@@ -1,55 +1,83 @@
-import axios from "axios";
-import store from "@/store";
+/* src/utils/request.js */
+import axios from 'axios'
+// 如果你用了 Vant UI,可以把下面这行解开,用来弹窗提示错误
+import { showToast, showFailToast } from 'vant';
+import 'vant/es/toast/style';
 
-// 中心化请求
-export function request(config) {
-  const instance = axios.create({
-    baseURL: "",
-    // 表示跨域请求时是否需要使用凭证
-    withCredentials: false,
-    timeout: 20000,
-  });
+// 1. 创建 axios 实例
+const service = axios.create({
+  // 基础 URL,通常在 .env 文件中配置 VUE_APP_BASE_API
+  baseURL: process.env.VUE_APP_BASE_API || '/api', 
+  // 请求超时时间 (毫秒),交易类应用建议设置短一点,比如 10秒
+  timeout: 10000, 
+  headers: {
+    'Content-Type': 'application/json;charset=utf-8'
+  }
+})
 
-  // 请求拦截器
-  instance.interceptors.request.use(
-    (config) => {
-      return config;
-    },
-    (err) => Promise.reject(err)
-  );
-
-  // 响应拦截器
-  instance.interceptors.response.use(
-    (res) => {
-      return res;
-    },
-    (err) => {
-      return Promise.reject(err);
+// 2. 请求拦截器 (Request Interceptor)
+// 在发送请求之前做些什么,比如加 Token
+service.interceptors.request.use(
+  config => {
+    // 假设你的 token 存在 localStorage 里
+    const token = localStorage.getItem('token')
+    
+    if (token) {
+      // 让每个请求携带自定义 token 请根据实际情况修改
+      // 例如:config.headers['Authorization'] = 'Bearer ' + token
+      config.headers['token'] = token 
     }
-  );
-  return instance(config);
-}
+    return config
+  },
+  error => {
+    // 对请求错误做些什么
+    console.log(error) 
+    return Promise.reject(error)
+  }
+)
 
-export function $get(url, data) {
-  return request({
-    url: url,
-    method: "get",
-    params: data,
-  });
-}
+// 3. 响应拦截器 (Response Interceptor)
+// 对响应数据做点什么
+service.interceptors.response.use(
+  response => {
+    const res = response.data
+    
+    // 这里根据后端返回的状态码来判断请求是否成功
+    // 假设后端约定 code === 00000 为成功
+    if (res.code !== "00000") {
+      // 如果不是 200,说明业务逻辑有错,比如“余额不足”
+      
+      // showFailToast(res.msg || 'Error') // 如果用了 Vant,可以用这个提示
+      console.error('业务报错:', res.msg)
 
-export function $post(url, data) {
-  return request({
-    url: url,
-    method: "post",
-    data,
-  });
-}
+      // 特殊处理:比如 401 表示 Token 过期,需要跳回登录页
+      if (res.code === 401) {
+        //以此处逻辑为准:清除本地数据,强制刷新或跳转
+        localStorage.removeItem('token')
+        location.reload()
+      }
+      
+      return Promise.reject(new Error(res.msg || 'Error'))
+    } else {
+      // 成功,直接把数据剥离出来
+      return res.data
+    }
+  },
+  error => {
+    console.log('网络报错' + error) // for debug
+    let message = error.message
+    
+    if (message == 'Network Error') {
+      message = '后端接口连接异常'
+    } else if (message.includes('timeout')) {
+      message = '系统接口请求超时'
+    } else if (message.includes('Request failed with status code')) {
+      message = '系统接口' + message.substr(message.length - 3) + '异常'
+    }
+    
+    showFailToast(message)
+    return Promise.reject(error)
+  }
+)
 
-export function $put(url, data) {
-  return request({
-    url: url,
-    method: "put",
-    data,
-  });
-}
+export default service

+ 30 - 23
src/views/bitcoin/CommonFunctionsPopup/CommonFunctionsPopup.vue

@@ -26,11 +26,7 @@
             @click="handleItemClick(item)"
           >
             <div class="icon-box">
-              <img
-                :src="getIconPath(item.icon)"
-                class="grid-icon-img"
-                loading="lazy"
-              />
+             <component :is="item.icon" class="grid-icon-svg" />
             </div>
             <span class="label">{{ item.name }}</span>
           </div>
@@ -52,6 +48,14 @@ import { ref, defineAsyncComponent } from 'vue';
 // 引入
 import TradeRules from  './GeneralLevel2/TradeRules.vue';
 import { useBodyScrollLock } from '@/composables/useBodyScrollLock' // 2. 引入 Hook
+import shezhi from '@/views/bitcoin/components/Icons/shezhi.vue'
+import jilu from '@/views/bitcoin/components/Icons/jilu.vue'
+import jisuan from '@/views/bitcoin/components/Icons/jisuan.vue'
+import feilv from '@/views/bitcoin/components/Icons/feilv.vue'
+import huazhuan from '@/views/bitcoin/components/Icons/huazhuan.vue'
+import jiaoyi from '@/views/bitcoin/components/Icons/jiaoyi.vue'
+import guizhe from '@/views/bitcoin/components/Icons/guizhe.vue'
+import yuyanqiehuan from '@/views/bitcoin/components/Icons/yuyanqiehuan.vue'
 
 // 控制显示的变量
 const showRules = ref(false);
@@ -98,22 +102,23 @@ const handleItemClick = (item) => {
 }
 
 // --- 图标加载 ---
-const getIconPath = (iconName) => {
-  try {
-    return require(`../../../assets/icon/bitcoin/${iconName}`)
-  } catch (e) {
-    return ''
-  }
-}
+// const getIconPath = (iconName) => {
+//   try {
+//     return require(`../../../assets/icon/bitcoin/${iconName}`)
+//   } catch (e) {
+//     return ''
+//   }
+// }
 
 const menuItems = [
-  { name: '交易设置', icon: 'shezhi.svg' },
-  { name: '交易记录', icon: 'jilu.svg' },
-  { name: '计算器', icon: 'jisuan.svg' },
-  { name: '费率', icon: 'feilv.svg' },
-  { name: '资金划转', icon: 'huazhuan.svg' },
-  { name: 'OTC交易', icon: 'jiaoyi.svg' },
-  { name: '交易规则', icon: 'guizhe.svg' },
+  { name: '交易设置', icon: shezhi },
+  { name: '交易记录', icon: jilu },
+  { name: '计算器', icon: jisuan },
+  { name: '费率', icon: feilv },
+  { name: '资金划转', icon: huazhuan },
+  { name: 'OTC交易', icon: jiaoyi },
+  { name: '交易规则', icon: guizhe },
+  {name: '语言切换', icon: yuyanqiehuan },
 ]
 </script>
 
@@ -210,13 +215,15 @@ const menuItems = [
   display: flex;
   align-items: center;
   justify-content: center;
-  margin-bottom: 8px;
+  margin-bottom: 6px;
 }
-.grid-icon-img {
+
+/* 【核心修改 4】:新增 SVG 样式控制 */
+.grid-icon-svg {
   width: 100%;
   height: 100%;
-  object-fit: contain;
-  display: block;
+  /* 如果你的 SVG 内部没有写死颜色,这里可以控制颜色 */
+   fill: #333;
 }
 .label {
   font-size: 12px;

+ 0 - 1001
src/views/bitcoin/Index.vue

@@ -1,1001 +0,0 @@
-<template>
-  <div class="market">
-    <div class="market-nav">
-      <div class="nav-left">
-        <div class="pf600 fs18 fc121212" @click="messageChange('selfSelected')">合约
-          <div class="active-line"></div>
-        </div>
-        <div class="sys-notifi pf600 fs14 fcA8A8A8" @click="messageChange('contract')">秒合约</div>
-        <div
-            class="sys-notifi pf600 fs14 fcA8A8A8"
-            @click="messageChange('secondContract')">
-          期权
-        </div>
-        <div
-            class="sys-notifi pf600 fs14 fcA8A8A8"
-            @click="messageChange('secondContract')">
-          杠杆
-        </div>
-      </div>
-
-    </div>
-    <div class="menu">
-      <div class="menu-left">
-        <img class="fc333333" src="../../assets/icon/bitcoin/menu.svg" alt="">
-        <div class="pf600 fs18 fc121212">BTCUSDT 永续</div>
-      </div>
-      <div class="menu-right fc333333">
-
-        <img src="../../assets/icon/bitcoin/jisuanqi.svg" alt="" @click="$router.push({name: 'calculator'})">
-        <img src="../../assets/icon/bitcoin/hangqing.svg" alt="">
-        <img src="../../assets/icon/bitcoin/den.svg" alt="" @click="$router.push({ name: 'BitcoinFunctions' })">
-      </div>
-    </div>
-    <div class="menu-bottom">
-      <div class="pf500 fs12 menu-leftb">
-        +2.18%
-      </div>
-      <div class="fc333333 fs12 pf400 menu-rightb">
-        <div>资金费率/倒计时</div>
-        <div>0.003%/1:57:32</div>
-      </div>
-    </div>
-    <div class="menu-content">
-      <!--      //左边-->
-      <div class="menu-content-l">
-        <div class="menu-content-lb pf400 fs14 fc666666">
-          <span>价格</span><span>数量(USDT)</span>
-        </div>
-        <div class="menu-content-lb1">
-          <div class="menu-content-lb1l pf400 fs14 fcFF7171">
-            <span class="">40,166.82</span>
-            <span>40,166.82</span>
-            <span>40,166.82</span>
-            <span>40,166.82</span>
-            <span>40,166.82</span>
-            <span>40,166.82</span>
-            <span>40,166.82</span>
-            <span>40,166.82</span>
-            <span>40,166.82</span>
-
-          </div>
-          <div class="menu-content-lb1r pf400 fs14 fc444444">
-            <span>37.80K</span>
-            <span>37.80K</span>
-            <span>37.80K</span>
-            <span>37.80K</span>
-            <span>37.80K</span>
-            <span>37.80K</span>
-            <span>37.80K</span>
-            <span>37.80K</span>
-            <span>37.80K</span>
-
-          </div>
-        </div>
-        <div class="menu-content-lb2">
-          <p class="pf600 fs16 fcDF384C">5,678.00</p>
-          <p class="fs12 fcA8A8A8 pf400">1,678.00</p>
-        </div>
-        <div class="menu-content-lb1">
-          <div class="menu-content-lb1l pf400 fs14 fcFF7171">
-            <span class="">40,166.82</span>
-            <span>40,166.82</span>
-            <span>40,166.82</span>
-            <span>40,166.82</span>
-            <span>40,166.82</span>
-            <span>40,166.82</span>
-            <span>40,166.82</span>
-            <span>40,166.82</span>
-            <span>40,166.82</span>
-
-          </div>
-          <div class="menu-content-lb1r pf400 fs14 fc444444">
-            <span>37.80K</span>
-            <span>37.80K</span>
-            <span>37.80K</span>
-            <span>37.80K</span>
-            <span>37.80K</span>
-            <span>37.80K</span>
-            <span>37.80K</span>
-            <span>37.80K</span>
-            <span>37.80K</span>
-
-          </div>
-        </div>
-        <div class="menu-content-lb1">
-          <div class="menu-content-lb1l pf400 fs12 fc333333" @click="isPickerVisible = true">
-            <span class="fs12">{{ displayLabel }}</span>
-            <img src="../../assets/icon/bitcoin/shendul.svg" alt="">
-
-          </div>
-          <div class="menu-content-lb1r pf400 fs14 fc444444">
-            <img src="../../assets/icon/bitcoin/shendur.svg" alt="">
-            <img v-if="isassetlessState" src="../../assets/icon/bitcoin/wuzichan.svg" alt="">
-          </div>
-        </div>
-
-      </div>
-      <assetlessStateData v-if="isassetlessState"></assetlessStateData>
-      <SellTradingStatusData v-if="isassetlessState"></SellTradingStatusData>
-      <!--      //右边-->
-      <div class="menu-content-r">
-        <div class="menu-content-rb pf400 fs14">
-          <div class="menu-content-rb1 fs14 fc333333" @click="showInfo = true">
-            <img src="../../assets/icon/bitcoin/quancang.svg" alt="">
-            <span>{{ selectedLabel1 }}</span>
-          </div>
-          <div @click="showModal1 = true" style="font-size: 12px; margin-right: 12px;">
-            <VanIcon :style="{ fontWeight: 'bold' }" :name="showModal1 ? 'arrow-up' : 'arrow-down'"/>
-          </div>
-
-
-        </div>
-        <div class="menu-content-rb pf400 fs14">
-          <div class="menu-content-rb1 fs14 fc333333" @click="showModal3 = true">
-            <img src="../../assets/icon/bitcoin/quancang.svg" alt="">
-            <span>{{ selectedLabel2 }}</span>
-          </div>
-          <div @click="showModal2 = true" style="font-size: 12px; margin-right: 12px;">
-            <VanIcon :style="{ fontWeight: 'bold' }" :name="showModal2 ? 'arrow-up' : 'arrow-down'"/>
-          </div>
-
-        </div>
-        <div class="menu-content-rb pf400 fs14">
-          <div class="menu-content-rb1 fs14 fc333333">
-            <span>125000</span>
-          </div>
-          <span>最优价</span>
-        </div>
-        <div class="menu-content-rb pf400 fs14 fc333333">≈25.2250 USDT</div>
-        <div class="menu-content-rb pf400 fs14">
-          <div class="menu-content-rb1 fs14 fc333333">
-            <span class="menu-content-rb1s fc999999">数量</span>
-          </div>
-          <span>USDT</span>
-        </div>
-        <div class="menu-content-rb pf400 fs14">
-          <div class="menu-content-rb1 fs14 fc333333">
-            <span class="menu-content-rb1s fc333333">可用</span>
-          </div>
-          <div class="menu-content-rb1">
-            <span>0</span>
-            <span>USDT</span>
-            <img class="fs16" src="../../assets/icon/bitcoin/qianbao1.svg" alt="">
-          </div>
-
-        </div>
-        <div class="menu-content-rb pf400 fs14">
-          <div class="menu-content-rb1 fs14 fc333333">
-            <span class="menu-content-rb1s fc333333">倍数</span>
-          </div>
-          <div class="menu-content-rb1" @click="showLeverageModal = true">
-            <span></span>
-            <span>{{ selectedLeverage }}X</span>
-            <span>更多</span>
-          </div>
-        </div>
-        <div class="menu-content-rb pf400 fs14">
-          <div class="menu-content-rb1 fs14 fc333333">
-            <van-checkbox
-                v-model="isEnabled"
-                shape="square"
-                checked-color="#DC4653"
-                icon-size="16px"
-            >
-            </van-checkbox>
-            <span class="menu-content-rb1s fc333333">只减仓</span>
-          </div>
-          <div class="menu-content-rb1" @click="showModal = true">
-            <span></span>
-            <span>{{ currentType }}</span>
-            <div style="font-size: 12px;">
-              <VanIcon :style="{ fontWeight: 'bold' }" :name="showModal ? 'arrow-up' : 'arrow-down'"/>
-            </div>
-            <!--            <img class="fs16" src="../../assets/icon/bitcoin/quangcang1.svg" alt="">-->
-          </div>
-        </div>
-        <TakeProfitsTopLoss v-show="!isEnabled"></TakeProfitsTopLoss>
-
-        <div class="menu-content-rb pf400 fs14">
-          <div class="menu-content-rb1 fs14 fc333333">
-            <span class="menu-content-rb1s fc333333">可用</span>
-          </div>
-          <div class="menu-content-rb1">
-            <span>0</span>
-            <span>USDT</span>
-          </div>
-        </div>
-        <div class="menu-content-rb pf400 fs14">
-          <div class="menu-content-rb1 fs14 fc333333">
-            <span class="menu-content-rb1s fc333333">保证金</span>
-          </div>
-          <div class="menu-content-rb1">
-            <span>0</span>
-            <span>USDT</span>
-          </div>
-        </div>
-        <div class="menu-content-rb pf400 fs14" @click="showConfirm = true">
-          <div class="pf400 fs16 fcFFFFFF">买入(做多)</div>
-        </div>
-        <div class="menu-content-rb pf400 fs14">
-          <div class="menu-content-rb1 fs14 fc333333">
-            <span class="menu-content-rb1s fc333333">可用</span>
-          </div>
-          <div class="menu-content-rb1">
-            <span>0</span>
-            <span>USDT</span>
-          </div>
-        </div>
-        <div class="menu-content-rb pf400 fs14">
-          <div class="menu-content-rb1 fs14 fc333333">
-            <span class="menu-content-rb1s fc333333">保证金</span>
-          </div>
-          <div class="menu-content-rb1">
-            <span>0</span>
-            <span>USDT</span>
-          </div>
-        </div>
-        <div class="menu-content-rb pf400 fs14">
-          <div class="pf400 fs16 fcFFFFFF" @click="showConfirm = true">卖出(做空)</div>
-        </div>
-      </div>
-    </div>
-    <!--    订单列表组件-->
-    <sellOrder></sellOrder>
-    <!--    //各种弹窗-->
-    <div v-if="isassetlessState">
-      <assetlessState></assetlessState>
-    </div>
-    <div>
-      <ChooseThisDepth
-          v-model:show="isPickerVisible"
-          v-model="currentDepth"
-      ></ChooseThisDepth>
-    </div>
-    <div>
-      <LeveragePopup
-          v-model:visible="showLeverageModal"
-          :initial-value="selectedLeverage"
-          @confirm="handleConfirm">
-      </LeveragePopup>
-    </div>
-    <div>
-      <OrderConfirmPopup
-          v-model:visible="showConfirm"
-          @confirm="onOrderConfirmed">
-      </OrderConfirmPopup>
-    </div>
-    <div>
-      <OrderTimeSheet
-          v-model:visible="showModal"
-          v-model="currentType"
-      ></OrderTimeSheet>
-    </div>
-    <div>
-      <MarginInfoSheet
-          v-model:visible="showInfo"
-      ></MarginInfoSheet>
-    </div>
-    <div>
-      <FundingOptions
-          v-model:visible="showModal1"
-          :selected-id="currentId1"
-          @confirm="handleConfirm1">
-      </FundingOptions>
-    </div>
-
-    <div>
-      <OrderType
-          v-model:visible="showModal2"
-          :selected-id="currentId2"
-          @confirm="handleConfirm2"
-      ></OrderType>
-    </div>
-
-    <div>
-      <LimitOrderModal
-          v-model:visible="showModal3"
-      ></LimitOrderModal>
-    </div>
-
-    <!--    <div>-->
-    <!--      <InsufficientBalance-->
-    <!--          v-model:visible="showRechargeModal"-->
-    <!--          @confirm="handleGoRecharge"-->
-    <!--      ></InsufficientBalance>-->
-    <!--    </div>-->
-  </div>
-<!--  <router-view v-slot="{ Component }">-->
-<!--    <keep-alive :include="['BitcoinFunctions', 'Calculator']">-->
-<!--      <component :is="Component" />-->
-<!--    </keep-alive>-->
-<!--  </router-view>-->
-  <router-view></router-view>
-</template>
-<script setup>
-        import {Checkbox as VanCheckbox, Icon as VanIcon} from 'vant';
-        import {computed, defineAsyncComponent, ref} from 'vue';
-        import { useRouter } from 'vue-router'
-
-        const router = useRouter()
-
-        // 懒加载多个组件
-        const priceLimit = defineAsyncComponent(() => import("./components/priceLimit.vue"));
-        const assetlessState = defineAsyncComponent(() => import("./components/assetlessState.vue"));
-        const assetlessStateData = defineAsyncComponent(() => import("./components/assetlessStateData.vue"));
-        //订单列表组件
-        const sellOrder = defineAsyncComponent(() => import('./components/sellOrder.vue'));
-        const SellTradingStatusData = defineAsyncComponent(() => import('./components/SellTradingStatusData.vue'));
-        const TakeProfitsTopLoss = defineAsyncComponent(() => import('./components/TakeProfitsTopLoss.vue'));
-        const ChooseThisDepth = defineAsyncComponent(() => import('./components/ChooseThisDepth.vue'));
-        //控制倍数组件
-        const LeveragePopup = defineAsyncComponent(() => import('./components/LeveragePopup.vue'));
-        const OrderConfirmPopup = defineAsyncComponent(() => import('./components/OrderConfirmPopup.vue'));
-        const OrderTimeSheet = defineAsyncComponent(() => import('./components/OrderTimeSheet.vue'));
-        //全仓逐仓组件
-        const MarginInfoSheet = defineAsyncComponent(() => import('./components/MarginInfoSheet.vue'));
-        const FundingOptions = defineAsyncComponent(() => import('./components/FundingOptions.vue'));
-        const OrderType = defineAsyncComponent(() => import('./components/OrderType.vue'));
-        //余额不足提示
-        // const InsufficientBalance = defineAsyncComponent(() => import('./StatusComponent/InsufficientBalance.vue'));
-        const LimitOrderModal = defineAsyncComponent(() => import('./components/LimitOrderModal.vue'));
-        /*常用功能*/
-        // 跳转到子路由
-        const openFunctions = () => {
-          router.push({ name: 'BitcoinFunctions' })
-        }
-
-       /*市价说明*/
-        const showModal3 = ref(false);
-
-        // //余额不足提示
-        //   // 控制弹窗显示的变量
-        // const showRechargeModal = ref(false);
-        //
-        //   // 处理点击“立即充币”后的逻辑
-        // const handleGoRecharge = () => {
-        //   // console.log('用户点击了立即充币,正在跳转充值页面...');
-        //   // 这里写你的跳转逻辑,例如: router.push('/recharge')
-        // };
-
-        /*订单类型选项*/
-        const showModal2 = ref(false);
-
-        // 定义状态用于存储
-        const currentId2 = ref(1); // 默认选中第一个
-        const selectedLabel2 = ref('市价'); // 默认值
-        const selectedUnit2 = ref('%');      // 默认值
-
-        /*回调函数:子组件选中后触发*/
-        const handleConfirm2 = (item) => {
-          // console.log('子组件返回的对象:', item);
-
-          // 保存ID用于下次打开时回显高亮
-          currentId2.value = item.id;
-          // const a = item.label.slice(-2)
-
-          // 核心需求:在这里将值“拆开”为两个字符串
-          selectedLabel2.value = item.label.slice(0, 2); // 字符串1: "涨跌幅"
-          selectedUnit2.value = item.unit;   // 字符串2: "%"
-        };
-
-        /* 全仓逐仓选项*/
-        const showModal1 = ref(false);
-
-        // 定义状态用于存储
-        const currentId1 = ref(1); // 默认选中第一个
-        const selectedLabel1 = ref('全仓'); // 默认值
-        const selectedUnit1 = ref('%');      // 默认值
-
-        // 回调函数:子组件选中后触发
-        const handleConfirm1 = (item) => {
-          // console.log('子组件返回的对象:', item);
-
-          // 保存ID用于下次打开时回显高亮
-          currentId1.value = item.id;
-          // const a = item.label.slice(-2)
-
-          // 核心需求:在这里将值“拆开”为两个字符串
-          selectedLabel1.value = item.label.slice(0, 2); // 字符串1: "涨跌幅"
-          selectedUnit1.value = item.unit;   // 字符串2: "%"
-        };
-
-       /* // 全仓逐仓说明*/
-        const showInfo = ref(false);
-
-        /*//GTC弹框*/
-        // 控制弹窗显示
-        const showModal = ref(false);
-
-        // 选中的值,默认为 GTC
-        const currentType = ref('GTC');
-
-
-       /* // --- 深度弹窗 ---*/
-        const isPickerVisible = ref(false); // 控制弹窗开关
-        const currentDepth = ref('depth1'); // 当前选中的深度
-        const depthMap = {
-          'depth1': '深度1',
-          'depth2': '深度2',
-          'depth3': '深度3',
-        };
-        const displayLabel = computed(() => depthMap[currentDepth.value] || '请选择');
-
-        /*控制止盈止损*/
-        const isEnabled = ref(false);
-
-        /*控制倍数*/
-        // 控制弹窗显示
-        const showLeverageModal = ref(false);
-        // 存储当前选中的倍数,默认 100
-        const selectedLeverage = ref(100);
-        // 处理子组件回传的确认事件
-        const handleConfirm = (value) => {
-          // console.log('用户选择了:', value);
-          selectedLeverage.value = value;
-          // 这里可以继续添加发送 API 请求的逻辑
-        };
-
-        /*做多买入*/
-        const showConfirm = ref(false);
-        const onOrderConfirmed = () => {
-          // console.log('订单已提交');
-          // 这里可以添加 Toast 提示
-        };
-
-
-</script>
-<style lang="less" scoped>
-
-/* 容器基础样式,基于 375px 设计稿 */
-:deep(.van-checkbox__icon--square .van-icon) {
-  /* 这里的 4px 是圆角大小,数值越大越圆 */
-  border-radius: 2px !important;
-}
-
-.market {
-  display: flex;
-  flex-direction: column;
-  justify-content: flex-start;
-  align-items: center;
-  margin-bottom: 100px;
-  width: 100%;
-  z-index: 1;
-
-  .market-nav {
-    display: flex;
-    flex-direction: row;
-    justify-content: space-between;
-    align-items: center;
-    margin-top: 21px;
-    width: 345px;
-    height: 24px;
-
-    .nav-left {
-      display: flex;
-      flex-direction: row;
-      justify-content: flex-start;
-      align-items: flex-end;
-      width: 349px;
-      height: 24px;
-
-      div:nth-child(1) {
-        position: relative;
-
-        .active-line {
-          position: absolute;
-          bottom: -6px;
-          left: 50%;
-          transform: translateX(-50%);
-          width: 20px;
-          height: 3px;
-          background-color: #323233;
-          border-radius: 2px;
-        }
-      }
-
-      .sys-notifi {
-        margin-left: 35px;
-      }
-    }
-
-    .nav-right {
-      width: 20px;
-      height: 20px;
-    }
-  }
-
-  .menu {
-    display: flex;
-    flex-direction: row;
-    justify-content: space-between;
-    align-items: center;
-    margin-top: 21px;
-    width: 345px;
-    height: 24px;
-
-    .menu-left {
-      display: flex;
-      flex-direction: row;
-      justify-content: flex-start;
-      align-items: center;
-      line-height: 0px;
-
-      img {
-        margin: 0px 10px 0 0;
-      }
-    }
-
-    .menu-right {
-      img {
-        margin-left: 14px;
-      }
-    }
-  }
-
-  .menu-bottom {
-    display: flex;
-    flex-direction: row;
-    justify-content: space-between;
-    align-items: center;
-    margin-top: 8px;
-    width: 345px;
-
-    .menu-leftb {
-      width: 61px;
-      height: 25px;
-      background-color: #45b26b;
-      border-radius: 5px;
-      color: #ffffff;
-      text-align: center;
-      line-height: 25px;
-    }
-
-    .menu-rightb {
-      text-align: right;
-    }
-  }
-
-  .menu-content {
-    width: 100%;
-    max-width: 345px;
-    display: flex;
-    flex-direction: row;
-    margin-top: 8px;
-
-    .menu-content-l {
-      flex: 1;
-
-      .menu-content-lb {
-        display: flex;
-        flex-direction: row;
-        justify-content: space-between;
-        padding-right: 14px;
-      }
-
-      .menu-content-lb1 {
-        margin-top: 11px;
-        display: flex;
-        flex-direction: row;
-        justify-content: space-between;
-        padding-right: 14px;
-
-        .menu-content-lb1l {
-          display: flex;
-          flex-direction: column;
-          line-height: 22px;
-        }
-
-        .menu-content-lb1r {
-          display: flex;
-          flex-direction: column;
-          line-height: 22px;
-        }
-      }
-
-      .menu-content-lb1:nth-child(4) {
-        .menu-content-lb1l {
-          color: #45b26b;
-        }
-      }
-
-      .menu-content-lb1:nth-child(5) {
-        .menu-content-lb1l {
-          width: 100%;
-          display: flex;
-          flex-direction: row;
-          justify-content: space-between;
-          align-items: center;
-          background-color: #f5f5f5;
-          border-radius: 6px;
-          height: 24px;
-          padding: 0 5px 0 13px;
-
-
-        }
-
-        .menu-content-lb1r {
-          margin-left: 15px;
-        }
-      }
-
-      .menu-content-lb2 {
-        margin-top: 8px;
-        line-height: 16px;
-        //span:nth-child(2){
-        //  //border-style: dashed;
-        //}
-      }
-    }
-
-    .menu-content-r {
-      flex: 1;
-      flex-basis: 63.5px;
-      max-width: 204.25px;
-
-      .menu-content-rb:nth-of-type(4) {
-        background-color: transparent;
-        height: 20px;
-
-      }
-
-      .menu-content-rb {
-        width: 100%;
-        display: flex;
-        justify-content: space-between;
-        align-items: center;
-        background-color: #f5f5f5;
-        border-radius: 6px;
-        margin-bottom: 8px;
-        height: 38px;
-
-        span {
-          padding-right: 12px;
-
-        }
-
-        img {
-          padding-right: 12px;
-          //height: 16px;
-          //width: 16px;
-        }
-
-        .menu-content-rb1 {
-          text-align: center;
-          margin-left: 12px;
-          display: flex;
-          align-content: center;
-          justify-content: center;
-          align-items: center;
-
-          img {
-            padding-right: 6px;
-
-          }
-
-        }
-      }
-
-      .menu-content-rb:nth-of-type(6) {
-        background-color: transparent;
-        height: 20px;
-
-        .menu-content-rb1 {
-          margin-left: 0;
-        }
-
-        .menu-content-rb1:nth-child(2) {
-          span:nth-child(1) {
-
-            font-size: 12px;
-          }
-
-          span:nth-child(2) {
-            margin: 0 9px 0 9px;
-            font-size: 12px;
-          }
-        }
-
-        span {
-          padding-right: 0;
-        }
-
-        img {
-          padding-right: 0;
-        }
-
-      }
-
-      .menu-content-rb:nth-of-type(7) {
-        background-color: transparent;
-        height: 20px;
-
-        .menu-content-rb1 {
-          margin-left: 0;
-        }
-
-        .menu-content-rb1:nth-child(2) {
-          span:nth-child(2) {
-            margin: 0 9px 0 9px;
-            font-size: 12px;
-          }
-
-          span:nth-child(3) {
-            color: #df384c;
-          }
-        }
-
-        span {
-          padding-right: 0;
-        }
-
-        img {
-          padding-right: 0;
-        }
-
-      }
-
-      .menu-content-rb:nth-of-type(8) {
-        background-color: transparent;
-        height: 20px;
-
-        .menu-content-rb1 {
-          display: flex;
-          align-items: center;
-          height: 24px;
-          margin-left: 0;
-          align-content: center;
-
-          input {
-            width: 16px;
-            height: 16px;
-            margin-right: 5px;
-          }
-        }
-
-        .menu-content-rb1:nth-child(2) {
-          span:nth-child(2) {
-            margin: 0 9px 0 9px;
-          }
-        }
-
-        .menu-content-rb1s {
-          margin-left: 6px;
-        }
-
-        span {
-          padding-right: 0;
-        }
-
-        img {
-          padding-right: 0;
-        }
-
-      }
-
-      .menu-content-rb:nth-of-type(9) {
-        background-color: transparent;
-        height: 20px;
-
-        .menu-content-rb1 {
-          display: flex;
-          align-items: center;
-          height: 24px;
-          margin-left: 0;
-          align-content: center;
-
-          input {
-            width: 16px;
-            height: 16px;
-            margin-right: 5px;
-          }
-
-          img {
-            padding-left: 5px;
-            font-size: 16px;
-            padding-top: 2px;
-          }
-        }
-
-        .menu-content-rb1:nth-child(2) {
-          span:nth-child(2) {
-            margin: 0 9px 0 9px;
-          }
-        }
-
-        span {
-          padding-right: 0;
-        }
-
-        img {
-          padding-right: 0;
-        }
-
-      }
-
-      .menu-content-rb:nth-of-type(10) {
-        background-color: transparent;
-        height: 20px;
-
-        .menu-content-rb1 {
-          display: flex;
-          align-items: center;
-          height: 24px;
-          margin-left: 0;
-          align-content: center;
-
-          input {
-            width: 16px;
-            height: 16px;
-            margin-right: 5px;
-          }
-
-          img {
-            padding-left: 5px;
-            font-size: 16px;
-            padding-top: 2px;
-          }
-        }
-
-        .menu-content-rb1:nth-child(2) {
-          span:nth-child(2) {
-            margin: 0 0px 0 5px;
-          }
-        }
-
-        span {
-          padding-right: 0;
-        }
-
-        img {
-          padding-right: 0;
-        }
-
-      }
-
-      .menu-content-rb:nth-of-type(11) {
-        background-color: transparent;
-        height: 20px;
-
-        .menu-content-rb1 {
-          display: flex;
-          align-items: center;
-          height: 24px;
-          margin-left: 0;
-          align-content: center;
-
-          input {
-            width: 16px;
-            height: 16px;
-            margin-right: 5px;
-          }
-
-          img {
-            padding-left: 5px;
-            font-size: 16px;
-            padding-top: 2px;
-          }
-        }
-
-        .menu-content-rb1:nth-child(2) {
-          span:nth-child(2) {
-            margin: 0 0px 0 5px;
-          }
-        }
-
-        span {
-          padding-right: 0;
-        }
-
-        img {
-          padding-right: 0;
-        }
-
-      }
-
-      .menu-content-rb:nth-of-type(12) {
-        background-color: #45b26b;
-
-        div {
-          margin: auto;
-        }
-
-      }
-
-      .menu-content-rb:nth-of-type(13) {
-        background-color: transparent;
-        height: 20px;
-
-        .menu-content-rb1 {
-          display: flex;
-          align-items: center;
-          height: 24px;
-          margin-left: 0;
-          align-content: center;
-
-          input {
-            width: 16px;
-            height: 16px;
-            margin-right: 5px;
-          }
-
-          img {
-            padding-left: 5px;
-            font-size: 16px;
-            padding-top: 2px;
-          }
-        }
-
-        .menu-content-rb1:nth-child(2) {
-          span:nth-child(2) {
-            margin: 0 0px 0 5px;
-          }
-        }
-
-        span {
-          padding-right: 0;
-        }
-
-        img {
-          padding-right: 0;
-        }
-
-      }
-
-      .menu-content-rb:nth-of-type(14) {
-        background-color: transparent;
-        height: 20px;
-
-        .menu-content-rb1 {
-          display: flex;
-          align-items: center;
-          height: 24px;
-          margin-left: 0;
-          align-content: center;
-
-          input {
-            width: 16px;
-            height: 16px;
-            margin-right: 5px;
-          }
-
-          img {
-            padding-left: 5px;
-            font-size: 16px;
-            padding-top: 2px;
-          }
-        }
-
-        .menu-content-rb1:nth-child(2) {
-          span:nth-child(2) {
-            margin: 0 0px 0 5px;
-          }
-        }
-
-        span {
-          padding-right: 0;
-        }
-
-        img {
-          padding-right: 0;
-        }
-
-      }
-
-      .menu-content-rb:nth-of-type(15) {
-        background-color: #df384c;
-
-        div {
-          margin: auto;
-        }
-
-      }
-
-      //.van-dropdown-menu__bar{
-      //  width: 214px;
-      //}
-
-    }
-  }
-}
-</style>

File diff suppressed because it is too large
+ 6 - 3
src/views/bitcoin/TradeFutures.vue


+ 1 - 1
src/views/bitcoin/TradeLayout.vue

@@ -88,7 +88,7 @@ const isCurrent = (name) => {
     justify-content: space-between;
     align-items: center;
     padding:0 15px;
-    width: 345px;
+    //width: 100px;
     height: 48px;
     position: fixed;
     top: 0;

File diff suppressed because it is too large
+ 11 - 0
src/views/bitcoin/components/Icons/feilv.vue


File diff suppressed because it is too large
+ 11 - 0
src/views/bitcoin/components/Icons/guizhe.vue


File diff suppressed because it is too large
+ 11 - 0
src/views/bitcoin/components/Icons/huazhuan.vue


File diff suppressed because it is too large
+ 11 - 0
src/views/bitcoin/components/Icons/jiaoyi.vue


File diff suppressed because it is too large
+ 11 - 0
src/views/bitcoin/components/Icons/jilu.vue


File diff suppressed because it is too large
+ 11 - 0
src/views/bitcoin/components/Icons/jisuan.vue


File diff suppressed because it is too large
+ 11 - 0
src/views/bitcoin/components/Icons/shezhi.vue


File diff suppressed because it is too large
+ 11 - 0
src/views/bitcoin/components/Icons/yuyanqiehuan.vue


+ 1 - 0
src/views/bitcoin/lever/TradeSeconds.vue

@@ -238,6 +238,7 @@ onUnmounted(() => {
   color: #333;
   /* 防止横向滚动条 */
   padding-top: 50px;
+  padding-bottom: 60px;
   overflow-x: hidden;
 }
 

+ 251 - 18
src/views/index/components/HotCoin.vue

@@ -36,34 +36,227 @@
         </div>
       </div>
     </div>
-    <div class="coin-body">
-      <div class="body-item" v-for="(item, index) in 5" :key="index">
-        <div class="item-left">
-          <div class="coin-img">
-            <img src="../../../assets/img/index/Frame 7.svg" alt="" />
-          </div>
-          <div class="coin-name">
-            <div class="upper-name pf500 fs14 fc2C3131">Ethereum</div>
-            <div class="letter-name pf400 fs10 fcA9A9A9">ETH</div>
-          </div>
-          <div class="coin-echars"></div>
-          <div class="coin-price">
-            <div class="upper-price pf500 fs14 fc2C3131">48.503.12</div>
-            <div class="letter-price pf400 fs10 fcA9A9A9">¥ 4250.00</div>
-          </div>
+    <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(`/marketDetails`)">
+        <div class="coin-img" >
+          <img :src="item.logo" alt="" />
+        </div>
+        <div class="coin-name">
+          <div class="upper-name pf500 fs14 fc2C3131">{{ formatSymbol(item.symbol) }}</div>
+          <div class="letter-name pf400 fs10 fcA9A9A9">{{ item.name }}</div>
+        </div>
+        <div class="coin-echars"></div>
+        <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>
-        <div class="item-right pf500 fs12 fcFFFFFF">+2.18%</div>
       </div>
+      <div class="item-right pf500 fs12 fcFFFFFF" :class="getChangeColor(item.change_rate)">{{ formatChange(item.change_rate) }}</div>
+    </div>
+<!--      <div class="body-item" v-for="(item, index) in 5" :key="index">-->
+<!--        <div class="item-left">-->
+<!--          <div class="coin-img">-->
+<!--            <img src="../../../assets/img/index/Frame 7.svg" alt="" />-->
+<!--          </div>-->
+<!--          <div class="coin-name">-->
+<!--            <div class="upper-name pf500 fs14 fc2C3131">Ethereum</div>-->
+<!--            <div class="letter-name pf400 fs10 fcA9A9A9">ETH</div>-->
+<!--          </div>-->
+<!--          <div class="coin-echars"></div>-->
+<!--          <div class="coin-price">-->
+<!--            <div class="upper-price pf500 fs14 fc2C3131">48.503.12</div>-->
+<!--            <div class="letter-price pf400 fs10 fcA9A9A9">¥ 4250.00</div>-->
+<!--          </div>-->
+<!--        </div>-->
+<!--        <div class="item-right pf500 fs12 fcFFFFFF">+2.18%</div>-->
+<!--      </div>-->
     </div>
   </div>
 </template>
-<script setup></script>
+<script setup>
+import { GetCoins } from '@/api/index'
+import { ref, onMounted, onUnmounted } from 'vue'
+ import { useRoute, useRouter } from "vue-router";
+
+const router = useRouter();
+
+const coinList = ref([])
+let socket = null
+let heartbeatTimer = null
+let reconnectTimer = null
+let isUnmounted = false
+
+// --- 辅助函数 ---
+const formatSymbol = (symbol) => symbol ? symbol.replace('USDT', '') : ''
+const formatPrice = (price) => price ? parseFloat(price).toFixed(2) : '0.00'
+
+// 格式化涨跌幅:+9.01%
+const formatChange = (val) => {
+  if (!val) return '+0.00%'
+  const num = parseFloat(val)
+  // 正数加 + 号,负数自带 - 号
+  return (num > 0 ? '+' : '') + num.toFixed(2) + '%'
+}
+
+// 获取颜色:涨绿跌红
+const getChangeColor = (val) => {
+  if (!val) return 'bg-gray' // 没有数据时显示灰色
+  return parseFloat(val) >= 0 ? 'bg-green' : 'bg-red'
+}
+
+// --- 1. WebSocket 核心逻辑 ---
+
+const initWebSocket = () => {
+  // 🔒【防死循环】清理旧连接
+  if (socket) {
+    socket.onclose = null
+    socket.close()
+    socket = null
+  }
+
+  if (coinList.value.length === 0) return
+
+  // 🔒【参数生成】
+  // 列表里是 BTCUSDT (大写) -> 转成 btcusdt (小写) -> 用 / 拼接
+  // 结果: "btcusdt/ethusdt/bnbusdt..."
+  const symbolsParam = coinList.value
+    .map(item => item.symbol.toLowerCase())
+    .join('/')
+
+  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'
+    ? '63.141.230.43:57676'
+    : 'localhost:8080' // 开发环境走代理
+    const wsUrl = `ws://${host}/ws/kline/${query}`
+
+  // console.log('🚀 开始连接:', wsUrl)
+
+  try {
+    socket = new WebSocket(wsUrl)
+  } catch (err) {
+    // console.error('WS 初始化失败:', err)
+    reconnect()
+    return
+  }
+
+  socket.onopen = () => {
+    // console.log('✅ 连接成功')
+    startHeartbeat()
+    if (reconnectTimer) clearTimeout(reconnectTimer)
+  }
+
+  socket.onmessage = (event) => {
+    if (event.data === 'pong' || event.data === 'ping') return
+    try {
+      const msg = JSON.parse(event.data)
+
+      // 兼容两种数据结构 (有时候后端会包一层 data)
+      if (msg.data) {
+        updateCoinData(msg.data)
+      } else {
+        updateCoinData(msg)
+      }
+    } catch (e) {}
+  }
+
+  socket.onerror = (err) => {
+    console.error('❌ WS 报错')
+  }
+
+  socket.onclose = (e) => {
+    // console.log(`⚠️ 断开 (Code: ${e.code})`)
+    // console.log('关闭原因:', e)
+    // console.log('是否正常关闭:', e.wasClean)
+    if (e.code === 1000) return
+    socket = null
+    reconnect()
+  }
+}
+
+// --- 2. 更新数据 (核心适配) ---
+const updateCoinData = (ticker) => {
+  // ticker 是 WS 推送的数据:
+  // { s: "ASTERUSDT", c: "1.016", P: "9.013", ... }
+
+  if (!ticker || !ticker.s) return
+
+  // 1. 找到列表里对应的币
+  // 列表里是 "BTCUSDT",WS 推送里 s 也是 "BTCUSDT"
+  // 统一转大写对比,确保匹配
+  const targetCoin = coinList.value.find(item =>
+    item.symbol.toUpperCase() === ticker.s.toUpperCase()
+  )
+
+  if (targetCoin) {
+    // 2. 更新价格 (c = current price)
+    if (ticker.c) targetCoin.price = ticker.c
+
+    // 3. 更新涨跌幅 (P = percentage change)
+    // 把 WS 里的 P 字段赋值给列表项的 change_rate
+    if (ticker.P) targetCoin.change_rate = ticker.P
+  }
+}
+
+// --- 3. 自动重连 ---
+const reconnect = () => {
+  if (isUnmounted) return
+  if (reconnectTimer) return
+  reconnectTimer = setTimeout(() => {
+    reconnectTimer = null
+    initWebSocket()
+  }, 3000)
+}
+
+// --- 4. 心跳保活 ---
+const startHeartbeat = () => {
+  if (heartbeatTimer) clearInterval(heartbeatTimer)
+  heartbeatTimer = setInterval(() => {
+    if (socket && socket.readyState === WebSocket.OPEN) {
+      socket.send('ping')
+    }
+  }, 10000)
+}
+
+// --- 生命周期 ---
+
+onMounted(async () => {
+  try {
+    // 1. 先拿列表 (只有价格,没有涨跌幅)
+    const res = await GetCoins()
+    coinList.value = Array.isArray(res) ? res : (res.data || [])
+
+    // 2. 再连 WS (获取实时数据)
+    if (coinList.value.length > 0) {
+      initWebSocket()
+    }
+  } catch (error) {
+    console.error('API 失败:', error)
+  }
+})
+
+onUnmounted(() => {
+  isUnmounted = true
+  if (heartbeatTimer) clearInterval(heartbeatTimer)
+  if (reconnectTimer) clearTimeout(reconnectTimer)
+  if (socket) {
+    socket.onclose = null
+    socket.close()
+    socket = null
+  }
+})
+</script>
 <style lang="less" scoped>
   .hot-coin {
     margin-top: 20px;
     width: 346px;
     height: 333px;
-
     .coin-title {
       height: 24px;
       line-height: 24px;
@@ -206,4 +399,44 @@
       }
     }
   }
+
+//
+//  //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-gray { background-color: #C0C0C0; }
 </style>

+ 1 - 0
src/views/index/components/HotFinancial.vue

@@ -32,6 +32,7 @@
   .hot-financial {
     margin-top: 30px;
     width: 100%;
+    padding-bottom: 80px;
 
     .financial-title {
       margin-left: 16px;

+ 22 - 0
vue.config.js

@@ -3,6 +3,28 @@ const { defineConfig } = require('@vue/cli-service')
 
 module.exports = defineConfig({
   transpileDependencies: true,
+  //跨域
+  // --- 核心配置开始 ---
+  devServer: {
+    proxy: {
+      '/api': {
+        // ⚠️【重要】这里必须改成你真实的后端地址!
+        // 如果后端在本地,可能是 http://localhost:8000
+        // 如果是线上测试服,可能是 http://47.100.xx.xx
+        target: 'http://63.141.230.43:57676',
+
+        changeOrigin: true, // 允许跨域
+
+      },
+      // 2.【新增】WebSocket 代理配置
+      '/ws/kline': {
+        target: 'http://63.141.230.43:57676', // 后端 IP
+        changeOrigin: true,
+        ws: true // ⚠️ 开启 WebSocket 支持
+        // 这里是否需要 pathRewrite 取决于后端路径有没有 /ws
+      }
+    }
+  },
 
   // 1. 基础路径 (解决白屏问题)
   publicPath: './',

Some files were not shown because too many files changed in this diff