ソースを参照

登录接口联调及问题修改

jhaoG 3 週間 前
コミット
560c4a1e9f
4 ファイル変更635 行追加302 行削除
  1. 21 22
      src/api/index.js
  2. 26 2
      src/views/index/ApplyPermission.vue
  3. 565 252
      src/views/market/details/MarketConditions.vue
  4. 23 26
      vue.config.js

+ 21 - 22
src/api/index.js

@@ -1,44 +1,43 @@
-import request from '@/utils/request'
+import request from "@/utils/request";
 
 // 登录接口
 export function login(data) {
   return request({
-    url: '/user/login',
-    method: 'post',
-    data
-  })
+    url: "/custom/login/",
+    method: "post",
+    data,
+  });
 }
 
 // 获取币币
 export function GetCoins() {
   return request({
-    url: '/finance/trading_pair/get_display_coin/',
-    method: 'get'
-  })
+    url: "/finance/trading_pair/get_display_coin/",
+    method: "get",
+  });
 }
 
 //获取K线
 export function GetCandlestickChart(id) {
   return request({
     url: `/finance/trading_pair/${id?.symbol}/get_kline/`,
-    method: 'get',
+    method: "get",
     params: {
-        interval: id.period,
-        limit: 150,
-        end_time: id.to || undefined,
-    }
-  })
+      interval: id.period,
+      limit: 150,
+      end_time: id.to || undefined,
+    },
+  });
 }
 
-
 //交易对
 export function TradingPair(id) {
-    return request({
+  return request({
     url: `/finance/trading_pair/`,
-    method: 'get',
+    method: "get",
     params: {
-        pageSize: id.pageSize,
-        pageNum: id.pageNum,
-    }
-  })
-}
+      pageSize: id.pageSize,
+      pageNum: id.pageNum,
+    },
+  });
+}

+ 26 - 2
src/views/index/ApplyPermission.vue

@@ -10,13 +10,15 @@
         <div>你确认将钱包地址公开给此网站吗?</div>
       </div>
       <div class="sure-btn pf600 fs14 fcFFFFFF" @click="sure">确认</div>
-      <div class="cancel-btn pf400 fs14 fcDF384C">取消</div>
+      <div class="cancel-btn pf400 fs14 fcDF384C" @click="cancel">取消</div>
     </div>
   </div>
 </template>
 <script setup>
-  import { l_setItem, l_removeItem } from "@/utils/storage";
+  import { l_setItem, l_getItem } from "@/utils/storage";
+  import { login } from "@/api/index";
   import { useRouter } from "vue-router";
+  import { onMounted } from "vue";
 
   const router = useRouter();
 
@@ -25,10 +27,32 @@
     const address = accounts[0];
 
     if (address) {
+      const params = {
+        address: address,
+      };
+      const data = await login(params);
+
+      l_setItem("token", data.accessToken);
       l_setItem("address", address);
       router.push("/");
     }
   };
