Pārlūkot izejas kodu

Merge branch 'main' into jiahao

jhaoG 4 nedēļas atpakaļ
vecāks
revīzija
bc9d53a418

+ 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 = '/api'

+ 2 - 0
package-lock.json

@@ -8,9 +8,11 @@
       "name": "bit_wise_world",
       "version": "0.1.0",
       "dependencies": {
+        "amfe-flexible": "^2.2.1",
         "axios": "^1.13.2",
         "html2canvas": "^1.4.1",
         "klinecharts": "^8.6.3",
+        "postcss-pxtorem": "^6.1.0",
         "vant": "^4.9.21",
         "vue": "^3.2.13",
         "vue-i18n": "^12.0.0-alpha.3",

+ 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>

+ 30 - 0
src/api/index.js

@@ -0,0 +1,30 @@
+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'
+  })
+}
+
+//获取K线
+export function GetCandlestickChart(id) {
+  return request({
+    url: `/finance/trading_pair/${id.symbol}/get_kline/`,
+    method: 'get',
+    params: {
+        interval: id.period,
+        limit: 150
+    }
+  })
+}

+ 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>

+ 1 - 1
src/router/index.js

@@ -92,7 +92,7 @@ const routes = [
         path: "/bitcoin",
         component: TradeLayout, // 而是布局组件
         // 当访问 /bitcoin 时,自动重定向到 /bitcoin/contract
-        redirect: "/bitcoin/contract",
+        redirect: "/bitcoin/CryptocurrencyTrading",
         children: [
           // 1. 核心交易子路由
           {

+ 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>

Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 6 - 3
src/views/bitcoin/TradeFutures.vue


+ 11 - 10
src/views/bitcoin/TradeLayout.vue

@@ -3,7 +3,15 @@
     <div class="market-nav">
       <div class="nav-left">
         <div
-          class="nav-item pf600"
+          class="nav-item pf600 "
+          :class="isCurrent('Cryptocurrency') ? 'fs18 fc121212' : 'fs14 fcA8A8A8'"
+          @click="switchTab('Cryptocurrency')"
+        >
+          币币
+          <div v-if="isCurrent('Cryptocurrency')" class="active-line"></div>
+        </div>
+        <div
+          class="nav-item pf600 sys-notifi"
           :class="isCurrent('TradeContract') ? 'fs18 fc121212' : 'fs14 fcA8A8A8'"
           @click="switchTab('TradeContract')"
         >
@@ -29,14 +37,7 @@
           <div v-if="isCurrent('TradeOptions')" class="active-line"></div>
         </div>
 
-        <div
-          class="nav-item pf600 sys-notifi"
-          :class="isCurrent('Cryptocurrency') ? 'fs18 fc121212' : 'fs14 fcA8A8A8'"
-          @click="switchTab('Cryptocurrency')"
-        >
-          币币交易
-          <div v-if="isCurrent('Cryptocurrency')" class="active-line"></div>
-        </div>
+
       </div>
     </div>
 
@@ -88,7 +89,7 @@ const isCurrent = (name) => {
     justify-content: space-between;
     align-items: center;
     padding:0 15px;
-    width: 345px;
+    //width: 100px;
     height: 48px;
     position: fixed;
     top: 0;

Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 11 - 0
src/views/bitcoin/components/Icons/feilv.vue


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 11 - 0
src/views/bitcoin/components/Icons/guizhe.vue


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 11 - 0
src/views/bitcoin/components/Icons/huazhuan.vue


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 11 - 0
src/views/bitcoin/components/Icons/jiaoyi.vue


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 11 - 0
src/views/bitcoin/components/Icons/jilu.vue


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 11 - 0
src/views/bitcoin/components/Icons/jisuan.vue


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 11 - 0
src/views/bitcoin/components/Icons/shezhi.vue


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 11 - 0
src/views/bitcoin/components/Icons/yuyanqiehuan.vue


+ 47 - 165
src/views/bitcoin/lever/TradeSeconds.vue

@@ -23,25 +23,24 @@
       <div class="stat-grid">
         <div>
           <div class="stat-item">
-          <span class="label">24h 最高价</span>
-          <span class="value">78,776.76</span>
-        </div>
-        <div class="stat-item">
-          <span class="label">24h 成交量 (BTC)</span>
-          <span class="value">78,776.76</span>
-        </div>
+            <span class="label">24h 最高价</span>
+            <span class="value">78,776.76</span>
+          </div>
+          <div class="stat-item">
+            <span class="label">24h 成交量 (BTC)</span>
+            <span class="value">78,776.76</span>
+          </div>
         </div>
         <div>
           <div class="stat-item">
-          <span class="label">24h 最低价</span>
-          <span class="value">78,776.76</span>
-        </div>
-        <div class="stat-item">
-          <span class="label">24h 成交额 (BTC)</span>
-          <span class="value">78,776.76</span>
-        </div>
+            <span class="label">24h 最低价</span>
+            <span class="value">78,776.76</span>
+          </div>
+          <div class="stat-item">
+            <span class="label">24h 成交额 (BTC)</span>
+            <span class="value">78,776.76</span>
+          </div>
         </div>
-
       </div>
     </section>
 
@@ -61,32 +60,41 @@
       </div>
     </nav>
 
+    <!-- 这里的 chart-wrapper 负责控制 padding 和高度 -->
     <div class="chart-wrapper">
-      <div id="k-line-chart" class="kline-container"></div>
+      <!-- 引入封装好的组件,只需传数据和高度 -->
+      <KlineChart
+        :data="kLineData"
+        height="100%"
+      />
     </div>
+
     <ChooseThisCycle></ChooseThisCycle>
     <sellOrder></sellOrder>
-<!--    <MarketPriceAndPlan></MarketPriceAndPlan>-->
+    <!-- <MarketPriceAndPlan></MarketPriceAndPlan> -->
   </div>
 </template>
 
 <script setup>
-import { onMounted, onUnmounted, ref , watch} from 'vue'
-import { useRoute } from 'vue-router' // 1. 引入 useRoute
-// ✅ 使用 namespace 引入,防止报错
-import * as klinecharts from 'klinecharts'
-import MarketPriceAndPlan from'./components/MarketPriceAndPlan.vue'
-import ChooseThisCycle from'./components/ChooseThisCycle.vue'
-import sellOrder from '@/views/bitcoin/components/sellOrder.vue';
+import { onMounted, ref } from 'vue'
+import { useRoute } from 'vue-router'
+import MarketPriceAndPlan from './components/MarketPriceAndPlan.vue'
+import ChooseThisCycle from './components/ChooseThisCycle.vue'
+import sellOrder from '@/views/bitcoin/components/sellOrder.vue'
+
+// 1. 引入新封装的组件 (请确保路径正确)
+import KlineChart from '@/views/bitcoin/lever/components/KLineChart.vue'
+
 const route = useRoute()
 const symbolTitle = ref(route.params.id || 'BTCUSDT')
+const showFunctions = ref(false) // 补全原始模板中用到的变量
 
 // --- 状态管理 ---
 const currentTab = ref('24h')
 const tabs = ['1h', '6h', '24h', '1w', '1m']
-let chartInstance = null
+const kLineData = ref([]) // 响应式数据,传递给子组件
 
-// --- 数据生成逻辑 ---
+// --- 数据生成逻辑 (保持不变) ---
 const generateData = (baseVal = 3812.74) => {
   const data = []
   let baseTime = new Date().getTime()
@@ -109,120 +117,15 @@ const generateData = (baseVal = 3812.74) => {
 // --- 切换周期逻辑 ---
 const switchPeriod = (period) => {
   currentTab.value = period
-  if (chartInstance) {
-    // 模拟数据刷新
-    const randomStart = 3800 + Math.random() * 100
-    chartInstance.applyNewData(generateData(randomStart))
-  }
+  // 模拟数据刷新,更新 kLineData,子组件会自动监听并重绘
+  const randomStart = 3800 + Math.random() * 100
+  kLineData.value = generateData(randomStart)
 }
 
-// --- 初始化与配置 ---
+// --- 初始化 ---
 onMounted(() => {
-  const chartDOM = document.getElementById('k-line-chart')
-  if (!chartDOM) return
-
-  // 1. 初始化图表
-  chartInstance = klinecharts.init(chartDOM)
-  if (!chartInstance) return
-
-  // 2. 样式常量
-  const targetBlue = '#4A6EF5' // 截图中的蓝色虚线颜色
-  const gridColor = '#F2F4F6'  // 极淡的网格线
-  const textColor = '#929AA5'  // 灰色文字
-
-  // 3. 核心配置 (setStyleOptions)
-  chartInstance.setStyleOptions({
-    grid: {
-      show: true,
-      horizontal: {
-        show: true,
-        size: 1,
-        color: gridColor,
-        style: 'dash',
-        dashValue: [5, 5]
-      },
-      vertical: { show: false }
-    },
-    candle: {
-      type: 'candle_solid',
-      bar: {
-        upColor: '#2EBD85',
-        downColor: '#F6465D',
-        noChangeColor: '#2EBD85'
-      },
-      // ✅ 重点:蓝色价格指示线配置
-      priceMark: {
-        show: true,
-        high: { show: false },
-        low: { show: false },
-        last: {
-          show: true,
-          // 强制无论涨跌都显示蓝色
-          upColor: targetBlue,
-          downColor: targetBlue,
-          line: { show: true, style: 'dash', dashValue: [4, 3] },
-          text: {
-            show: true,
-            color: '#FFFFFF',
-            size: 11,
-            paddingLeft: 4,
-            paddingRight: 4,
-            borderRadius: 2
-          }
-        }
-      },
-      tooltip: {
-        // 只有按压时才显示十字光标
-        showRule: 'follow_cross',
-        showType: 'rect',
-        dataSource: 'none', // 隐藏浮层数据,保持清爽
-        crosshair: {
-          show: true,
-          horizontal: { line: { style: 'dash', color: textColor } },
-          vertical: { line: { style: 'dash', color: textColor } }
-        }
-      }
-    },
-    // ✅ 重点:技术指标(VOL)颜色
-    technicalIndicator: {
-      bar: {
-        upColor: '#2EBD85',
-        downColor: '#F6465D',
-        noChangeColor: '#2EBD85'
-      }
-    },
-    // ✅ 重点:隐藏坐标轴线,只保留文字
-    xAxis: {
-      axisLine: { show: false },
-      tickLine: { show: false },
-      tickText: { color: textColor, size: 10, paddingTop: 8 }
-    },
-    yAxis: {
-      type: 'normal',
-      position: 'right',
-      inside: true,
-      axisLine: { show: false },
-      tickLine: { show: false },
-      tickText: { color: textColor, size: 10, paddingLeft: 8 }
-    },
-    separator: { size: 0 } // 去掉指标和K线之间的分割线
-  })
-
-  // 4. 创建副图指标 (VOL)
-  chartInstance.createTechnicalIndicator('VOL', false, { id: 'pane_1', heightRatio: 0.2 })
-
-  // 5. 加载数据并设置缩放
-  chartInstance.applyNewData(generateData())
-  if (chartInstance.setDataSpace) {
-  chartInstance.setDataSpace(7)
-}
-  // chartInstance.setBarSpace(7) // 设置蜡烛宽度
-})
-
-onUnmounted(() => {
-  if (chartInstance) {
-    klinecharts.dispose('k-line-chart')
-  }
+  // 页面加载时生成初始数据
+  kLineData.value = generateData()
 })
 </script>
 
@@ -233,24 +136,20 @@ onUnmounted(() => {
   max-width: 375px;
   margin: 0 auto;
   background-color: #fff;
-  /* 使用系统字体,还原原生质感 */
   font-family: -apple-system, BlinkMacSystemFont, "Helvetica Neue", Roboto, Arial, sans-serif;
   color: #333;
-  /* 防止横向滚动条 */
   padding-top: 50px;
+  padding-bottom: 60px;
   overflow-x: hidden;
 }
 
-/* 🔥🔥 核心修改:定义一个公共的左右内边距
-   给 header, market-info, time-tabs, chart-wrapper 都加上
-*/
+/* 公共内边距 */
 .header,
 .market-info,
 .time-tabs,
 .chart-wrapper {
   padding-left: 15px;
   padding-right: 15px;
-  /* 关键:确保 padding 不会撑大 width: 100% */
   box-sizing: border-box;
 }
 
@@ -259,7 +158,6 @@ onUnmounted(() => {
   display: flex;
   justify-content: space-between;
   align-items: center;
- /* padding-top: 12px;*/
   padding-bottom: 12px;
 }
 .title {
@@ -268,10 +166,6 @@ onUnmounted(() => {
   display: inline-block;
   margin-left: 10px;
 }
-.icon-menu, .icon-more {
-  font-size: 18px;
-  cursor: pointer;
-}
 
 /* --- 2. 行情信息 --- */
 .market-info {
@@ -285,7 +179,6 @@ onUnmounted(() => {
   font-weight: 600;
   line-height: 1.2;
   color: #111;
-  /* 稍微调整一下字间距,防止数字太紧 */
   letter-spacing: -0.5px;
 }
 .sub-info {
@@ -300,47 +193,41 @@ onUnmounted(() => {
 .sub-info .percent.up {
   color: #2EBD85;
 }
+
 /* 统计网格 */
 .stat-grid {
   display: flex;
   flex-direction: row;
   text-align: right;
   gap: 5px;
-
 }
 .stat-item {
   display: flex;
   flex-direction: column;
-
 }
 .stat-item .label {
   font-size: 11px;
   color: #999;
-  /* 稍微缩小字体 */
   transform: scale(0.9);
   transform-origin: right bottom;
-  white-space: nowrap; /* 防止文字换行 */
+  white-space: nowrap;
 }
 .stat-item .value {
   font-size: 11px;
   color: #333;
-  font-family: "DIN", -apple-system, sans-serif; /* 如果有数字字体更好 */
+  font-family: "DIN", -apple-system, sans-serif;
 }
 
 /* --- 3. Tabs --- */
 .time-tabs {
   display: flex;
   align-items: center;
-  /*gap: 5px;*/
   justify-content: space-between;
-  /*//padding-top: 8px;*/
-  /*padding-bottom: 8px;*/
-  /* 你的截图中似乎没有底边框,这里保持干净 */
 }
 .tab-item {
   font-size: 14px;
   color: #929AA5;
-  padding: 4px 10px; /* 稍微减小内边距,适应 15px 的两侧挤压 */
+  padding: 4px 10px;
   border-radius: 6px;
   cursor: pointer;
   font-weight: 500;
@@ -359,14 +246,9 @@ onUnmounted(() => {
 }
 
 /* --- 4. 图表容器 --- */
+/* 父组件控制高度和边距,子组件填充 100% */
 .chart-wrapper {
   width: 100%;
   height: 275px;
-  /*margin-top: 5px;*/
-  /* 🔥 这里加了 padding 后,K线图就会自动往里缩,左右留出白边 */
-}
-.kline-container {
-  width: 100%;
-  height: 100%;
 }
 </style>

+ 106 - 99
src/views/bitcoin/lever/components/KLineChart.vue

@@ -1,126 +1,133 @@
 <template>
-  <div class="kline-wrapper">
-    <!-- 时间周期 -->
-    <div class="chart-header">
-      <div class="time-intervals">
-        <span class="active">15分</span>
-        <span>1小时</span>
-        <span>4小时</span>
-        <span>日线</span>
-      </div>
-    </div>
-
-    <div ref="chartContainer" class="chart-container"></div>
-
-    <!-- 指标切换 -->
-    <div class="chart-footer">
-      <span class="indicator-btn" @click="setMainIndicator('MA')">MA</span>
-      <span class="indicator-btn" @click="setMainIndicator('BOLL')">BOLL</span>
-      <span class="divider">|</span>
-      <span class="indicator-btn" @click="setSubIndicator('VOL')">VOL</span>
-      <span class="indicator-btn" @click="setSubIndicator('MACD')">MACD</span>
-      <span class="indicator-btn" @click="setSubIndicator('KDJ')">KDJ</span>
-    </div>
+  <div class="kline-wrapper" :style="{ height: height }">
+    <div ref="chartContainer" class="kline-chart"></div>
   </div>
 </template>
 
 <script setup>
-import { onMounted, onUnmounted, ref } from 'vue'
-import { init, dispose } from 'klinecharts'
+import { onMounted, onBeforeUnmount, ref, watch, nextTick, toRaw } from 'vue'
+import * as klinecharts from 'klinecharts'
+
+const props = defineProps({
+  data: { type: Array, default: () => [] },
+  height: { type: String, default: '100%' },
+  precision: { type: Object, default: () => ({ price: 2, volume: 2 }) },
+  colors: {
+    type: Object,
+    default: () => ({
+      up: '#2EBD85',
+      down: '#F6465D',
+      grid: '#F2F4F6',
+      text: '#929AA5',
+      targetLine: '#4A6EF5'
+    })
+  }
+})
 
 const chartContainer = ref(null)
-let chartObj = null
-
-// 生成 mock 数据
-function getMockDataList() {
-  const dataList = []
-  let ts = Date.now()
-  let price = 40000
-
-  for (let i = 0; i < 500; i++) {
-    const timestamp = ts - (500 - i) * 60 * 1000 * 15
-    const random = (Math.random() - 0.5) * 200
-    const open = price
-    const close = price + random
-    const high = Math.max(open, close) + Math.random() * 50
-    const low = Math.min(open, close) - Math.random() * 50
-    const volume = Math.random() * 1000 + 500
-
-    dataList.push({ timestamp, open, high, low, close, volume })
-    price = close
+let chartInstance = null
+
+const initChart = () => {
+  if (!chartContainer.value) return
+  if (chartInstance) klinecharts.dispose(chartContainer.value)
+
+  chartInstance = klinecharts.init(chartContainer.value)
+
+  // 设置精度
+  const { price, volume } = props.precision
+  if (chartInstance.setPriceVolumePrecision) {
+    chartInstance.setPriceVolumePrecision(price, volume)
+  } else if (chartInstance.setPrecision) {
+    chartInstance.setPrecision(price, volume)
   }
 
-  return dataList
+  // 样式配置
+  const { up, down, grid, text, targetLine } = props.colors
+  chartInstance.setStyleOptions({
+    grid: { show: true, horizontal: { show: true, size: 1, color: grid, style: 'dash', dashValue: [5, 5] }, vertical: { show: false } },
+    candle: {
+      type: 'candle_solid',
+      bar: { upColor: up, downColor: down, noChangeColor: up },
+      priceMark: {
+        show: true,
+        last: { show: true, upColor: up, downColor: down, line: { show: true, style: 'dash' }, text: { show: true, color: '#FFF', paddingLeft: 4, paddingRight: 4, borderRadius: 2 } }
+      },
+      tooltip: { showRule: 'follow_cross', showType: 'rect', dataSource: 'none' }
+    },
+    xAxis: { axisLine: { show: false }, tickLine: { show: false }, tickText: { color: text, size: 10, paddingTop: 8 } },
+    yAxis: { inside: true, axisLine: { show: false }, tickLine: { show: false }, tickText: { color: text, size: 10, paddingLeft: 8 } },
+  })
+
+  chartInstance.createTechnicalIndicator('VOL', false, { id: 'pane_1', heightRatio: 0.2 })
+
+  // 初始加载
+  if (props.data && props.data.length > 0) {
+    chartInstance.applyNewData(toRaw(props.data))
+  }
 }
 
-onMounted(() => {
-  if (!chartContainer.value) {
-    console.error('找不到图表容器')
+// --- 🔥 核心修复:智能判断是“更新”还是“重置” ---
+watch(() => props.data, (newData) => {
+  if (!chartInstance) return
+
+  const rawData = toRaw(newData)
+  const currentList = chartInstance.getDataList()
+
+  // 1. 如果新数据为空,清空图表
+  if (rawData.length === 0) {
+    chartInstance.clearData()
     return
   }
 
-  // 初始化 v8 图表
-  chartObj = init(chartContainer.value)
-
-  console.log('chartObj 实例:', chartObj)
+  // 2. 如果当前图表为空,直接加载
+  if (currentList.length === 0) {
+    chartInstance.applyNewData(rawData)
+    return
+  }
 
-  if (!chartObj) {
-    console.error('图表初始化失败')
+  // 3. 🔥 关键判断:
+  // 如果第一根 K 线的时间戳变了,说明切换了周期或币种 -> 全量重置
+  const firstOld = currentList[0]
+  const firstNew = rawData[0]
+  if (firstOld.timestamp !== firstNew.timestamp) {
+    chartInstance.applyNewData(rawData)
     return
   }
 
-  // 设置主图 & 副图指标(v8 用 createIndicator)
-  chartObj.createIndicator('MA', false, { id: 'candle_pane' })
-  chartObj.createIndicator('VOL', false, { id: 'pane_1' })
+  // 4. 如果第一根时间没变,说明是实时跳动或追加 -> 增量更新
+  if (rawData.length > 0) {
+    const lastData = rawData[rawData.length - 1]
+    chartInstance.updateData(lastData)
+  }
+}, { deep: true })
 
-  // v8 的方法是 applyNewData(不是 applyData)
-  const data = getMockDataList()
-  chartObj.applyNewData(data)
-})
+// 监听精度
+watch(() => props.precision, (val) => {
+  if (chartInstance) {
+    if (chartInstance.setPriceVolumePrecision) chartInstance.setPriceVolumePrecision(val.price, val.volume)
+    else if (chartInstance.setPrecision) chartInstance.setPrecision(val.price, val.volume)
+  }
+}, { deep: true })
 
-onUnmounted(() => {
-  if (chartObj) dispose(chartContainer.value)
+onMounted(() => {
+  nextTick(() => initChart())
+  window.addEventListener('resize', handleResize)
 })
 
-// 切换主图指标
-function setMainIndicator(name) {
-  chartObj.createIndicator(name, false, { id: 'candle_pane' })
-}
+onBeforeUnmount(() => {
+  window.removeEventListener('resize', handleResize)
+  if (chartInstance) {
+    klinecharts.dispose(chartContainer.value)
+    chartInstance = null
+  }
+})
 
-// 切换副图指标
-function setSubIndicator(name) {
-  chartObj.createIndicator(name, false, { id: 'pane_1' })
+const handleResize = () => {
+  if (chartInstance) chartInstance.resize()
 }
 </script>
 
 <style scoped>
-.kline-wrapper {
-  display: flex;
-  flex-direction: column;
-  width: 100%;
-}
-
-.chart-header, .chart-footer {
-  display: flex;
-  padding: 10px 15px;
-  background: #fff;
-}
-
-.time-intervals span, .indicator-btn {
-  font-size: 12px;
-  color: #666;
-  margin-right: 15px;
-  cursor: pointer;
-}
-
-.time-intervals span.active {
-  font-weight: bold;
-  color: #333;
-}
-
-.chart-container {
-  width: 100%;
-  height: 450px;
-  background: #fff;
-}
-</style>
+.kline-wrapper { width: 100%; position: relative; }
+.kline-chart { width: 100%; height: 100%; }
+</style>

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

@@ -36,34 +36,210 @@
         </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({ path: '/marketDetails', query: { id: item.id,type: item.symbol.toLowerCase()} })">
+        <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>
   </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'
+    ? 'backend.66linknow.com'
+    : '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 +382,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/market/details/EntrustingOrder.vue

@@ -29,6 +29,7 @@
 <style lang="less" scoped>
   .entrusting-order {
     width: 100%;
+    padding: 0 15px;
 
     .order-header {
       display: flex;

+ 459 - 145
src/views/market/details/MarketConditions.vue

@@ -4,209 +4,523 @@
       <div class="price-left">
         <div class="left-price pf400 fs14 fc333333">
           实时价格
-          <img src="../../../assets/icon/market/bottom-arrow.svg" alt="" />
         </div>
-        <div class="left-number pf600 fs20 fc1F2937">1,125,158.00</div>
+        <div class="left-number pf600 fs20 fc1F2937" :class="getPriceColor(marketInfo.change)">
+          {{ formatNumber(marketInfo.price) }}
+        </div>
         <div class="left-appro pf500 fs14 fcA8A8A8">
-          ≈35,458.00
-          <div class="appro pf500 fs14 fc45B26B">+1.42%</div>
+          ≈{{ formatNumber(marketInfo.fiatPrice) }}
+          <span class="appro pf500 fs14" :class="getUpDownClass(marketInfo.change)">
+            {{ marketInfo.change > 0 ? '+' : '' }}{{ marketInfo.change }}%
+          </span>
         </div>
       </div>
+
       <div class="price-right">
         <div class="right-number-top">
           <div class="right-number-top-price">
             <div class="pf400 fs10 fcA8A8A8">24h 最高价</div>
-            <div class="pf400 fs10 fc2C3131">78,776.76</div>
+            <div class="pf400 fs10 fc2C3131">{{ formatNumber(marketInfo.high) }}</div>
           </div>
           <div class="right-number-top-number">
-            <div class="pf400 fs10 fcA8A8A8">24h 成交量 (BTC)</div>
-            <div class="pf400 fs10 fc2C3131">78,776.76</div>
+            <div class="pf400 fs10 fcA8A8A8">24h 成交量</div>
+            <div class="pf400 fs10 fc2C3131">{{ abbreviateNumber(marketInfo.vol) }}</div>
           </div>
         </div>
         <div class="right-number-bottom">
           <div class="right-number-top-price">
-            <div class="pf400 fs10 fcA8A8A8">24h 最价</div>
-            <div class="pf400 fs10 fc2C3131">78,776.76</div>
+            <div class="pf400 fs10 fcA8A8A8">24h 最价</div>
+            <div class="pf400 fs10 fc2C3131">{{ formatNumber(marketInfo.low) }}</div>
           </div>
           <div class="right-number-top-number">
-            <div class="pf400 fs10 fcA8A8A8">24h 成交量 (BTC)</div>
-            <div class="pf400 fs10 fc2C3131">78,776.76</div>
+            <div class="pf400 fs10 fcA8A8A8">24h 成交</div>
+            <div class="pf400 fs10 fc2C3131">{{ abbreviateNumber(marketInfo.amount) }}</div>
           </div>
         </div>
       </div>
     </div>
-    <div class="k-line-main"></div>
-    <div class="notifi-classifi">
-      <div class="pf600 fs14 fc121212" @click="messageChange('entrustingOrder')">
-        委托挂单
-      </div>
+
+    <!-- 周期切换 Tab -->
+    <nav class="time-tabs">
       <div
-        class="sys-notifi pf600 fs14 fcA8A8A8"
-        @click="messageChange('latestTransactions')">
-        最新成交
+        v-for="tab in tabs"
+        :key="tab"
+        class="tab-item"
+        :style="currentTab === tab ? { backgroundColor: '#F6465D', color: '#fff' } : {}"
+        @click="switchPeriod(tab)"
+      >
+        {{ tab }}
       </div>
+      <div class="tab-item icon">更多 <span class="triangle">◢</span></div>
+      <div class="tab-item icon">
+        <img src="@/assets/icon/bitcoin/lishidingdan.svg" alt="">
+      </div>
+    </nav>
+
+    <div class="k-line-main">
+      <KlineChart
+        ref="klineRef"
+        :data="kLineData"
+        height="100%"
+        :precision="{ price: getPricePrecision(marketInfo.price), volume: 2 }"
+      />
     </div>
-    <component :is="currentComponent" />
+
+    <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>
+
+    <component :is="currentComponent" :symbol-id="symbolId" />
   </div>
 </template>
+
 <script setup>
-  import EntrustingOrder from "./EntrustingOrder.vue";
-  import LatestTransactions from "./LatestTransactions.vue";
-  import { ref, computed } from "vue";
-
-  const current = ref("entrustingOrder");
-  const componentsMap = {
-    entrustingOrder: EntrustingOrder,
-    latestTransactions: LatestTransactions,
-  };
-  const currentComponent = computed(() => componentsMap[current.value]);
+import { ref, computed, onMounted, onUnmounted, watch, onBeforeUnmount } from "vue";
+import { useRoute } from "vue-router";
+import { GetCandlestickChart } from "@/api/index.js";
+import EntrustingOrder from "./EntrustingOrder.vue";
+import LatestTransactions from "./LatestTransactions.vue";
+import KlineChart from "@/views/bitcoin/lever/components/KLineChart.vue";
+
+const route = useRoute();
+const symbolId = computed(() => route.query.id || '6');
+
+const currentTab = ref('1d');
+const tabs = ['1h', '6h', '1d', '1w', '1m'];
+const kLineData = ref([]);
+const socket = ref(null);
+const WS_BASE_URL = 'ws://backend.66linknow.com/ws/kline/';
+
+// --- 生产级配置 ---
+const HEARTBEAT_INTERVAL = 15000; // 心跳间隔 15s
+const RECONNECT_DELAY = 3000;     // 重连延迟 3s
+let heartbeatTimer = null;
+let reconnectTimer = null;
+let isUnmounted = false;
+
+const marketInfo = ref({
+  price: '0.00', fiatPrice: '0.00', change: 0.00, high: '0.00', low: '0.00', vol: '0', amount: '0'
+});
+
+const current = ref("entrustingOrder");
+const componentsMap = { entrustingOrder: EntrustingOrder, latestTransactions: LatestTransactions };
+const currentComponent = computed(() => componentsMap[current.value]);
+
+// --- 1. 切换周期 ---
+const switchPeriod = (period) => {
+  if (currentTab.value === period) return;
+
+  currentTab.value = period;
+
+  // 清空数据,触发子组件重置,并重新请求
+  kLineData.value = [];
+  getKlineData();
+};
+
+// --- 2. HTTP 获取历史数据 ---
+const getKlineData = async () => {
+  if (typeof GetCandlestickChart !== 'function') return;
+
+  try {
+    const res = await GetCandlestickChart({
+      symbol: symbolId.value,
+      period: currentTab.value
+    });
+
+    let rawList = [];
+    if (Array.isArray(res)) rawList = res;
+    else if (res && Array.isArray(res.data)) rawList = 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);
+      kLineData.value = formattedData;
+
+      // 同步合并:如果 WS 已经有最新价,立即修正历史数据最后一根,防止回跳
+      if (marketInfo.value.price !== '0.00') {
+        const lastBar = formattedData[formattedData.length - 1];
+        const realTimePrice = parseFloat(marketInfo.value.price);
+        lastBar.close = realTimePrice;
+        // 修正高低
+        if (realTimePrice > lastBar.high) lastBar.high = realTimePrice;
+        if (realTimePrice < lastBar.low) lastBar.low = realTimePrice;
+      }
+
+      updateMarketInfoFromKline(formattedData);
+    }
+  } catch (error) { console.error("API Error", error); }
+};
 
-  const messageChange = (key) => {
-    current.value = key;
+const updateMarketInfoFromKline = (data) => {
+  if (!data.length) return;
+  const lastBar = data[data.length - 1];
+
+  // 仅当没数据或 WS 未连接时使用历史数据兜底
+  if (marketInfo.value.price === '0.00' || !socket.value || socket.value.readyState !== 1) {
+    const firstBar = data[0];
+    let maxHigh = -Infinity, minLow = Infinity, totalVol = 0;
+    data.forEach(item => {
+      if (item.high > maxHigh) maxHigh = item.high;
+      if (item.low < minLow) minLow = item.low;
+      totalVol += item.volume;
+    });
+    const changeRate = firstBar.open ? ((lastBar.close - firstBar.open) / firstBar.open) * 100 : 0;
+
+    marketInfo.value = {
+      price: lastBar.close,
+      change: changeRate.toFixed(2),
+      high: maxHigh, low: minLow, vol: totalVol, amount: totalVol * lastBar.close,
+      fiatPrice: lastBar.close
+    };
+  }
+};
+
+// --- 3. WS 连接 (含心跳设计) ---
+const connectWebSocket = () => {
+  // 清理旧资源
+  closeWebSocket();
+
+  const symbolStr = String(route.query.type || 'btcusdt').toLowerCase();
+  const url = `${WS_BASE_URL}?symbol=${symbolStr}`;
+
+  console.log('WS 连接:', url);
+
+  try {
+    socket.value = new WebSocket(url);
+
+    socket.value.onopen = () => {
+      console.log('✅ WS Connected');
+      startHeartbeat(); // 启动心跳
+    };
+
+    socket.value.onmessage = (event) => {
+      handleSocketMessage(event.data);
+    };
+
+    socket.value.onclose = () => {
+      stopHeartbeat(); // 停止心跳
+      if (!isUnmounted) {
+        console.log('⚠️ WS Closed, reconnecting...');
+        clearTimeout(reconnectTimer);
+        reconnectTimer = setTimeout(() => connectWebSocket(), RECONNECT_DELAY);
+      }
+    };
+
+    socket.value.onerror = (err) => {
+      // onerror 通常会触发 onclose,由 onclose 处理重连
+      console.error('❌ WS Error', err);
+    };
+  } 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(() => {
+    if (socket.value && socket.value.readyState === WebSocket.OPEN) {
+      // 发送 ping,具体格式看后端要求,一般是字符串 'ping' 或 JSON
+      socket.value.send("ping");
+    }
+  }, HEARTBEAT_INTERVAL);
+};
+
+const stopHeartbeat = () => {
+  if (heartbeatTimer) {
+    clearInterval(heartbeatTimer);
+    heartbeatTimer = null;
+  }
+};
+
+// --- 辅助:获取周期对应的毫秒数 ---
+const getPeriodMs = (period) => {
+  const map = {
+    '1m': 60 * 1000,
+    '5m': 5 * 60 * 1000,
+    '15m': 15 * 60 * 1000,
+    '30m': 30 * 60 * 1000,
+    '1h': 60 * 60 * 1000,
+    '4h': 4 * 60 * 60 * 1000,
+    '6h': 6 * 60 * 60 * 1000,
+    '1d': 24 * 60 * 60 * 1000,
+    '1w': 7 * 24 * 60 * 60 * 1000,
+    '1M': 30 * 24 * 60 * 60 * 1000
   };
+  return map[period] || 60 * 60 * 1000;
+}
+
+// --- 4. 核心:处理实时消息 (24hrTicker) ---
+const handleSocketMessage = (msgStr) => {
+  try {
+    // 忽略心跳响应
+    if (msgStr === 'pong') return;
+
+    const rawData = JSON.parse(msgStr);
+    const msg = rawData.data || rawData;
+
+    if (msg.e === "24hrTicker") {
+      marketInfo.value = {
+        price: msg.c, change: parseFloat(msg.P), high: msg.h, low: msg.l, vol: msg.v, amount: msg.q, fiatPrice: msg.c
+      };
+
+      if (kLineData.value.length > 0) {
+        const lastIndex = kLineData.value.length - 1;
+        const lastBar = kLineData.value[lastIndex];
+        const newPrice = parseFloat(msg.c);
+        const currentTime = Number(msg.E); // 事件时间
+        const periodMs = getPeriodMs(currentTab.value);
+
+        // 标准时间戳对齐算法: (当前时间 / 周期) * 周期
+        const currentBarStart = Math.floor(currentTime / periodMs) * periodMs;
+
+        // 如果计算出的起始时间 > 最后一根的起始时间,说明跨周期了,生成新 K 线
+        if (currentBarStart > lastBar.timestamp) {
+          const newBar = {
+            timestamp: currentBarStart,
+            open: newPrice,
+            high: newPrice,
+            low: newPrice,
+            close: newPrice,
+            volume: 0
+          };
+          // 扩展运算符触发更新
+          kLineData.value = [...kLineData.value, newBar];
+
+        } else {
+          // 还在当前周期内,更新最后一根
+          const updatedBar = {
+            ...lastBar,
+            close: newPrice,
+            high: Math.max(lastBar.high, newPrice),
+            low: Math.min(lastBar.low, newPrice)
+          };
+          kLineData.value.splice(lastIndex, 1, updatedBar);
+        }
+      }
+    }
+  } catch (e) {}
+};
+
+// --- 页面可见性监听 (切屏回来自动重连) ---
+const handleVisibilityChange = () => {
+  if (document.visibilityState === 'visible') {
+    if (!socket.value || socket.value.readyState !== WebSocket.OPEN) {
+      console.log('👀 Page Visible, reconnecting...');
+      connectWebSocket();
+    }
+  }
+};
+
+onMounted(() => {
+  isUnmounted = false;
+  getKlineData();
+  connectWebSocket();
+  document.addEventListener('visibilitychange', handleVisibilityChange);
+});
+
+onBeforeUnmount(() => {
+  isUnmounted = true;
+  closeWebSocket();
+  document.removeEventListener('visibilitychange', handleVisibilityChange);
+});
+
+watch(symbolId, () => {
+  kLineData.value = [];
+  getKlineData();
+  connectWebSocket();
+}, { immediate: false });
+
+const messageChange = (key) => { current.value = key; };
+const formatNumber = (num) => {
+  if (!num) return '0.00';
+  const n = Number(num);
+  return n.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 6 });
+};
+const abbreviateNumber = (value) => {
+  if (!value) return '0.00';
+  let num = parseFloat(value);
+  if (isNaN(num)) return '0.00';
+  if (num >= 1e9) return (num / 1e9).toFixed(2) + 'B';
+  if (num >= 1e6) return (num / 1e6).toFixed(2) + 'M';
+  if (num >= 1e3) return (num / 1e3).toFixed(2) + 'K';
+  return num.toFixed(2);
+};
+const getPricePrecision = (price) => price < 1 ? 6 : (price < 10 ? 4 : 2);
+const getPriceColor = (change) => change >= 0 ? 'fc45B26B' : 'fcF6465D';
+const getUpDownClass = (change) => change >= 0 ? 'fc45B26B' : 'fcF6465D';
 </script>
+
 <style lang="less" scoped>
-  .market-conditions {
+.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-top: 10px;
+  padding-bottom: 0px;
+}
+
+.tab-item {
+  font-size: 14px;
+  color: #929AA5;
+  padding: 4px 10px;
+  border-radius: 6px;
+  cursor: pointer;
+  font-weight: 500;
+  transition: all 0.2s;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.tab-item.icon {
+  color: #929AA5;
+  font-size: 12px;
+  padding: 4px 4px;
+}
+
+.triangle {
+  font-size: 8px;
+  margin-left: 2px;
+  transform: scale(0.9);
+}
+
+.tab-item img {
+  display: block;
+  height: 16px;
+  width: auto;
+}
+
+.market-conditions {
+  display: flex;
+  flex-direction: column;
+  justify-content: flex-start;
+  align-items: center;
+  width: 100%;
+
+  .market-price {
     display: flex;
-    flex-direction: column;
-    justify-content: flex-start;
-    align-items: center;
-    width: 349px;
+    flex-direction: row;
+    justify-content: space-between;
+    margin-top: 8px;
+    width: 100%;
+    height: 73px;
+    padding: 0 15px;
+    box-sizing: border-box;
 
-    .market-price {
+    .price-left {
       display: flex;
-      flex-direction: row;
-      justify-content: space-between;
-      margin-top: 8px;
-      width: 100%;
-      height: 73px;
+      flex-direction: column;
+      justify-content: flex-start;
+      width: 144px;
+      height: 69px;
 
-      .price-left {
+      .left-price {
         display: flex;
-        flex-direction: column;
+        flex-direction: row;
         justify-content: flex-start;
-        width: 144px;
-        height: 69px;
-
-        .left-price {
-          display: flex;
-          flex-direction: row;
-          justify-content: flex-start;
-          align-items: center;
-          height: 18px;
-
-          img {
-            margin-left: 5px;
-            width: 8px;
-            height: 4px;
-          }
-        }
+        align-items: center;
+        height: 18px;
 
-        .left-number {
-          margin-top: 5px;
+        img {
+          margin-left: 5px;
+          width: 8px;
+          height: 4px;
         }
+      }
 
-        .left-appro {
-          display: flex;
-          flex-direction: row;
-          justify-content: flex-start;
-          align-items: center;
-          margin-top: 3px;
-
-          .appro {
-            margin-left: 9px;
-          }
-        }
+      .left-number {
+        margin-top: 5px;
       }
 
-      .price-right {
+      .left-appro {
         display: flex;
-        flex-direction: column;
+        flex-direction: row;
         justify-content: flex-start;
-        height: 100%;
-
-        .right-number-top {
-          display: flex;
-          flex-direction: row;
-          justify-content: flex-start;
-          width: 189px;
-          height: 32px;
-
-          .right-number-top-price {
-            width: 93px;
-            height: 32px;
-
-            div {
-              height: 16px;
-              line-height: 16px;
-              text-align: end;
-            }
-          }
+        align-items: center;
+        margin-top: 3px;
 
-          .right-number-top-number {
-            width: 93px;
-            height: 32px;
-
-            div {
-              height: 16px;
-              line-height: 16px;
-              text-align: end;
-            }
-          }
+        .appro {
+          margin-left: 9px;
         }
+      }
+    }
 
-        .right-number-bottom {
-          display: flex;
-          flex-direction: row;
-          justify-content: flex-start;
-          margin-top: 9px;
-          width: 189px;
-          height: 32px;
-
-          .right-number-top-price {
-            width: 93px;
-            height: 32px;
-
-            div {
-              height: 16px;
-              line-height: 16px;
-              text-align: end;
-            }
-          }
+    .price-right {
+      display: flex;
+      flex-direction: column;
+      justify-content: flex-start;
+      height: 100%;
 
-          .right-number-top-number {
-            width: 93px;
-            height: 32px;
+      .right-number-top, .right-number-bottom {
+        display: flex;
+        flex-direction: row;
+        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;
             }
-          }
         }
       }
+      .right-number-bottom {
+        margin-top: 9px;
+      }
     }
+  }
 
-    .k-line-main {
-      margin-top: 15px;
-      width: 100%;
-      height: 283px;
-      background: pink;
-    }
+  .k-line-main {
+    height: 50vh;
+    min-height: 350px;
+    width: 100%;
+  }
 
-    .notifi-classifi {
-      display: flex;
-      flex-direction: row;
-      justify-content: flex-start;
-      align-items: flex-end;
-      margin-top: 15px;
-      width: 349px;
-      height: 24px;
+  .notifi-classifi {
+    display: flex;
+    flex-direction: row;
+    justify-content: flex-start;
+    align-items: flex-end;
+    margin-top: 15px;
+    width: 100%;
+    padding: 0 15px;
+    box-sizing: border-box;
+    height: 24px;
 
-      .sys-notifi {
-        margin-left: 47px;
-      }
+    .sys-notifi {
+      margin-left: 47px;
     }
   }
-</style>
+}
+</style>

+ 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://backend.66linknow.com', // 后端 IP
+        changeOrigin: true,
+        ws: true // ⚠️ 开启 WebSocket 支持
+        // 这里是否需要 pathRewrite 取决于后端路径有没有 /ws
+      }
+    }
+  },
 
   // 1. 基础路径 (解决白屏问题)
   publicPath: './',

Daži faili netika attēloti, jo izmaiņu fails ir pārāk liels