| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425 |
- <template>
- <div class="hot-coin">
- <div class="coin-title pf600 fs18 fc121212">热门币种</div>
- <div class="coin-head">
- <div class="name">
- <div class="name-text pf400 fs12 fc666666">交易对</div>
- <div class="list-sort">
- <div class="sort-up">
- <img src="../../../assets/icon/index/Triangle.svg" alt="" />
- </div>
- <div class="sort-bottom">
- <img src="../../../assets/icon/index/Triangle 2.svg" alt="" />
- </div>
- </div>
- </div>
- <div class="name price">
- <div class="name-text pf400 fs12 fc666666">最新价</div>
- <div class="list-sort">
- <div class="sort-up">
- <img src="../../../assets/icon/index/Triangle.svg" alt="" />
- </div>
- <div class="sort-bottom">
- <img src="../../../assets/icon/index/Triangle 2.svg" alt="" />
- </div>
- </div>
- </div>
- <div class="name today">
- <div class="name-text pf400 fs12 fc666666">今日涨跌幅</div>
- <div class="list-sort">
- <div class="sort-up">
- <img src="../../../assets/icon/index/Triangle.svg" alt="" />
- </div>
- <div class="sort-bottom">
- <img src="../../../assets/icon/index/Triangle 2.svg" alt="" />
- </div>
- </div>
- </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>
- <div class="item-right pf500 fs12 fcFFFFFF" :class="getChangeColor(item.change_rate)">{{ formatChange(item.change_rate) }}</div>
- </div>
- </div>
- </div>
- </template>
- <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;
- }
- .coin-head {
- display: flex;
- flex-direction: row;
- justify-content: flex-start;
- align-items: center;
- margin-top: 6px;
- width: 100%;
- height: 24px;
- .name {
- display: flex;
- flex-direction: row;
- justify-content: flex-start;
- align-items: center;
- height: 24px;
- .list-sort {
- display: flex;
- flex-direction: column;
- justify-content: flex-start;
- margin-left: 4px;
- width: 8px;
- height: 16px;
- .sort-up,
- .sort-bottom {
- display: flex;
- flex-direction: row;
- justify-content: center;
- align-items: center;
- width: 8px;
- height: 8px;
- img {
- width: 8px;
- height: 4px;
- }
- }
- }
- }
- .price {
- margin-left: 128px;
- }
- .today {
- margin-left: 47px;
- }
- }
- .coin-body {
- margin-top: 9px;
- width: 100%;
- .body-item {
- display: flex;
- flex-direction: row;
- justify-content: flex-start;
- align-items: center;
- margin-top: 23.5px;
- width: 100%;
- height: 38px;
- &:nth-child(1) {
- margin-top: 0;
- }
- .item-left {
- display: flex;
- flex-direction: row;
- justify-content: flex-start;
- align-items: center;
- width: 276px;
- height: 100%;
- .coin-img {
- width: 32px;
- height: 32px;
- img {
- width: 32px;
- height: 32px;
- }
- }
- .coin-name {
- margin-left: 10px;
- width: 85px;
- height: 34px;
- .upper-name {
- height: 20px;
- line-height: 20px;
- }
- .letter-name {
- height: 14px;
- line-height: 14px;
- }
- }
- .coin-echars {
- width: 60px;
- height: 35px;
- }
- .coin-price {
- margin-left: 13px;
- width: 75px;
- height: 38px;
- .upper-price {
- height: 20px;
- line-height: 20px;
- text-align: right;
- }
- .letter-price {
- height: 16px;
- line-height: 16px;
- text-align: right;
- }
- }
- }
- .item-right {
- margin-left: 8px;
- width: 61px;
- height: 25px;
- line-height: 25px;
- text-align: center;
- background: #45b26b;
- border-radius: 5px;
- }
- }
- }
- }
- //
- // //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>
|