+
+  const cancel = () => {
+    // 强制退出当前 dApp WebView
+    window.location.replace("about:blank");
+
+    // 备用关闭(部分钱包生效)
+    setTimeout(() => {
+      window.close();
+    }, 80);
+  };
+
+  onMounted(() => {
+    if (l_getItem("address")) {
+      router.push("/");
+    }
+  });
 </script>
 <style lang="less" scoped>
   .apply-permission {

+ 565 - 252
src/views/market/details/MarketConditions.vue

@@ -4,7 +4,9 @@
     <div class="market-price">
       <div class="price-left">
         <div class="left-price pf400 fs14 fc333333">实时价格</div>
-        <div class="left-number pf600 fs20 fc1F2937" :class="getPriceColor(marketInfo.change)">
+        <div
+          class="left-number pf600 fs20 fc1F2937"
+          :class="getPriceColor(marketInfo.change)">
           {{ formatNumber(marketInfo.price) }}
         </div>
         <div class="left-appro pf500 fs14 fcA8A8A8">
@@ -32,7 +34,9 @@
           </div>
           <div class="right-number-top-number">
             <div class="pf400 fs10 fcA8A8A8">24h 成交额</div>
-            <div class="pf400 fs10 fc2C3131">{{ abbreviateNumber(marketInfo.amount) }}</div>
+            <div class="pf400 fs10 fc2C3131">
+              {{ abbreviateNumber(marketInfo.amount) }}
+            </div>
           </div>
         </div>
       </div>
@@ -49,13 +53,20 @@
         {{ getTabLabel(tab) }}
       </div>
 
-      <div class="tab-item icon-btn" @click.stop="toggleMore" :style="isMoreActive ? { backgroundColor: '#F6465D', color: '#fff' } : {}">
-        <span :class="{ 'active-text': isMoreActive }">{{ isMoreActive ? getTabLabel(currentTab) : '更多' }}</span>
+      <div
+        class="tab-item icon-btn"
+        @click.stop="toggleMore"
+        :style="isMoreActive ? { backgroundColor: '#F6465D', color: '#fff' } : {}">
+        <span :class="{ 'active-text': isMoreActive }">{{
+          isMoreActive ? getTabLabel(currentTab) : "更多"
+        }}</span>
         <span class="triangle" :style="isMoreActive ? { color: '#fff' } : {}">◢</span>
         <div class="dropdown-menu" v-show="showMoreMenu">
           <div
-            v-for="mt in moreTabs" :key="mt"
-            class="drop-item" :class="{ active: currentTab === mt }"
+            v-for="mt in moreTabs"
+            :key="mt"
+            class="drop-item"
+            :class="{ active: currentTab === mt }"
             @click.stop="switchPeriod(mt)">
             {{ getTabLabel(mt) }}
           </div>
@@ -72,19 +83,36 @@
       <div class="panel-section">
         <div class="section-title">主图指标</div>
         <div class="btn-group">
-          <div v-for="m in ['MA', 'EMA', 'BOLL', 'SAR']" :key="m"
-               class="idx-btn" :class="{ active: mainIdx === m }"
-               @click="changeMain(m)">{{ m }}</div>
-          <div class="idx-btn" :class="{ active: mainIdx === 'Hide' }" @click="changeMain('Hide')">隐藏</div>
+          <div
+            v-for="m in ['MA', 'EMA', 'BOLL', 'SAR']"
+            :key="m"
+            class="idx-btn"
+            :class="{ active: mainIdx === m }"
+            @click="changeMain(m)">
+            {{ m }}
+          </div>
+          <div
+            class="idx-btn"
+            :class="{ active: mainIdx === 'Hide' }"
+            @click="changeMain('Hide')">
+            隐藏
+          </div>
         </div>
       </div>
       <div class="panel-section">
         <div class="section-title">副图指标 (可多选)</div>
         <div class="btn-group">
-          <div v-for="s in ['VOL', 'MACD', 'KDJ', 'RSI', 'WR', 'CCI', 'OBV']" :key="s"
-               class="idx-btn" :class="{ active: subIdx.includes(s) }"
-               @click="toggleSub(s)">{{ s }}</div>
-          <div class="idx-btn" :class="{ active: subIdx.length === 0 }" @click="clearSub">隐藏</div>
+          <div
+            v-for="s in ['VOL', 'MACD', 'KDJ', 'RSI', 'WR', 'CCI', 'OBV']"
+            :key="s"
+            class="idx-btn"
+            :class="{ active: subIdx.includes(s) }"
+            @click="toggleSub(s)">
+            {{ s }}
+          </div>
+          <div class="idx-btn" :class="{ active: subIdx.length === 0 }" @click="clearSub">
+            隐藏
+          </div>
         </div>
       </div>
     </div>
@@ -101,263 +129,548 @@
 
     <!-- 5. 底部原有的 委托挂单/最新成交 -->
     <div class="notifi-classifi">
-      <div class="pf600 fs14" :class="current === 'entrustingOrder' ? 'fc121212' : 'fcA8A8A8'" @click="messageChange('entrustingOrder')">委托挂单</div>
-      <div class="sys-notifi pf600 fs14" :class="current === 'latestTransactions' ? 'fc121212' : 'fcA8A8A8'" @click="messageChange('latestTransactions')">最新成交</div>
+      <div
+        class="pf600 fs14"
+        :class="current === 'entrustingOrder' ? 'fc121212' : 'fcA8A8A8'"
+        @click="messageChange('entrustingOrder')">
+        委托挂单
+      </div>
+      <div
+        class="sys-notifi pf600 fs14"
+        :class="current === 'latestTransactions' ? 'fc121212' : 'fcA8A8A8'"
+        @click="messageChange('latestTransactions')">
+        最新成交
+      </div>
     </div>
 
-    <component :is="currentComponent" :symbol-id="symbolId" :latestTransactionData="latestTransactionData" :orderPlacement="orderPlacement" />
+    <component
+      :is="currentComponent"
+      :symbol-id="symbolId"
+      :latestTransactionData="latestTransactionData"
+      :orderPlacement="orderPlacement" />
 
     <div
+      style="
+        box-shadow: 0 -0.05333rem 0.26667rem rgba(109 109 109 / 5%);
+        box-sizing: border-box;
+        margin-top: 20px;
+        width: 100%;
+        z-index: 99;
+        position: sticky;
+        bottom: 0;
+        background-color: #ffffff;
+        padding: 20px;
+      ">
+      <div
+        @click="router.push('/bitcoin/CryptocurrencyTrading')"
         style="
-    box-shadow: 0 -0.05333rem 0.26667rem rgba(109 109 109 / 5%);
-    box-sizing: border-box;margin-top: 20px;
-    width:100%;z-index: 99;position: sticky; bottom: 0;background-color: #FFFFFF;padding: 20px;">
-      <div @click="router.push('/bitcoin/CryptocurrencyTrading')"
-          style="font-size: 17px; margin:0 auto;border-radius:19px;color:#FFFFFF; text-align:center; width:80%; line-height: 38px; font-weight: 500; background-color: #F6465D; ">交易</div>
+          font-size: 17px;
+          margin: 0 auto;
+          border-radius: 19px;
+          color: #ffffff;
+          text-align: center;
+          width: 80%;
+          line-height: 38px;
+          font-weight: 500;
+          background-color: #f6465d;
+        ">
+        交易
+      </div>
     </div>
   </div>
 </template>
 
 <script setup>
-import { ref, computed, onMounted, onBeforeUnmount, watch } from "vue";
-import { useRoute,useRouter } from "vue-router";
-import { GetCandlestickChart } from "@/api/index.js";
-
-const router = useRouter(); // 【新增】 实例化路由
-
-// 引入组件
-import KLineChart from "@/views/bitcoin/lever/components/KLineChart.vue";
-import EntrustingOrder from "./EntrustingOrder.vue";
-import LatestTransactions from "./LatestTransactions.vue";
-
-const route = useRoute();
-const symbolId = computed(() => route.query.id || "6");
-
-// --- 周期 ---
-const currentTab = ref("1d");
-const visibleTabs = ["15m", "1h", "4h", "1d"];
-const moreTabs = ["1m", "5m", "30m", "1w", "1M"];
-const isMoreActive = computed(() => moreTabs.includes(currentTab.value));
-const getTabLabel = (t) => ({ '1m':'1分', '5m':'5分', '15m':'15分', '30m':'30分', '1h':'1小时', '4h':'4小时', '1d':'日线', '1w':'周线', '1M':'月线' }[t] || t);
-
-// --- 指标管理 ---
-const showMoreMenu = ref(false);
-const showIndicatorMenu = ref(false);
-const klineRef = ref(null);
-const mainIdx = ref('MA');
-const subIdx = ref(['VOL', 'MACD']);
-
-const changeMain = (name) => { mainIdx.value = name; if (klineRef.value) klineRef.value.setMainIndicator(name); };
-const toggleSub = (name) => {
-  const index = subIdx.value.indexOf(name);
-  if (index > -1) subIdx.value.splice(index, 1);
-  else {
-    if (subIdx.value.length >= 3) subIdx.value.shift();
-    subIdx.value.push(name);
-  }
-  if (klineRef.value) klineRef.value.setSubIndicators(subIdx.value);
-};
-const clearSub = () => { subIdx.value = []; if (klineRef.value) klineRef.value.setSubIndicators([]); };
-
-const toggleMore = () => { showIndicatorMenu.value = false; showMoreMenu.value = !showMoreMenu.value; };
-const toggleIndicators = () => { showMoreMenu.value = false; showIndicatorMenu.value = !showIndicatorMenu.value; };
-const closePopups = () => { showMoreMenu.value = false; showIndicatorMenu.value = false; };
-
-const switchPeriod = (period) => {
-  if (currentTab.value === period) return;
-  currentTab.value = period;
-  showMoreMenu.value = false;
-  kLineData.value = [];
-  getKlineData();
-};
-
-// --- 数据/WS ---
-const kLineData = ref([]);
-const socket = ref(null);
-const WS_BASE_URL = "ws://backend.66linknow.com/ws/kline/";
-const HEARTBEAT_INTERVAL = 15000;
-const RECONNECT_DELAY = 3000;
-let heartbeatTimer = null, reconnectTimer = null, isUnmounted = false;
-const isLoadingMore = ref(false);
-
-const marketInfo = ref({ price: "0.00", fiatPrice: "0.00", change: 0.0, high: "0.00", low: "0.00", vol: "0", amount: "0" });
-
-const orderPlacement = ref();
-const latestTransactionData = ref();
-const current = ref("entrustingOrder");
-const componentsMap = { entrustingOrder: EntrustingOrder, latestTransactions: LatestTransactions };
-const currentComponent = computed(() => componentsMap[current.value]);
-const messageChange = (key) => current.value = key;
-
-// --- 获取 K线数据 (核心) ---
-const getKlineData = async (endTime = null) => {
-  if (typeof GetCandlestickChart !== "function") return;
-
-  try {
-    const params = {
-      symbol: symbolId.value,
-      period: currentTab.value,
-      limit: 150,
-      to: endTime || undefined
-    };
-
-    const res = await GetCandlestickChart(params);
-
-    let rawList = Array.isArray(res) ? res : (res && res.data ? res.data : []);
-
-    if (rawList.length > 0) {
-      const formattedData = rawList.map((item) => ({
-        timestamp: Number(item[0]),
-        open: parseFloat(item[1]),
-        high: parseFloat(item[2]),
-        low: parseFloat(item[3]),
-        close: parseFloat(item[4]),
-        volume: parseFloat(item[5])
-      }));
-
-      // 保证按时间升序
-      formattedData.sort((a, b) => a.timestamp - b.timestamp);
-
-      if (endTime) {
-        // --- 加载历史模式 ---
-        if (klineRef.value) {
-          const hasMore = formattedData.length >= 150;
-          // 核心点:子组件 KLineChart 内部现在会自动处理 overlap
-          klineRef.value.applyHistoryData(formattedData, hasMore);
+  import { ref, computed, onMounted, onBeforeUnmount, watch } from "vue";
+  import { useRoute, useRouter } from "vue-router";
+  import { GetCandlestickChart } from "@/api/index.js";
+
+  const router = useRouter(); // 【新增】 实例化路由
+
+  // 引入组件
+  import KLineChart from "@/views/bitcoin/lever/components/KLineChart.vue";
+  import EntrustingOrder from "./EntrustingOrder.vue";
+  import LatestTransactions from "./LatestTransactions.vue";
+
+  const route = useRoute();
+  const symbolId = computed(() => route.query.id || "6");
+
+  // --- 周期 ---
+  const currentTab = ref("1d");
+  const visibleTabs = ["15m", "1h", "4h", "1d"];
+  const moreTabs = ["1m", "5m", "30m", "1w", "1M"];
+  const isMoreActive = computed(() => moreTabs.includes(currentTab.value));
+  const getTabLabel = (t) =>
+    ({
+      "1m": "1分",
+      "5m": "5分",
+      "15m": "15分",
+      "30m": "30分",
+      "1h": "1小时",
+      "4h": "4小时",
+      "1d": "日线",
+      "1w": "周线",
+      "1M": "月线",
+    }[t] || t);
+
+  // --- 指标管理 ---
+  const showMoreMenu = ref(false);
+  const showIndicatorMenu = ref(false);
+  const klineRef = ref(null);
+  const mainIdx = ref("MA");
+  const subIdx = ref(["VOL", "MACD"]);
+
+  const changeMain = (name) => {
+    mainIdx.value = name;
+    if (klineRef.value) klineRef.value.setMainIndicator(name);
+  };
+  const toggleSub = (name) => {
+    const index = subIdx.value.indexOf(name);
+    if (index > -1) subIdx.value.splice(index, 1);
+    else {
+      if (subIdx.value.length >= 3) subIdx.value.shift();
+      subIdx.value.push(name);
+    }
+    if (klineRef.value) klineRef.value.setSubIndicators(subIdx.value);
+  };
+  const clearSub = () => {
+    subIdx.value = [];
+    if (klineRef.value) klineRef.value.setSubIndicators([]);
+  };
+
+  const toggleMore = () => {
+    showIndicatorMenu.value = false;
+    showMoreMenu.value = !showMoreMenu.value;
+  };
+  const toggleIndicators = () => {
+    showMoreMenu.value = false;
+    showIndicatorMenu.value = !showIndicatorMenu.value;
+  };
+  const closePopups = () => {
+    showMoreMenu.value = false;
+    showIndicatorMenu.value = false;
+  };
+
+  const switchPeriod = (period) => {
+    if (currentTab.value === period) return;
+    currentTab.value = period;
+    showMoreMenu.value = false;
+    kLineData.value = [];
+    getKlineData();
+  };
+
+  // --- 数据/WS ---
+  const kLineData = ref([]);
+  const socket = ref(null);
+  const WS_BASE_URL = "ws://backend.66linknow.com/ws/kline/";
+  const HEARTBEAT_INTERVAL = 15000;
+  const RECONNECT_DELAY = 3000;
+  let heartbeatTimer = null,
+    reconnectTimer = null,
+    isUnmounted = false;
+  const isLoadingMore = ref(false);
+
+  const marketInfo = ref({
+    price: "0.00",
+    fiatPrice: "0.00",
+    change: 0.0,
+    high: "0.00",
+    low: "0.00",
+    vol: "0",
+    amount: "0",
+  });
+
+  const orderPlacement = ref();
+  const latestTransactionData = ref();
+  const current = ref("entrustingOrder");
+  const componentsMap = {
+    entrustingOrder: EntrustingOrder,
+    latestTransactions: LatestTransactions,
+  };
+  const currentComponent = computed(() => componentsMap[current.value]);
+  const messageChange = (key) => (current.value = key);
+
+  // --- 获取 K线数据 (核心) ---
+  const getKlineData = async (endTime = null) => {
+    if (typeof GetCandlestickChart !== "function") return;
+
+    try {
+      const params = {
+        symbol: symbolId.value,
+        period: currentTab.value,
+        limit: 150,
+        to: endTime || undefined,
+      };
+
+      const res = await GetCandlestickChart(params);
+
+      let rawList = Array.isArray(res) ? res : res && res.data ? res.data : [];
+
+      if (rawList.length > 0) {
+        const formattedData = rawList.map((item) => ({
+          timestamp: Number(item[0]),
+          open: parseFloat(item[1]),
+          high: parseFloat(item[2]),
+          low: parseFloat(item[3]),
+          close: parseFloat(item[4]),
+          volume: parseFloat(item[5]),
+        }));
+
+        // 保证按时间升序
+        formattedData.sort((a, b) => a.timestamp - b.timestamp);
+
+        if (endTime) {
+          // --- 加载历史模式 ---
+          if (klineRef.value) {
+            const hasMore = formattedData.length >= 150;
+            // 核心点:子组件 KLineChart 内部现在会自动处理 overlap
+            klineRef.value.applyHistoryData(formattedData, hasMore);
+          }
+        } else {
+          // --- 初始化/重置模式 ---
+          kLineData.value = formattedData;
+          if (formattedData.length > 0) {
+            const lastBar = formattedData[formattedData.length - 1];
+            marketInfo.value = {
+              ...marketInfo.value,
+              price: lastBar.close,
+              fiatPrice: lastBar.close,
+              high: lastBar.high,
+              low: lastBar.low,
+            };
+          }
         }
       } else {
-        // --- 初始化/重置模式 ---
-        kLineData.value = formattedData;
-        if (formattedData.length > 0) {
-           const lastBar = formattedData[formattedData.length - 1];
-           marketInfo.value = { ...marketInfo.value, price: lastBar.close, fiatPrice: lastBar.close, high: lastBar.high, low: lastBar.low };
+        // 没数据了
+        if (endTime && klineRef.value) {
+          klineRef.value.applyHistoryData([], false);
         }
       }
-    } else {
-      // 没数据了
+    } catch (error) {
+      console.error("API Error", error);
       if (endTime && klineRef.value) {
+        // 发生错误时也要告知 KLineChart 停止加载,否则 loading 动画会一直转
         klineRef.value.applyHistoryData([], false);
       }
+    } finally {
+      isLoadingMore.value = false;
     }
-  } catch (error) {
-    console.error("API Error", error);
-    if (endTime && klineRef.value) {
-      // 发生错误时也要告知 KLineChart 停止加载,否则 loading 动画会一直转
-      klineRef.value.applyHistoryData([], false);
+  };
+
+  const handleLoadMore = (timestamp) => {
+    if (isLoadingMore.value) return;
+    isLoadingMore.value = true;
+    getKlineData(timestamp);
+  };
+
+  // --- WebSocket ---
+  const connectWebSocket = () => {
+    closeWebSocket();
+    const symbolStr = String(route.query.type || "btcusdt").toLowerCase();
+    const url = `${WS_BASE_URL}?symbol=${symbolStr}`;
+    try {
+      socket.value = new WebSocket(url);
+      socket.value.onopen = () => startHeartbeat();
+      socket.value.onmessage = (event) => handleSocketMessage(event.data);
+      socket.value.onclose = () => {
+        stopHeartbeat();
+        if (!isUnmounted) {
+          clearTimeout(reconnectTimer);
+          reconnectTimer = setTimeout(() => connectWebSocket(), RECONNECT_DELAY);
+        }
+      };
+    } catch (e) {
+      if (!isUnmounted)
+        reconnectTimer = setTimeout(() => connectWebSocket(), RECONNECT_DELAY);
     }
-  } finally {
-    isLoadingMore.value = false;
-  }
-};
-
-const handleLoadMore = (timestamp) => {
-  if (isLoadingMore.value) return;
-  isLoadingMore.value = true;
-  getKlineData(timestamp);
-};
-
-// --- WebSocket ---
-const connectWebSocket = () => {
-  closeWebSocket();
-  const symbolStr = String(route.query.type || "btcusdt").toLowerCase();
-  const url = `${WS_BASE_URL}?symbol=${symbolStr}`;
-  try {
-    socket.value = new WebSocket(url);
-    socket.value.onopen = () => startHeartbeat();
-    socket.value.onmessage = (event) => handleSocketMessage(event.data);
-    socket.value.onclose = () => { stopHeartbeat(); if (!isUnmounted) { clearTimeout(reconnectTimer); reconnectTimer = setTimeout(() => connectWebSocket(), RECONNECT_DELAY); } };
-  } catch (e) { if (!isUnmounted) reconnectTimer = setTimeout(() => connectWebSocket(), RECONNECT_DELAY); }
-};
-
-const closeWebSocket = () => { if (socket.value) { socket.value.close(); socket.value = null; } stopHeartbeat(); clearTimeout(reconnectTimer); };
-const startHeartbeat = () => { stopHeartbeat(); heartbeatTimer = setInterval(() => { socket.value?.readyState === 1 && socket.value.send("ping"); }, HEARTBEAT_INTERVAL); };
-const stopHeartbeat = () => { clearInterval(heartbeatTimer); heartbeatTimer = null; };
-
-const handleSocketMessage = (msgStr) => {
-  try {
-    if (msgStr === "pong") return;
-    const rawData = JSON.parse(msgStr);
-    const msg = rawData.data || rawData;
-    if (msg.e === "kline") {
-      const k = msg.k;
-      if (k.i !== currentTab.value) return;
-      const newBar = { timestamp: Number(k.t), open: parseFloat(k.o), high: parseFloat(k.h), low: parseFloat(k.l), close: parseFloat(k.c), volume: parseFloat(k.v) };
-      updateKlineData(newBar);
-      marketInfo.value.price = newBar.close;
-      marketInfo.value.fiatPrice = newBar.close;
-    } else if (msg.e === "24hrTicker") {
-      marketInfo.value.change = parseFloat(msg.P).toFixed(2);
-      marketInfo.value.high = msg.h; marketInfo.value.low = msg.l;
-      marketInfo.value.vol = msg.v; marketInfo.value.amount = msg.q;
-    } else if (rawData.stream?.includes("@depth20")) orderPlacement.value = rawData.data;
-    else if (rawData.stream?.includes("@aggTrade")) latestTransactionData.value = rawData.data;
-  } catch (e) {}
-};
-
-const updateKlineData = (newBar) => {
-  if (!kLineData.value?.length) { kLineData.value = [newBar]; return; }
-  const lastIndex = kLineData.value.length - 1;
-  const lastBar = kLineData.value[lastIndex];
-  // 简单的 WebSocket 去重
-  if (newBar.timestamp === lastBar.timestamp) kLineData.value.splice(lastIndex, 1, newBar);
-  else if (newBar.timestamp > lastBar.timestamp) kLineData.value.push(newBar);
-};
-
-const handleVisibilityChange = () => {
-  if (document.visibilityState === "visible" && socket.value?.readyState !== 1)
-    connectWebSocket(); };
-onMounted(() => { isUnmounted = false;
-  getKlineData(); connectWebSocket();
-  document.addEventListener("visibilitychange", handleVisibilityChange); });
-onBeforeUnmount(() => { isUnmounted = true; closeWebSocket(); document.removeEventListener("visibilitychange", handleVisibilityChange); });
-watch(symbolId, () => { kLineData.value = []; getKlineData(); connectWebSocket(); });
-const formatNumber = (num) => num ? Number(num).toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 6 }) : "0.00";
-const abbreviateNumber = (v) => { if (!v) return "0.00"; let n = parseFloat(v);
-  if (n>=1e9) return (n/1e9).toFixed(2)+"B";
-  if (n>=1e6) return (n/1e6).toFixed(2)+"M";
-  if (n>=1e3) return (n/1e3).toFixed(2)+"K";
-  return n.toFixed(2); };
-const getPricePrecision = (p) => (p < 1 ? 6 : p < 10 ? 4 : 2);
-const getPriceColor = (c) => (c >= 0 ? "fc45B26B" : "fcF6465D");
-const getUpDownClass = (c) => (c >= 0 ? "fc45B26B" : "fcF6465D");
+  };
+
+  const closeWebSocket = () => {
+    if (socket.value) {
+      socket.value.close();
+      socket.value = null;
+    }
+    stopHeartbeat();
+    clearTimeout(reconnectTimer);
+  };
+  const startHeartbeat = () => {
+    stopHeartbeat();
+    heartbeatTimer = setInterval(() => {
+      socket.value?.readyState === 1 && socket.value.send("ping");
+    }, HEARTBEAT_INTERVAL);
+  };
+  const stopHeartbeat = () => {
+    clearInterval(heartbeatTimer);
+    heartbeatTimer = null;
+  };
+
+  const handleSocketMessage = (msgStr) => {
+    try {
+      if (msgStr === "pong") return;
+      const rawData = JSON.parse(msgStr);
+      const msg = rawData.data || rawData;
+      if (msg.e === "kline") {
+        const k = msg.k;
+        if (k.i !== currentTab.value) return;
+        const newBar = {
+          timestamp: Number(k.t),
+          open: parseFloat(k.o),
+          high: parseFloat(k.h),
+          low: parseFloat(k.l),
+          close: parseFloat(k.c),
+          volume: parseFloat(k.v),
+        };
+        updateKlineData(newBar);
+        marketInfo.value.price = newBar.close;
+        marketInfo.value.fiatPrice = newBar.close;
+      } else if (msg.e === "24hrTicker") {
+        marketInfo.value.change = parseFloat(msg.P).toFixed(2);
+        marketInfo.value.high = msg.h;
+        marketInfo.value.low = msg.l;
+        marketInfo.value.vol = msg.v;
+        marketInfo.value.amount = msg.q;
+      } else if (rawData.stream?.includes("@depth20"))
+        orderPlacement.value = rawData.data;
+      else if (rawData.stream?.includes("@aggTrade"))
+        latestTransactionData.value = rawData.data;
+    } catch (e) {}
+  };
+
+  const updateKlineData = (newBar) => {
+    if (!kLineData.value?.length) {
+      kLineData.value = [newBar];
+      return;
+    }
+    const lastIndex = kLineData.value.length - 1;
+    const lastBar = kLineData.value[lastIndex];
+    // 简单的 WebSocket 去重
+    if (newBar.timestamp === lastBar.timestamp)
+      kLineData.value.splice(lastIndex, 1, newBar);
+    else if (newBar.timestamp > lastBar.timestamp) kLineData.value.push(newBar);
+  };
+
+  const handleVisibilityChange = () => {
+    if (document.visibilityState === "visible" && socket.value?.readyState !== 1)
+      connectWebSocket();
+  };
+  onMounted(() => {
+    isUnmounted = false;
+    getKlineData();
+    connectWebSocket();
+    document.addEventListener("visibilitychange", handleVisibilityChange);
+  });
+  onBeforeUnmount(() => {
+    isUnmounted = true;
+    closeWebSocket();
+    document.removeEventListener("visibilitychange", handleVisibilityChange);
+  });
+  watch(symbolId, () => {
+    kLineData.value = [];
+    getKlineData();
+    connectWebSocket();
+  });
+  const formatNumber = (num) =>
+    num
+      ? Number(num).toLocaleString("en-US", {
+          minimumFractionDigits: 2,
+          maximumFractionDigits: 6,
+        })
+      : "0.00";
+  const abbreviateNumber = (v) => {
+    if (!v) return "0.00";
+    let n = parseFloat(v);
+    if (n >= 1e9) return (n / 1e9).toFixed(2) + "B";
+    if (n >= 1e6) return (n / 1e6).toFixed(2) + "M";
+    if (n >= 1e3) return (n / 1e3).toFixed(2) + "K";
+    return n.toFixed(2);
+  };
+  const getPricePrecision = (p) => (p < 1 ? 6 : p < 10 ? 4 : 2);
+  const getPriceColor = (c) => (c >= 0 ? "fc45B26B" : "fcF6465D");
+  const getUpDownClass = (c) => (c >= 0 ? "fc45B26B" : "fcF6465D");
 </script>
 
 <style lang="less" scoped>
-.fc45B26B { color: #2ebd85 !important; }
-.fcF6465D { color: #f6465d !important; }
-.fc1F2937 { color: #1f2937; }
-.time-tabs { display: flex; align-items: center; justify-content: space-between; width: 100%;
-  box-sizing: border-box; padding:5px 0 15px 15px; position: relative; z-index: 20; }
-.tab-item { font-size: 14px; color: #929aa5; padding: 4px 8px; border-radius: 4px; cursor: pointer;
-  font-weight: 500; display: flex; align-items: center; position: relative; }
-.tab-item.icon-btn { padding: 4px 4px; }
-.active-text { color: #fff; font-weight: 600; }
-.triangle { font-size: 8px; margin-left: 2px; transform: scale(0.9); }
-.tab-item img { display: block; height: 16px; width: auto; }
-.dropdown-menu { position: absolute; top: 30px; left: -20px; width: 80px; background: #fff;
-  box-shadow: 0 4px 12px rgba(0,0,0,0.15); border-radius: 6px; padding: 5px 0; z-index: 100;
-  display: flex; flex-direction: column; }
-.drop-item { padding: 8px 15px; font-size: 13px; color: #666; text-align: center; }
-.drop-item.active { color: #F6465D; background: #fff5f5; }
-.indicator-panel { position: absolute; top: 155px; left: 15px; right: 15px; background: #fff;
-  box-shadow: 0 4px 15px rgba(0,0,0,0.1); border-radius: 8px; padding: 15px; z-index: 99; }
-.panel-section { margin-bottom: 15px; }
-.section-title { font-size: 12px; color: #999; margin-bottom: 8px; }
-.btn-group { display: flex; flex-wrap: wrap; gap: 10px; }
-.idx-btn { padding: 4px 12px; border: 1px solid #eee; border-radius: 14px; font-size: 12px;
-  color: #666; cursor: pointer; }
-.idx-btn.active { border-color: #F6465D; color: #F6465D; background-color: #fff5f5; }
-.market-conditions { display: flex; flex-direction: column; align-items: center; width: 100%;
-  position: relative; min-height: 100vh; background: #fff; }
-.market-price { display: flex; justify-content: space-between; margin-top: 4px;
-  width: 100%; padding: 0 15px; box-sizing: border-box;
-  .price-left { display: flex;line-height: 20px; flex-direction: column; width: 144px;
-    .left-price { height: 18px; line-height: 18px; } .left-number { margin-top: 5px; }
-    .left-appro { margin-top: 3px; .appro { margin-left: 9px; } } }
-  .price-right { display: flex; flex-direction: column; height: 100%;
-    .right-number-top, .right-number-bottom { display: flex; justify-content: flex-end; width: 100%; height: 32px; .right-number-top-price, .right-number-top-number { margin-left: 10px; text-align: right; div { height: 16px; line-height: 16px; text-align: end; } } } } }
-.k-line-main { height: 50vh; min-height: 350px; width: 100%; padding: 0 15px; }
-.notifi-classifi { display: flex; align-items: flex-end; margin-top: 140px;
-  width: 100%; padding: 0 15px 0 15px; box-sizing: border-box; height: 24px;
-  border-bottom: 1px solid #f5f5f5; .sys-notifi { margin-left: 47px; } }
-</style>
+  .fc45B26B {
+    color: #2ebd85 !important;
+  }
+  .fcF6465D {
+    color: #f6465d !important;
+  }
+  .fc1F2937 {
+    color: #1f2937;
+  }
+  .time-tabs {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    width: 100%;
+    box-sizing: border-box;
+    padding: 5px 0 15px 15px;
+    position: relative;
+    z-index: 20;
+  }
+  .tab-item {
+    font-size: 14px;
+    color: #929aa5;
+    padding: 4px 8px;
+    border-radius: 4px;
+    cursor: pointer;
+    font-weight: 500;
+    display: flex;
+    align-items: center;
+    position: relative;
+  }
+  .tab-item.icon-btn {
+    padding: 4px 4px;
+  }
+  .active-text {
+    color: #fff;
+    font-weight: 600;
+  }
+  .triangle {
+    font-size: 8px;
+    margin-left: 2px;
+    transform: scale(0.9);
+  }
+  .tab-item img {
+    display: block;
+    height: 16px;
+    width: auto;
+  }
+  .dropdown-menu {
+    position: absolute;
+    top: 30px;
+    left: -20px;
+    width: 80px;
+    background: #fff;
+    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
+    border-radius: 6px;
+    padding: 5px 0;
+    z-index: 100;
+    display: flex;
+    flex-direction: column;
+  }
+  .drop-item {
+    padding: 8px 15px;
+    font-size: 13px;
+    color: #666;
+    text-align: center;
+  }
+  .drop-item.active {
+    color: #f6465d;
+    background: #fff5f5;
+  }
+  .indicator-panel {
+    position: absolute;
+    top: 155px;
+    left: 15px;
+    right: 15px;
+    background: #fff;
+    box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
+    border-radius: 8px;
+    padding: 15px;
+    z-index: 99;
+  }
+  .panel-section {
+    margin-bottom: 15px;
+  }
+  .section-title {
+    font-size: 12px;
+    color: #999;
+    margin-bottom: 8px;
+  }
+  .btn-group {
+    display: flex;
+    flex-wrap: wrap;
+    gap: 10px;
+  }
+  .idx-btn {
+    padding: 4px 12px;
+    border: 1px solid #eee;
+    border-radius: 14px;
+    font-size: 12px;
+    color: #666;
+    cursor: pointer;
+  }
+  .idx-btn.active {
+    border-color: #f6465d;
+    color: #f6465d;
+    background-color: #fff5f5;
+  }
+  .market-conditions {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    width: 100%;
+    position: relative;
+    min-height: 100vh;
+    background: #fff;
+  }
+  .market-price {
+    display: flex;
+    justify-content: space-between;
+    margin-top: 4px;
+    width: 100%;
+    padding: 0 15px;
+    box-sizing: border-box;
+    .price-left {
+      display: flex;
+      line-height: 20px;
+      flex-direction: column;
+      width: 144px;
+      .left-price {
+        height: 18px;
+        line-height: 18px;
+      }
+      .left-number {
+        margin-top: 5px;
+      }
+      .left-appro {
+        margin-top: 3px;
+        .appro {
+          margin-left: 9px;
+        }
+      }
+    }
+    .price-right {
+      display: flex;
+      flex-direction: column;
+      height: 100%;
+      .right-number-top,
+      .right-number-bottom {
+        display: flex;
+        justify-content: flex-end;
+        width: 100%;
+        height: 32px;
+        .right-number-top-price,
+        .right-number-top-number {
+          margin-left: 10px;
+          text-align: right;
+          div {
+            height: 16px;
+            line-height: 16px;
+            text-align: end;
+          }
+        }
+      }
+    }
+  }
+  .k-line-main {
+    height: 50vh;
+    min-height: 350px;
+    width: 100%;
+    padding: 0 15px;
+  }
+  .notifi-classifi {
+    display: flex;
+    align-items: flex-end;
+    margin-top: 140px;
+    width: 100%;
+    padding: 0 15px 0 15px;
+    box-sizing: border-box;
+    height: 24px;
+    .sys-notifi {
+      margin-left: 47px;
+    }
+  }
+</style>

+ 23 - 26
vue.config.js

@@ -1,5 +1,5 @@
 // vue.config.js
-const { defineConfig } = require('@vue/cli-service')
+const { defineConfig } = require("@vue/cli-service");
 
 module.exports = defineConfig({
   transpileDependencies: true,
@@ -7,73 +7,70 @@ module.exports = defineConfig({
   // --- 核心配置开始 ---
   devServer: {
     proxy: {
-      '/api': {
+      "/api": {
         // ⚠️【重要】这里必须改成你真实的后端地址!
         // 如果后端在本地,可能是 http://localhost:8000
         // 如果是线上测试服,可能是 http://47.100.xx.xx
-          //'http://63.141.230.43:57676',
-          // 'http://backend.66linknow.com'
-        target:'http://backend.66linknow.com', // ✅ 必须加上协议
+        //'http://63.141.230.43:57676',
+        // 'http://backend.66linknow.com'
+        target: "http://63.141.230.43:57676", // ✅ 必须加上协议
         changeOrigin: true, // 允许跨域
-
       },
       // 2.【新增】WebSocket 代理配置
-      '/ws/kline': {
-        target: 'ws://backend.66linknow.com', // 后端 IP
+      "/ws/kline": {
+        target: "ws://backend.66linknow.com", // 后端 IP
         changeOrigin: true,
-        ws: true // ⚠️ 开启 WebSocket 支持
+        ws: true, // ⚠️ 开启 WebSocket 支持
         // 这里是否需要 pathRewrite 取决于后端路径有没有 /ws
-      }
-    }
+      },
+    },
   },
 
   // 1. 基础路径 (解决白屏问题)
-  publicPath: './',
+  publicPath: "./",
 
   // 2. 关闭 SourceMap (最简单粗暴的减体积)
   // 生产环境不生成 .map 文件,体积能减少 50% 以上,且防源码泄露
   productionSourceMap: false,
 
   // 3. Webpack 核心优化 (代码分割)
-  configureWebpack: config => {
+  configureWebpack: (config) => {
     // 只有在打包生产环境 (npm run build) 时才运行优化
-    if (process.env.NODE_ENV === 'production') {
-
+    if (process.env.NODE_ENV === "production") {
       // 开启代码分割 (SplitChunks)
       config.optimization.splitChunks = {
-        chunks: 'all', // 对所有代码进行分割 (无论是异步还是同步)
+        chunks: "all", // 对所有代码进行分割 (无论是异步还是同步)
 
         // 缓存组配置:决定什么代码打包到什么文件里
         cacheGroups: {
           // 组1: 第三方库 (node_modules)
           // 作用:把所有 node_modules 里的东西拆出来
           libs: {
-            name: 'chunk-libs',
+            name: "chunk-libs",
             test: /[\\/]node_modules[\\/]/,
             priority: 10,
-            chunks: 'initial' // 只打包初始依赖
+            chunks: "initial", // 只打包初始依赖
           },
 
           // 组2: Vant UI 组件库 (单独拆分)
           // 作用:Vant 体积较大,单独打包有利于缓存
           vant: {
-            name: 'chunk-vant',
+            name: "chunk-vant",
             priority: 20, // 优先级更高,会先被提取
-            test: /[\\/]node_modules[\\/]_?vant(.*)/
+            test: /[\\/]node_modules[\\/]_?vant(.*)/,
           },
 
           // 组3: 公共代码
           // 作用:如果你的多个页面都引用了同一个组件,提取出来公用
           commons: {
-            name: 'chunk-commons',
+            name: "chunk-commons",
             test: /[\\/]src[\\/]components[\\/]/, // 匹配 src/components 下的组件
             minChunks: 2, // 只要被引用2次及以上就提取
             priority: 5,
-            reuseExistingChunk: true
-          }
-        }
+            reuseExistingChunk: true,
+          },
+        },
       };
     }
   },
-
-})
+});