377 lines
9.4 KiB
Vue
377 lines
9.4 KiB
Vue
<template>
|
||
<view class="crypto-page">
|
||
<!-- 状态栏占位 -->
|
||
<view class="status-bar" :style="{ height: statusBarHeight + 'px' }"></view>
|
||
|
||
<!-- 顶部导航 -->
|
||
<view class="navbar">
|
||
<text class="navbar__title">加密行情</text>
|
||
<AiAssistantEntry size="compact" />
|
||
</view>
|
||
|
||
<!-- 排序 Tab + 搜索 -->
|
||
<view class="market-tabs">
|
||
<view v-for="(tab, i) in marketTabs" :key="i" class="market-tab" :class="{ active: currentTab === i }"
|
||
@tap="switchTab(i)">
|
||
<text>{{ tab.label }}</text>
|
||
</view>
|
||
<view class="market-tabs__search">
|
||
<u-icon name="search" size="24" color="#999" />
|
||
<input class="market-tabs__input" v-model="searchKey" placeholder="搜索" placeholder-style="color:#ccc"
|
||
confirm-type="search" />
|
||
<u-icon v-if="searchKey" name="close-circle-fill" size="28" color="#ccc" @click="searchKey = ''" />
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 列表头 -->
|
||
<view class="list-header">
|
||
<text class="list-header__col">名称</text>
|
||
<text class="list-header__col">最新价</text>
|
||
<text class="list-header__col">24h涨跌</text>
|
||
</view>
|
||
|
||
<!-- 币种列表 -->
|
||
<scroll-view scroll-y class="coin-list" :style="{ height: coinListHeight }">
|
||
<view v-for="coin in displayCoins" :key="coin.symbol" class="coin-row" @tap="goCoinDetail(coin)">
|
||
<!-- 左:Logo + 名称 -->
|
||
<view class="coin-row__left">
|
||
<view class="coin-row__logo" :style="{ background: coin.color + '18' }">
|
||
<text class="coin-row__logo-text" :style="{ color: coin.color }">{{ coin.icon }}</text>
|
||
</view>
|
||
<view class="coin-row__name">
|
||
<text class="coin-row__symbol">{{ coin.symbol }}</text>
|
||
<text class="coin-row__subtitle">{{ coin.name }}</text>
|
||
</view>
|
||
</view>
|
||
<!-- 中:价格 + 市值 -->
|
||
<view class="coin-row__price">
|
||
<text class="coin-row__price-val">${{ coin.price }}</text>
|
||
<text class="coin-row__mcap">市值{{ coin.mcap }}</text>
|
||
</view>
|
||
<!-- 右:涨跌 Badge -->
|
||
<view class="coin-row__badge" :class="coin.change >= 0 ? 'up' : 'down'">
|
||
<text class="coin-row__badge-arrow">{{ coin.change >= 0 ? '▲' : '▼' }}</text>
|
||
<text class="coin-row__badge-text">{{ Math.abs(coin.change).toFixed(2) }}%</text>
|
||
</view>
|
||
</view>
|
||
</scroll-view>
|
||
|
||
</view>
|
||
</template>
|
||
|
||
<script lang="ts" setup>
|
||
import { ref, computed, onUnmounted } from 'vue'
|
||
import { onLoad, onShow, onHide } from '@dcloudio/uni-app'
|
||
import AiAssistantEntry from '@/components/ai-assistant-entry/ai-assistant-entry.vue'
|
||
import { getBinanceTickers, loadCoinConfig, type CoinTicker } from '@/api/crypto'
|
||
|
||
const statusBarHeight = ref(0)
|
||
const windowHeight = ref(667)
|
||
const coinListHeight = computed(() => {
|
||
const fixedHeight = statusBarHeight.value + uni.upx2px(232)
|
||
return `${windowHeight.value - fixedHeight}px`
|
||
})
|
||
|
||
const allCoins = ref<CoinTicker[]>([])
|
||
const searchKey = ref('')
|
||
|
||
const marketTabs = ref([
|
||
{ label: '热门', key: 'hot' },
|
||
{ label: '涨幅榜', key: 'gainers' },
|
||
{ label: '跌幅榜', key: 'losers' },
|
||
{ label: '成交额', key: 'volume' }
|
||
])
|
||
|
||
const currentTab = ref(0)
|
||
|
||
const displayCoins = computed(() => {
|
||
let list = [...allCoins.value]
|
||
const kw = searchKey.value.trim().toLowerCase()
|
||
if (kw) {
|
||
list = list.filter(c => c.symbol.toLowerCase().includes(kw) || c.name.toLowerCase().includes(kw))
|
||
}
|
||
if (currentTab.value === 1) return list.sort((a, b) => b.change - a.change)
|
||
if (currentTab.value === 2) return list.sort((a, b) => a.change - b.change)
|
||
if (currentTab.value === 3) return list.sort((a, b) => b.priceRaw - a.priceRaw)
|
||
return list
|
||
})
|
||
|
||
const fetchTickers = async () => {
|
||
console.log('[CryptoList] fetchTickers start')
|
||
try {
|
||
allCoins.value = await getBinanceTickers()
|
||
console.log('[CryptoList] fetchTickers ok count=>', allCoins.value.length)
|
||
} catch (e) {
|
||
console.error('[CryptoList] fetchTickers error=>', e)
|
||
uni.showToast({ title: '行情加载失败', icon: 'none' })
|
||
}
|
||
}
|
||
|
||
const switchTab = (i: number) => { currentTab.value = i }
|
||
|
||
|
||
const goCoinDetail = (coin: any) => {
|
||
uni.navigateTo({ url: `/pages/crypto/crypto_detail?symbol=${coin.symbol}` })
|
||
}
|
||
|
||
let refreshTimer: ReturnType<typeof setInterval> | null = null
|
||
|
||
const startAutoRefresh = () => {
|
||
stopAutoRefresh()
|
||
refreshTimer = setInterval(fetchTickers, 30000)
|
||
}
|
||
|
||
const stopAutoRefresh = () => {
|
||
if (refreshTimer) {
|
||
clearInterval(refreshTimer)
|
||
refreshTimer = null
|
||
}
|
||
}
|
||
|
||
onLoad(() => {
|
||
uni.getSystemInfo({
|
||
success: (res) => {
|
||
windowHeight.value = res.windowHeight || 667
|
||
// #ifdef APP-PLUS || MP
|
||
statusBarHeight.value = res.statusBarHeight || 44
|
||
// #endif
|
||
// #ifdef H5
|
||
statusBarHeight.value = 0
|
||
// #endif
|
||
}
|
||
})
|
||
loadCoinConfig().then(fetchTickers)
|
||
})
|
||
|
||
onShow(() => {
|
||
startAutoRefresh()
|
||
})
|
||
onHide(() => { stopAutoRefresh() })
|
||
onUnmounted(() => { stopAutoRefresh() })
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
// Design tokens from Figma
|
||
$bg: #ffffff;
|
||
$text-primary: #000000;
|
||
$text-secondary: #999999;
|
||
$border: rgba(0, 0, 0, 0.06);
|
||
$up: #ff2c58;
|
||
$down: #00c482;
|
||
$accent: #333333;
|
||
|
||
.crypto-page {
|
||
background: $bg;
|
||
min-height: 100vh;
|
||
color: $text-primary;
|
||
}
|
||
|
||
.status-bar {
|
||
background: $bg;
|
||
}
|
||
|
||
/* 导航栏 */
|
||
.navbar {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 16rpx;
|
||
padding: 0 32rpx;
|
||
height: 88rpx;
|
||
background: $bg;
|
||
|
||
&__title {
|
||
font-size: 36rpx;
|
||
font-weight: 700;
|
||
color: $text-primary;
|
||
}
|
||
|
||
}
|
||
|
||
/* 排序Tab */
|
||
.market-tabs {
|
||
display: flex;
|
||
padding: 0 32rpx;
|
||
gap: 8rpx;
|
||
height: 72rpx;
|
||
align-items: center;
|
||
|
||
&__search {
|
||
margin-left: auto;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8rpx;
|
||
background: #f5f5f5;
|
||
border-radius: 28rpx;
|
||
padding: 0 16rpx;
|
||
height: 52rpx;
|
||
flex: 1;
|
||
max-width: 200px;
|
||
}
|
||
|
||
&__input {
|
||
font-size: 24rpx;
|
||
width: 0;
|
||
flex: 1;
|
||
height: 52rpx;
|
||
line-height: 52rpx;
|
||
color: $text-primary;
|
||
}
|
||
}
|
||
|
||
.market-tab {
|
||
padding: 10rpx 18rpx;
|
||
font-size: 24rpx;
|
||
white-space: nowrap;
|
||
flex-shrink: 0;
|
||
color: $text-secondary;
|
||
border-radius: 32rpx;
|
||
transition: all 0.2s;
|
||
|
||
&.active {
|
||
color: #fff;
|
||
background: $accent;
|
||
font-weight: 600;
|
||
}
|
||
}
|
||
|
||
/* 列表头 — Figma: 402x41, bg=#F5F5F5, text=#666 */
|
||
.list-header {
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 0 32rpx;
|
||
height: 72rpx;
|
||
background: #f5f5f5;
|
||
|
||
&__col {
|
||
font-size: 22rpx;
|
||
color: #666666;
|
||
|
||
&:first-child {
|
||
flex: 1;
|
||
text-align: left;
|
||
padding-left: 60rpx;
|
||
}
|
||
|
||
&:nth-child(2) {
|
||
width: 160rpx;
|
||
text-align: right;
|
||
margin-right: 24rpx;
|
||
}
|
||
|
||
&:last-child {
|
||
width: 168rpx;
|
||
text-align: center;
|
||
}
|
||
}
|
||
}
|
||
|
||
/* 币种列表 */
|
||
.coin-list {
|
||
background: $bg;
|
||
}
|
||
|
||
/* 行情行 — 匹配 Figma: 370x65 → 740x130rpx */
|
||
.coin-row {
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 0 32rpx;
|
||
height: 130rpx;
|
||
border-bottom: 1rpx solid $border;
|
||
|
||
&__left {
|
||
flex: 1;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 20rpx;
|
||
min-width: 0;
|
||
}
|
||
|
||
&__logo {
|
||
width: 80rpx;
|
||
height: 80rpx;
|
||
border-radius: 50%;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
&__logo-text {
|
||
font-size: 28rpx;
|
||
font-weight: 700;
|
||
}
|
||
|
||
&__name {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 4rpx;
|
||
min-width: 0;
|
||
}
|
||
|
||
&__symbol {
|
||
font-size: 30rpx;
|
||
font-weight: 600;
|
||
color: $text-primary;
|
||
}
|
||
|
||
&__subtitle {
|
||
font-size: 24rpx;
|
||
color: $text-secondary;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
&__price {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: flex-end;
|
||
gap: 4rpx;
|
||
margin-right: 24rpx;
|
||
}
|
||
|
||
&__price-val {
|
||
font-size: 30rpx;
|
||
font-weight: 600;
|
||
color: $text-primary;
|
||
}
|
||
|
||
&__mcap {
|
||
font-size: 24rpx;
|
||
color: $text-secondary;
|
||
}
|
||
|
||
/* 涨跌Badge — Figma: 84x32 radius=12 */
|
||
&__badge {
|
||
width: 168rpx;
|
||
height: 64rpx;
|
||
border-radius: 24rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 6rpx;
|
||
flex-shrink: 0;
|
||
|
||
&.up {
|
||
background: $up;
|
||
}
|
||
|
||
&.down {
|
||
background: $down;
|
||
}
|
||
}
|
||
|
||
&__badge-arrow {
|
||
font-size: 18rpx;
|
||
color: #ffffff;
|
||
}
|
||
|
||
&__badge-text {
|
||
font-size: 28rpx;
|
||
font-weight: 600;
|
||
color: #ffffff;
|
||
}
|
||
}
|
||
</style>
|