|
|
@@ -36,185 +36,200 @@
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
- <div class="coin-body" >
|
|
|
- <div class="body-item" v-for="(item, index) in coinList" :key="item.id || index">
|
|
|
- <div class="item-left" @click="router.push({ path: '/marketDetails', query: { id: item.id, type: item.symbol.toLowerCase()} })">
|
|
|
- <div class="coin-img" >
|
|
|
- <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 class="coin-body">
|
|
|
+ <div class="body-item" v-for="(item, index) in coinList" :key="item.id || index">
|
|
|
+ <div
|
|
|
+ class="item-left"
|
|
|
+ @click="
|
|
|
+ router.push({
|
|
|
+ path: '/marketDetails',
|
|
|
+ query: { id: item.id, type: item.symbol.toLowerCase() },
|
|
|
+ })
|
|
|
+ ">
|
|
|
+ <div class="coin-img">
|
|
|
+ <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.current_price) }}
|
|
|
+ </div>
|
|
|
+ <div class="letter-price pf400 fs10 fcA9A9A9">
|
|
|
+ ≈ ${{ formatPrice(item.current_price) }}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
- <div class="coin-echars"></div>
|
|
|
- <div class="coin-price">
|
|
|
- <div class="upper-price pf500 fs14 fc2C3131">{{ formatPrice(item.current_price) }}</div>
|
|
|
- <div class="letter-price pf400 fs10 fcA9A9A9">≈ ${{ formatPrice(item.current_price) }}</div>
|
|
|
+ <div class="item-right pf500 fs12 fcFFFFFF" :class="getChangeColor(item.trend)">
|
|
|
+ {{ formatChange(item.trend) }}
|
|
|
</div>
|
|
|
</div>
|
|
|
- <div class="item-right pf500 fs12 fcFFFFFF" :class="getChangeColor(item.trend)">{{ formatChange(item.trend) }}</div>
|
|
|
- </div>
|
|
|
</div>
|
|
|
</div>
|
|
|
</template>
|
|
|
|
|
|
<script setup>
|
|
|
-import { GetCoins } from '@/api/index.js'
|
|
|
-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'
|
|
|
-
|
|
|
-// 格式化涨跌幅
|
|
|
-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
|
|
|
+ import { GetCoins } from "@/api/index.js";
|
|
|
+ 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");
|
|
|
+
|
|
|
+ // 格式化涨跌幅
|
|
|
+ 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;
|
|
|
+ }
|
|
|
|
|
|
- const symbolsParam = coinList.value
|
|
|
- .map(item => item.symbol.toLowerCase())
|
|
|
- .join('/')
|
|
|
+ if (coinList.value.length === 0) return;
|
|
|
|
|
|
- const query = `?symbol=${symbolsParam}`
|
|
|
+ const symbolsParam = coinList.value
|
|
|
+ .map((item) => item.symbol.toLowerCase())
|
|
|
+ .join("/");
|
|
|
|
|
|
- const host = process.env.NODE_ENV === 'production'
|
|
|
- ? 'backend.66linknow.com'
|
|
|
- : 'localhost:8080'
|
|
|
- const wsUrl = `ws://${host}/ws/kline/${query}`
|
|
|
+ const query = `?symbol=${symbolsParam}`;
|
|
|
|
|
|
- try {
|
|
|
- socket = new WebSocket(wsUrl)
|
|
|
- } catch (err) {
|
|
|
- reconnect()
|
|
|
- return
|
|
|
- }
|
|
|
+ const wsUrl = `wss://test2.66linknow.com/ws/kline/${query}`;
|
|
|
|
|
|
- socket.onopen = () => {
|
|
|
- startHeartbeat()
|
|
|
- if (reconnectTimer) clearTimeout(reconnectTimer)
|
|
|
- }
|
|
|
-
|
|
|
- socket.onmessage = (event) => {
|
|
|
- if (event.data === 'pong' || event.data === 'ping') return
|
|
|
try {
|
|
|
- const msg = JSON.parse(event.data)
|
|
|
- if (msg.data) {
|
|
|
- updateCoinData(msg.data)
|
|
|
- } else {
|
|
|
- updateCoinData(msg)
|
|
|
- }
|
|
|
- } catch (e) {}
|
|
|
- }
|
|
|
-
|
|
|
- socket.onerror = (err) => {
|
|
|
- console.error('❌ WS 报错')
|
|
|
- }
|
|
|
-
|
|
|
- socket.onclose = (e) => {
|
|
|
- if (e.code === 1000) return
|
|
|
- socket = null
|
|
|
- reconnect()
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-// --- 2. 更新数据 (核心适配) ---
|
|
|
-const updateCoinData = (ticker) => {
|
|
|
- // WS 推送格式: { s: "XRPUSDT", c: "2.18", P: "9.01" }
|
|
|
-
|
|
|
- if (!ticker || !ticker.s) return
|
|
|
-
|
|
|
- const targetCoin = coinList.value.find(item =>
|
|
|
- item.symbol.toUpperCase() === ticker.s.toUpperCase()
|
|
|
- )
|
|
|
-
|
|
|
- if (targetCoin) {
|
|
|
- // 【修改点4】: WebSocket 更新时,赋值给新的字段名
|
|
|
- // c = current price -> 赋值给 current_price
|
|
|
- if (ticker.c) targetCoin.current_price = ticker.c
|
|
|
+ socket = new WebSocket(wsUrl);
|
|
|
+ } catch (err) {
|
|
|
+ reconnect();
|
|
|
+ return;
|
|
|
+ }
|
|
|
|
|
|
- // P = percentage change -> 赋值给 trend
|
|
|
- if (ticker.P) {targetCoin.trend = ticker.P; targetCoin.p = 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')
|
|
|
+ socket.onopen = () => {
|
|
|
+ startHeartbeat();
|
|
|
+ if (reconnectTimer) clearTimeout(reconnectTimer);
|
|
|
+ };
|
|
|
+
|
|
|
+ socket.onmessage = (event) => {
|
|
|
+ if (event.data === "pong" || event.data === "ping") return;
|
|
|
+ try {
|
|
|
+ const msg = JSON.parse(event.data);
|
|
|
+ if (msg.data) {
|
|
|
+ updateCoinData(msg.data);
|
|
|
+ } else {
|
|
|
+ updateCoinData(msg);
|
|
|
+ }
|
|
|
+ } catch (e) {}
|
|
|
+ };
|
|
|
+
|
|
|
+ socket.onerror = (err) => {
|
|
|
+ console.error("❌ WS 报错");
|
|
|
+ };
|
|
|
+
|
|
|
+ socket.onclose = (e) => {
|
|
|
+ if (e.code === 1000) return;
|
|
|
+ socket = null;
|
|
|
+ reconnect();
|
|
|
+ };
|
|
|
+ };
|
|
|
+
|
|
|
+ // --- 2. 更新数据 (核心适配) ---
|
|
|
+ const updateCoinData = (ticker) => {
|
|
|
+ // WS 推送格式: { s: "XRPUSDT", c: "2.18", P: "9.01" }
|
|
|
+
|
|
|
+ if (!ticker || !ticker.s) return;
|
|
|
+
|
|
|
+ const targetCoin = coinList.value.find(
|
|
|
+ (item) => item.symbol.toUpperCase() === ticker.s.toUpperCase()
|
|
|
+ );
|
|
|
+
|
|
|
+ if (targetCoin) {
|
|
|
+ // 【修改点4】: WebSocket 更新时,赋值给新的字段名
|
|
|
+ // c = current price -> 赋值给 current_price
|
|
|
+ if (ticker.c) targetCoin.current_price = ticker.c;
|
|
|
+
|
|
|
+ // P = percentage change -> 赋值给 trend
|
|
|
+ if (ticker.P) {
|
|
|
+ targetCoin.trend = ticker.P;
|
|
|
+ targetCoin.p = ticker.P;
|
|
|
+ }
|
|
|
}
|
|
|
- }, 10000)
|
|
|
-}
|
|
|
+ };
|
|
|
+
|
|
|
+ // --- 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 {
|
|
|
- const res = await GetCoins()
|
|
|
- // 注意:确保 res 已经是数组,或者取 res.data
|
|
|
- coinList.value = Array.isArray(res) ? res : (res.data || [])
|
|
|
+ onMounted(async () => {
|
|
|
+ try {
|
|
|
+ const res = await GetCoins();
|
|
|
+ // 注意:确保 res 已经是数组,或者取 res.data
|
|
|
+ coinList.value = Array.isArray(res) ? res : res.data || [];
|
|
|
|
|
|
- if (coinList.value.length > 0) {
|
|
|
- initWebSocket()
|
|
|
+ if (coinList.value.length > 0) {
|
|
|
+ initWebSocket();
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error("API 失败:", error);
|
|
|
}
|
|
|
- } 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
|
|
|
- }
|
|
|
-})
|
|
|
+ });
|
|
|
+
|
|
|
+ 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;
|
|
|
@@ -362,8 +377,14 @@ onUnmounted(() => {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-/* 颜色配置 */
|
|
|
-.bg-green { background-color: #2EBD85!important; }
|
|
|
-.bg-red { background-color: #F6465D!important; }
|
|
|
-.bg-gray { background-color: #C0C0C0; }
|
|
|
-</style>
|
|
|
+ /* 颜色配置 */
|
|
|
+ .bg-green {
|
|
|
+ background-color: #2ebd85 !important;
|
|
|
+ }
|
|
|
+ .bg-red {
|
|
|
+ background-color: #f6465d !important;
|
|
|
+ }
|
|
|
+ .bg-gray {
|
|
|
+ background-color: #c0c0c0;
|
|
|
+ }
|
|
|
+</style>
|