HotCoin.vue 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369
  1. <template>
  2. <div class="hot-coin">
  3. <div class="coin-title pf600 fs18 fc121212">热门币种</div>
  4. <div class="coin-head">
  5. <div class="name">
  6. <div class="name-text pf400 fs12 fc666666">交易对</div>
  7. <div class="list-sort">
  8. <div class="sort-up">
  9. <img src="../../../assets/icon/index/Triangle.svg" alt="" />
  10. </div>
  11. <div class="sort-bottom">
  12. <img src="../../../assets/icon/index/Triangle 2.svg" alt="" />
  13. </div>
  14. </div>
  15. </div>
  16. <div class="name price">
  17. <div class="name-text pf400 fs12 fc666666">最新价</div>
  18. <div class="list-sort">
  19. <div class="sort-up">
  20. <img src="../../../assets/icon/index/Triangle.svg" alt="" />
  21. </div>
  22. <div class="sort-bottom">
  23. <img src="../../../assets/icon/index/Triangle 2.svg" alt="" />
  24. </div>
  25. </div>
  26. </div>
  27. <div class="name today">
  28. <div class="name-text pf400 fs12 fc666666">今日涨跌幅</div>
  29. <div class="list-sort">
  30. <div class="sort-up">
  31. <img src="../../../assets/icon/index/Triangle.svg" alt="" />
  32. </div>
  33. <div class="sort-bottom">
  34. <img src="../../../assets/icon/index/Triangle 2.svg" alt="" />
  35. </div>
  36. </div>
  37. </div>
  38. </div>
  39. <div class="coin-body" >
  40. <div class="body-item" v-for="(item, index) in coinList" :key="item.id || index">
  41. <div class="item-left" @click="router.push({ path: '/marketDetails', query: { id: item.id, type: item.symbol.toLowerCase()} })">
  42. <div class="coin-img" >
  43. <img :src="item.logo" alt="" />
  44. </div>
  45. <div class="coin-name">
  46. <div class="upper-name pf500 fs14 fc2C3131">{{ formatSymbol(item.symbol) }}</div>
  47. <div class="letter-name pf400 fs10 fcA9A9A9">{{ item.name }}</div>
  48. </div>
  49. <div class="coin-echars"></div>
  50. <div class="coin-price">
  51. <div class="upper-price pf500 fs14 fc2C3131">{{ formatPrice(item.current_price) }}</div>
  52. <div class="letter-price pf400 fs10 fcA9A9A9">≈ ${{ formatPrice(item.current_price) }}</div>
  53. </div>
  54. </div>
  55. <div class="item-right pf500 fs12 fcFFFFFF" :class="getChangeColor(item.trend)">{{ formatChange(item.trend) }}</div>
  56. </div>
  57. </div>
  58. </div>
  59. </template>
  60. <script setup>
  61. import { GetCoins } from '@/api/index.js'
  62. import { ref, onMounted, onUnmounted } from 'vue'
  63. import { useRoute, useRouter } from "vue-router";
  64. const router = useRouter();
  65. const coinList = ref([])
  66. let socket = null
  67. let heartbeatTimer = null
  68. let reconnectTimer = null
  69. let isUnmounted = false
  70. // --- 辅助函数 ---
  71. const formatSymbol = (symbol) => symbol ? symbol.replace('USDT', '') : ''
  72. const formatPrice = (price) => price ? parseFloat(price).toFixed(2) : '0.00'
  73. // 格式化涨跌幅
  74. const formatChange = (val) => {
  75. if (!val) return '+0.00%'
  76. const num = parseFloat(val)
  77. return (num > 0 ? '+' : '') + num.toFixed(2) + '%'
  78. }
  79. // 获取颜色
  80. const getChangeColor = (val) => {
  81. if (!val) return 'bg-gray'
  82. return parseFloat(val) >= 0 ? 'bg-green' : 'bg-red'
  83. }
  84. // --- 1. WebSocket 核心逻辑 ---
  85. const initWebSocket = () => {
  86. if (socket) {
  87. socket.onclose = null
  88. socket.close()
  89. socket = null
  90. }
  91. if (coinList.value.length === 0) return
  92. const symbolsParam = coinList.value
  93. .map(item => item.symbol.toLowerCase())
  94. .join('/')
  95. const query = `?symbol=${symbolsParam}`
  96. const host = process.env.NODE_ENV === 'production'
  97. ? 'backend.66linknow.com'
  98. : 'localhost:8080'
  99. const wsUrl = `ws://${host}/ws/kline/${query}`
  100. try {
  101. socket = new WebSocket(wsUrl)
  102. } catch (err) {
  103. reconnect()
  104. return
  105. }
  106. socket.onopen = () => {
  107. startHeartbeat()
  108. if (reconnectTimer) clearTimeout(reconnectTimer)
  109. }
  110. socket.onmessage = (event) => {
  111. if (event.data === 'pong' || event.data === 'ping') return
  112. try {
  113. const msg = JSON.parse(event.data)
  114. if (msg.data) {
  115. updateCoinData(msg.data)
  116. } else {
  117. updateCoinData(msg)
  118. }
  119. } catch (e) {}
  120. }
  121. socket.onerror = (err) => {
  122. console.error('❌ WS 报错')
  123. }
  124. socket.onclose = (e) => {
  125. if (e.code === 1000) return
  126. socket = null
  127. reconnect()
  128. }
  129. }
  130. // --- 2. 更新数据 (核心适配) ---
  131. const updateCoinData = (ticker) => {
  132. // WS 推送格式: { s: "XRPUSDT", c: "2.18", P: "9.01" }
  133. if (!ticker || !ticker.s) return
  134. const targetCoin = coinList.value.find(item =>
  135. item.symbol.toUpperCase() === ticker.s.toUpperCase()
  136. )
  137. if (targetCoin) {
  138. // 【修改点4】: WebSocket 更新时,赋值给新的字段名
  139. // c = current price -> 赋值给 current_price
  140. if (ticker.c) targetCoin.current_price = ticker.c
  141. // P = percentage change -> 赋值给 trend
  142. if (ticker.P) {targetCoin.trend = ticker.P; targetCoin.p = ticker.P}
  143. }
  144. }
  145. // --- 3. 自动重连 ---
  146. const reconnect = () => {
  147. if (isUnmounted) return
  148. if (reconnectTimer) return
  149. reconnectTimer = setTimeout(() => {
  150. reconnectTimer = null
  151. initWebSocket()
  152. }, 3000)
  153. }
  154. // --- 4. 心跳保活 ---
  155. const startHeartbeat = () => {
  156. if (heartbeatTimer) clearInterval(heartbeatTimer)
  157. heartbeatTimer = setInterval(() => {
  158. if (socket && socket.readyState === WebSocket.OPEN) {
  159. socket.send('ping')
  160. }
  161. }, 10000)
  162. }
  163. // --- 生命周期 ---
  164. onMounted(async () => {
  165. try {
  166. const res = await GetCoins()
  167. // 注意:确保 res 已经是数组,或者取 res.data
  168. coinList.value = Array.isArray(res) ? res : (res.data || [])
  169. if (coinList.value.length > 0) {
  170. initWebSocket()
  171. }
  172. } catch (error) {
  173. console.error('API 失败:', error)
  174. }
  175. })
  176. onUnmounted(() => {
  177. isUnmounted = true
  178. if (heartbeatTimer) clearInterval(heartbeatTimer)
  179. if (reconnectTimer) clearTimeout(reconnectTimer)
  180. if (socket) {
  181. socket.onclose = null
  182. socket.close()
  183. socket = null
  184. }
  185. })
  186. </script>
  187. <style lang="less" scoped>
  188. /* 样式保持不变,省略以节省空间 */
  189. .hot-coin {
  190. margin-top: 20px;
  191. width: 346px;
  192. height: 333px;
  193. .coin-title {
  194. height: 24px;
  195. line-height: 24px;
  196. }
  197. .coin-head {
  198. display: flex;
  199. flex-direction: row;
  200. justify-content: flex-start;
  201. align-items: center;
  202. margin-top: 6px;
  203. width: 100%;
  204. height: 24px;
  205. .name {
  206. display: flex;
  207. flex-direction: row;
  208. justify-content: flex-start;
  209. align-items: center;
  210. height: 24px;
  211. .list-sort {
  212. display: flex;
  213. flex-direction: column;
  214. justify-content: flex-start;
  215. margin-left: 4px;
  216. width: 8px;
  217. height: 16px;
  218. .sort-up,
  219. .sort-bottom {
  220. display: flex;
  221. flex-direction: row;
  222. justify-content: center;
  223. align-items: center;
  224. width: 8px;
  225. height: 8px;
  226. img {
  227. width: 8px;
  228. height: 4px;
  229. }
  230. }
  231. }
  232. }
  233. .price {
  234. margin-left: 128px;
  235. }
  236. .today {
  237. margin-left: 47px;
  238. }
  239. }
  240. .coin-body {
  241. margin-top: 9px;
  242. width: 100%;
  243. .body-item {
  244. display: flex;
  245. flex-direction: row;
  246. justify-content: flex-start;
  247. align-items: center;
  248. margin-top: 23.5px;
  249. width: 100%;
  250. height: 38px;
  251. &:nth-child(1) {
  252. margin-top: 0;
  253. }
  254. .item-left {
  255. display: flex;
  256. flex-direction: row;
  257. justify-content: flex-start;
  258. align-items: center;
  259. width: 276px;
  260. height: 100%;
  261. .coin-img {
  262. width: 32px;
  263. height: 32px;
  264. img {
  265. width: 32px;
  266. height: 32px;
  267. }
  268. }
  269. .coin-name {
  270. margin-left: 10px;
  271. width: 85px;
  272. height: 34px;
  273. .upper-name {
  274. height: 20px;
  275. line-height: 20px;
  276. }
  277. .letter-name {
  278. height: 14px;
  279. line-height: 14px;
  280. }
  281. }
  282. .coin-echars {
  283. width: 60px;
  284. height: 35px;
  285. }
  286. .coin-price {
  287. margin-left: 13px;
  288. width: 75px;
  289. height: 38px;
  290. .upper-price {
  291. height: 20px;
  292. line-height: 20px;
  293. text-align: right;
  294. }
  295. .letter-price {
  296. height: 16px;
  297. line-height: 16px;
  298. text-align: right;
  299. }
  300. }
  301. }
  302. .item-right {
  303. margin-left: 8px;
  304. width: 61px;
  305. height: 25px;
  306. line-height: 25px;
  307. text-align: center;
  308. background: #45b26b;
  309. border-radius: 5px;
  310. }
  311. }
  312. }
  313. }
  314. /* 颜色配置 */
  315. .bg-green { background-color: #2EBD85!important; }
  316. .bg-red { background-color: #F6465D!important; }
  317. .bg-gray { background-color: #C0C0C0; }
  318. </style>