<template>
|
<div class="options-trade-page">
|
<!-- Header -->
|
<div class="trade-header">
|
<div class="header-tabs">
|
<button class="tab-btn" :class="{ active: tradeType === 'options' }" @click="tradeType = 'options'">
|
{{ $t('期权交易') }}
|
</button>
|
<button class="tab-btn" :class="{ active: tradeType === 'contract' }" @click="tradeType = 'contract'">
|
{{ $t('合约交易') }}
|
</button>
|
</div>
|
|
<div class="symbol-row">
|
<div class="symbol-selector" @click="showSymbolModal = true">
|
<span class="symbol-text">{{ currentSymbolName }}</span>
|
<img src="@/assets/image/icon-more.png" alt="arrow" class="down-icon" />
|
</div>
|
<!-- <div class="favorite-icon" @click="toggleFavorite">
|
<img :src="isFavorite ? starActiveIcon : starIcon" alt="favorite" />
|
</div> -->
|
</div>
|
</div>
|
|
<!-- Main Content -->
|
<div class="trade-content">
|
<OptionsContract v-if="currentSymbol" :symbol="currentSymbol" :symbol-display-name="currentSymbolName"
|
:select-index="embedSelectIndex" :type="embedType" @update-symbol="currentSymbol = $event" />
|
</div>
|
|
<!-- Symbol Selection Modal -->
|
<van-popup v-model:show="showSymbolModal" round position="bottom" :style="{ height: '80%' }" closeable
|
close-icon-position="top-right">
|
<div class="symbol-modal">
|
<!-- Modal Tabs:同一 activeTab 的 label 与 value 统一配置 -->
|
<div v-if="modalTabs.length > 1" class="modal-tabs">
|
<div class="tab-item" v-for="tab in modalTabs" :key="tab.value"
|
:class="{ active: activeTab === tab.value }" @click="switchTab(tab.value)">
|
{{ $t(tab.label) }}
|
</div>
|
</div>
|
|
<!-- 搜索栏:股票/ETF/外汇走 item!list.action;加密货币本地过滤 -->
|
<div class="search-bar">
|
<van-icon name="search" size="20" class="search-icon" />
|
<input type="text" :placeholder="$t('搜索交易种类')" v-model="searchKeyword"
|
class="search-input" @input="handleSearchInput" />
|
</div>
|
|
<!-- 品种列表:搜索模式用普通列表(避免 van-list 不刷新);无搜索词时用 van-list 分页 -->
|
<div ref="symbolListRef" class="symbol-list">
|
<!-- 搜索模式 -->
|
<template v-if="isSearchMode">
|
<div v-if="usesApiSearch && searchLoading" class="search-loading-tip">
|
{{ $t('加载中') || '加载中...' }}
|
</div>
|
<div class="symbol-item" v-for="(item, index) in displayList"
|
:key="`${listRenderKey}-${item.symbol}-${index}`" @click="selectSymbol(item)">
|
<div class="symbol-left">
|
<div class="symbol-info">
|
<div class="symbol-name">{{ getSymbolDisplayName(item) }}</div>
|
</div>
|
</div>
|
<div class="symbol-change" :class="getChangeRatio(item) >= 0 ? 'up' : 'down'">
|
{{ getChangeRatio(item) >= 0 ? '+' : '' }}{{ getChangeRatio(item) }}%
|
</div>
|
<div class="symbol-price">{{ item.close }}</div>
|
</div>
|
<div v-if="!searchLoading && !displayList.length" class="search-empty-tip">
|
{{ $t('暂无数据') || '暂无数据' }}
|
</div>
|
</template>
|
<!-- 浏览模式:publicRealtimeByType 分页加载 -->
|
<van-list v-else v-model:loading="symbolLoading" :finished="symbolFinished"
|
:immediate-check="false" :scroll-target="symbolListRef"
|
:finished-text="$t('没有更多了') || '没有更多了'" @load="loadMoreSymbols">
|
<div class="symbol-item" v-for="(item, index) in displayList"
|
:key="`${item.symbol}-${index}`" @click="selectSymbol(item)">
|
<div class="symbol-left">
|
<div class="symbol-info">
|
<div class="symbol-name">{{ getSymbolDisplayName(item) }}</div>
|
</div>
|
</div>
|
<div class="symbol-change" :class="getChangeRatio(item) >= 0 ? 'up' : 'down'">
|
{{ getChangeRatio(item) >= 0 ? '+' : '' }}{{ getChangeRatio(item) }}%
|
</div>
|
<div class="symbol-price">{{ item.close }}</div>
|
</div>
|
</van-list>
|
</div>
|
</div>
|
</van-popup>
|
</div>
|
</template>
|
|
<script setup>
|
import { ref, computed, onMounted, onBeforeUnmount, watch, nextTick } from 'vue'
|
import { useRoute } from 'vue-router'
|
import { useI18n } from 'vue-i18n'
|
import { Popup, Icon, showToast, List as VanList } from 'vant'
|
import OptionsContract from './components/OptionsContract.vue'
|
import { _getHomeList } from '@/service/home.api'
|
import { _getRealtimeByType, _isItemHasAddGlobal } from '@/service/quotes.api'
|
import { _collect, _deleteCollect, _getCoins } from '@/service/cryptos.api'
|
import { useUserStore } from '@/store/user'
|
import { HOST_URL } from '@/config'
|
|
const route = useRoute()
|
const { t } = useI18n()
|
const useStore = useUserStore()
|
|
// ---------- 页面状态 ----------
|
const tradeType = ref('options') // 顶部 Tab:options=期权(交割) / contract=合约(永续)
|
const currentSymbol = ref('btc') // 当前交易品种
|
const isFavorite = ref(false)
|
const showSymbolModal = ref(false)
|
const activeTab = ref('cryptos') // 弹窗品种类型 Tab
|
|
// ---------- 搜索相关 ----------
|
const searchKeyword = ref('') // 搜索关键词
|
const searchResultList = ref([]) // 股票/ETF/外汇 API 搜索结果
|
const searchLoading = ref(false)
|
const listRenderKey = ref(0) // 列表强制刷新 key,避免搜索后 DOM 不更新
|
let searchDebounceTimer = null // 搜索防抖定时器
|
let searchRequestId = 0 // 请求序号,丢弃过期的搜索响应
|
|
// ---------- 品种列表分页 ----------
|
const symbolList = ref([]) // 默认列表(publicRealtimeByType)
|
const symbolPage = ref(1)
|
const symbolLoading = ref(false)
|
const symbolFinished = ref(false)
|
const symbolListRef = ref(null)
|
const symbolInitialLoading = ref(false) // tab 切换首屏加载中,防止 van-list @load 重复请求
|
|
// ---------- 常量配置 ----------
|
const SYMBOL_PAGE_SIZE = 10
|
const SEARCH_DEBOUNCE_MS = 300 // 搜索防抖间隔(ms)
|
const QUOTE_CHUNK_SIZE = 30 // hobi!getRealtime 单次请求 symbol 数量上限,避免 URL 过长只返回部分
|
const API_SEARCH_TYPES = ['cryptos', 'US-stocks', 'indices', 'forex'] // 走 item!list.action 搜索的类型
|
const TAB_TYPES = ['cryptos', 'US-stocks', 'indices', 'forex']
|
const DEFAULT_SYMBOL = {
|
cryptos: 'btc',
|
'US-stocks': 'AAPL',
|
indices: 'GlobalETF500',
|
forex: 'EURUSD',
|
} // 切换 Tab 时的默认品种
|
|
// 从路由 path(params) 或 query 同步 symbol、activeTab
|
function applyFromRoute() {
|
const p = route.params || {}
|
const q = route.query || {}
|
const symbol = q.symbol ?? p.symbol
|
const tab = q.activeTab ?? p.activeTab
|
if (tab && TAB_TYPES.includes(String(tab))) {
|
activeTab.value = String(tab)
|
}
|
if (symbol != null && String(symbol).trim()) {
|
currentSymbol.value = String(symbol).trim()
|
}
|
}
|
|
// 弹窗品种 Tab 配置(label 用于 i18n,value 对应接口 type)
|
const modalTabs = [
|
{ label: '加密货币', value: 'cryptos' },
|
{ label: '股票', value: 'US-stocks' },
|
{ label: 'ETF', value: 'indices' },
|
{ label: '外汇', value: 'forex' },
|
]
|
|
// 头部显示:关联选择项的 name,无则用 symbol
|
const currentSymbolName = computed(() => {
|
const sym = currentSymbol.value
|
if (!sym) return ''
|
const item = symbolList.value.find(i => (i.symbol || '').toLowerCase() === sym.toLowerCase())
|
return (item && item.name) ? item.name : sym
|
})
|
|
// 嵌入合约:1=永续(合约交易),2=交割(期权交易)
|
const embedSelectIndex = computed(() => tradeType.value === 'contract' ? 1 : 2)
|
// 嵌入合约品种类型
|
// 传给 OptionsContract 的品种类型,切换 Tab 时子组件会 SET_COIN_LIST → item!list.action
|
const embedType = computed(() => activeTab.value)
|
// 当前 Tab 是否走远程搜索(股票/ETF/外汇)
|
const usesApiSearch = computed(() => API_SEARCH_TYPES.includes(activeTab.value))
|
// 是否处于搜索模式(有搜索词即 true,切换为普通列表渲染)
|
const isSearchMode = computed(() => !!searchKeyword.value.trim())
|
|
/** 统一涨跌幅字段(接口可能返回 change_ratio 或 changeRatio) */
|
function getChangeRatio(item) {
|
return item?.change_ratio ?? item?.changeRatio ?? 0
|
}
|
|
/** 将接口数据规范为列表展示结构 */
|
function normalizeListItem(item) {
|
return {
|
symbol: item.symbol,
|
name: item.enName || item.name || item.symbol,
|
close: item.close ?? item.lastPrice ?? '--',
|
change_ratio: getChangeRatio(item),
|
type: item.type,
|
}
|
}
|
|
function getSymbolDisplayName(item) {
|
if (!item) return ''
|
const name = item.name || item.enName || item.symbol || ''
|
if (activeTab.value === 'forex') return String(name).toUpperCase()
|
if (activeTab.value === 'cryptos') return String(item.symbol || name).toUpperCase()
|
return name
|
}
|
|
function mapSymbolItem(item) {
|
return normalizeListItem(item)
|
}
|
|
/** 分批拉取行情,避免 symbol 过多时 getRealtime 只返回子集 */
|
async function fetchQuotesBySymbols(symbols) {
|
const uniqueSymbols = [...new Set((symbols || []).filter(Boolean))]
|
if (!uniqueSymbols.length) return []
|
const chunks = []
|
for (let i = 0; i < uniqueSymbols.length; i += QUOTE_CHUNK_SIZE) {
|
chunks.push(uniqueSymbols.slice(i, i + QUOTE_CHUNK_SIZE).join(','))
|
}
|
const results = await Promise.all(
|
chunks.map(chunk => _getHomeList(chunk).catch(() => []))
|
)
|
return results.flat().filter(item => item && item.symbol)
|
}
|
|
/**
|
* 以 item!list / publicRealtimeByType 的完整列表为准,用行情数据补全价格与涨跌幅
|
* 不能直接用 homeData 替换,否则无行情的品种会被丢弃
|
*/
|
function mergeItemsWithQuotes(rawItems, quoteItems) {
|
const quoteMap = new Map()
|
;(quoteItems || []).forEach(item => {
|
if (item?.symbol) {
|
quoteMap.set(String(item.symbol).toLowerCase(), item)
|
}
|
})
|
return (rawItems || []).map(raw => {
|
const quote = quoteMap.get(String(raw.symbol || '').toLowerCase())
|
return normalizeListItem(quote ? { ...raw, ...quote } : raw)
|
})
|
}
|
|
/** 递增 listRenderKey,强制列表重新渲染 */
|
function bumpListRenderKey() {
|
listRenderKey.value += 1
|
}
|
|
/** 清空搜索状态(切换 Tab、选中品种、关闭弹窗时调用) */
|
function clearSearchState() {
|
searchKeyword.value = ''
|
searchResultList.value = []
|
searchLoading.value = false
|
bumpListRenderKey()
|
if (searchDebounceTimer) {
|
clearTimeout(searchDebounceTimer)
|
searchDebounceTimer = null
|
}
|
}
|
|
/**
|
* 切换弹窗品种 Tab
|
* 1. 更新 activeTab、默认品种
|
* 2. 清空搜索
|
* 3. 重新拉取 publicRealtimeByType 第一页
|
*/
|
function switchTab(tab) {
|
if (activeTab.value === tab) return
|
activeTab.value = tab
|
currentSymbol.value = DEFAULT_SYMBOL[tab] || 'btc'
|
clearSearchState()
|
fetchData()
|
}
|
|
// 图标路径
|
const starIcon = new URL('@/assets/image/icon-star.png', import.meta.url).href
|
const starActiveIcon = new URL('@/assets/image/icon-star_active.png', import.meta.url).href
|
|
// 获取交易对图标
|
const getSymbolIcon = (symbol) => {
|
if (!symbol) return ''
|
const baseSymbol = symbol.split('/')[0] || symbol.split('USDT')[0] || symbol
|
return `${HOST_URL}/symbol/${baseSymbol.toLowerCase()}.png`
|
}
|
|
/**
|
* 拉取品种列表(浏览模式)
|
* 接口:publicRealtimeByType → hobi!getRealtime.action 补全行情
|
*/
|
const fetchTradingData = async (pageNo = 1, append = false) => {
|
try {
|
const type = activeTab.value
|
const params = {
|
type,
|
pageNo: pageNo,
|
pageSize: SYMBOL_PAGE_SIZE
|
}
|
|
const data = await _getRealtimeByType(params)
|
|
if (data && Array.isArray(data)) {
|
const symbols = data.map(item => item.symbol).filter(Boolean)
|
let list = data.map(mapSymbolItem)
|
if (symbols.length) {
|
const homeData = await fetchQuotesBySymbols(symbols)
|
if (homeData.length) {
|
list = mergeItemsWithQuotes(data, homeData)
|
}
|
}
|
if (append) {
|
symbolList.value = [...symbolList.value, ...list]
|
} else {
|
symbolList.value = list
|
}
|
if (data.length <= 0) {
|
symbolFinished.value = true
|
}
|
} else {
|
if (!append) symbolList.value = []
|
symbolFinished.value = true
|
}
|
} catch (error) {
|
console.error('获取交易数据失败:', error)
|
if (!append) symbolList.value = []
|
symbolFinished.value = true
|
}
|
}
|
|
// 上拉触底加载更多(仅浏览模式;搜索模式 item!list 一次返回全部)
|
const loadMoreSymbols = async () => {
|
if (symbolInitialLoading.value || searchKeyword.value.trim()) {
|
symbolLoading.value = false
|
symbolFinished.value = true
|
return
|
}
|
try {
|
await fetchTradingData(symbolPage.value, true)
|
symbolPage.value += 1
|
} finally {
|
symbolLoading.value = false
|
}
|
}
|
|
/** Tab 切换或首次进入时重置分页并拉取第一页 */
|
const fetchData = async () => {
|
symbolPage.value = 1
|
symbolFinished.value = false
|
symbolInitialLoading.value = true
|
try {
|
await fetchTradingData(1, false)
|
symbolPage.value = 2
|
// tab 切换后列表滚动回顶部
|
await nextTick()
|
if (symbolListRef.value) symbolListRef.value.scrollTop = 0
|
} finally {
|
symbolInitialLoading.value = false
|
}
|
}
|
|
/**
|
* 远程搜索(股票/ETF/外汇)
|
* 接口:item!list.action(_getCoins)
|
* 1. 优先 { type, name } 查询
|
* 2. 无结果则 { name } 查询后按 type 过滤
|
* 3. 再用 hobi!getRealtime.action 补全价格与涨跌幅
|
*/
|
async function fetchSearchList(keyword) {
|
const type = activeTab.value
|
const requestId = ++searchRequestId
|
searchLoading.value = true
|
try {
|
let raw = []
|
try {
|
const res = await _getCoins({ type, name: keyword.trim() })
|
raw = Array.isArray(res) ? res : []
|
} catch (e) {
|
raw = []
|
}
|
if (!raw.length) {
|
const res = await _getCoins({ name: keyword.trim() })
|
raw = (Array.isArray(res) ? res : []).filter(item => item.type === type)
|
}
|
if (requestId !== searchRequestId) return // 已有更新的搜索请求,丢弃本次结果
|
if (!raw.length) {
|
searchResultList.value = []
|
bumpListRenderKey()
|
return
|
}
|
if (requestId !== searchRequestId) return // 已有更新的搜索请求,丢弃本次结果
|
const symbols = raw.map(item => item.symbol).filter(Boolean)
|
let list = raw.map(mapSymbolItem)
|
if (symbols.length) {
|
const homeData = await fetchQuotesBySymbols(symbols)
|
if (requestId !== searchRequestId) return // 已有更新的搜索请求,丢弃本次结果
|
if (homeData.length) {
|
list = mergeItemsWithQuotes(raw, homeData)
|
}
|
}
|
searchResultList.value = list
|
bumpListRenderKey()
|
} catch (error) {
|
if (requestId !== searchRequestId) return // 已有更新的搜索请求,丢弃本次结果
|
console.error('搜索交易品种失败:', error)
|
searchResultList.value = []
|
bumpListRenderKey()
|
} finally {
|
if (requestId === searchRequestId) {
|
searchLoading.value = false
|
}
|
}
|
}
|
|
/** 搜索框 input 事件,与 watch(searchKeyword) 配合触发防抖搜索 */
|
function handleSearchInput(e) {
|
const val = e?.target?.value ?? searchKeyword.value
|
scheduleSearch(val)
|
}
|
|
/**
|
* 调度搜索(300ms 防抖)
|
* 所有 Tab 均走 item!list.action;行情仅用于补全价格,不裁剪列表
|
*/
|
function scheduleSearch(keyword) {
|
if (searchDebounceTimer) {
|
clearTimeout(searchDebounceTimer)
|
searchDebounceTimer = null
|
}
|
bumpListRenderKey()
|
const trimmed = (keyword ?? '').trim()
|
if (!trimmed) {
|
searchRequestId += 1
|
searchResultList.value = []
|
searchLoading.value = false
|
symbolFinished.value = false
|
return
|
}
|
searchResultList.value = []
|
searchLoading.value = true
|
// 搜索模式不用 van-list 分页,item!list 一次返回全部匹配项
|
symbolFinished.value = true
|
searchDebounceTimer = setTimeout(() => {
|
searchDebounceTimer = null
|
fetchSearchList(trimmed)
|
}, SEARCH_DEBOUNCE_MS)
|
}
|
|
/**
|
* 最终展示列表
|
* - 无关键词:symbolList(publicRealtimeByType 分页浏览,支持上拉加载)
|
* - 有关键词:searchResultList(item!list.action 全量返回,容器内滚动)
|
*/
|
const displayList = computed(() => {
|
const keyword = searchKeyword.value.trim()
|
if (!keyword) {
|
return symbolList.value
|
}
|
return searchResultList.value
|
})
|
|
// 选择交易对
|
const selectSymbol = (item) => {
|
currentSymbol.value = item.symbol
|
showSymbolModal.value = false
|
clearSearchState()
|
}
|
function checkFavorite() {
|
if (!useStore.userInfo?.token || !currentSymbol.value) {
|
isFavorite.value = false
|
return
|
}
|
_isItemHasAddGlobal({ symbol: currentSymbol.value }).then((data) => {
|
isFavorite.value = !!data
|
}).catch(() => {
|
isFavorite.value = false
|
})
|
}
|
|
// 切换收藏:与 add-currency 一致,使用 _collect / _deleteCollect(默认自选列表)
|
async function toggleFavorite() {
|
if (!currentSymbol.value) return
|
if (!useStore.userInfo?.token) {
|
showToast(t('请先登录'))
|
return
|
}
|
try {
|
if (isFavorite.value) {
|
await _deleteCollect(currentSymbol.value)
|
isFavorite.value = false
|
showToast(t('successfullyDeleted'))
|
} else {
|
await _collect(currentSymbol.value)
|
isFavorite.value = true
|
showToast(t('添加成功'))
|
}
|
} catch (e) {
|
showToast(e?.msg || t('操作失败'))
|
}
|
}
|
|
// 当前交易对变化时刷新收藏状态
|
watch(currentSymbol, () => {
|
checkFavorite()
|
})
|
|
watch(searchKeyword, (val) => {
|
scheduleSearch(val)
|
})
|
|
// 路由 query 变化时同步 symbol、activeTab 并刷新列表
|
watch(() => route.query, () => {
|
applyFromRoute()
|
fetchData()
|
}, { deep: true })
|
|
// 组件挂载时:先从 path/query 取 symbol、activeTab,再拉数据并刷新收藏状态
|
onMounted(() => {
|
applyFromRoute()
|
fetchData()
|
checkFavorite()
|
})
|
|
onBeforeUnmount(() => {
|
// 组件卸载时清除搜索防抖定时器
|
if (searchDebounceTimer) {
|
clearTimeout(searchDebounceTimer)
|
searchDebounceTimer = null
|
}
|
})
|
</script>
|
|
<style lang="scss" scoped>
|
.options-trade-page {
|
min-height: 100vh;
|
background: #fff;
|
}
|
|
/* Header */
|
.trade-header {
|
padding: 2rem;
|
background: #fff;
|
border-bottom: 0.1rem solid #f0f0f0;
|
|
.header-tabs {
|
display: flex;
|
background: #fff;
|
border: 0.1rem solid #e0e0e0;
|
border-radius: 1.2rem;
|
padding: 0.4rem;
|
gap: 0.2rem;
|
|
.tab-btn {
|
flex: 1;
|
padding: 1rem 2rem;
|
font-size: 2rem;
|
border: none;
|
border-radius: 0.8rem;
|
background: transparent;
|
color: $text_color;
|
cursor: pointer;
|
transition: all 0.3s ease;
|
|
&.active {
|
background: #0a6bfa;
|
color: #fff;
|
font-weight: 600;
|
}
|
}
|
}
|
|
.symbol-row {
|
display: flex;
|
align-items: center;
|
justify-content: space-between;
|
gap: 1rem;
|
margin-top: 2.5rem;
|
}
|
|
.symbol-selector {
|
display: flex;
|
align-items: center;
|
gap: 0.5rem;
|
cursor: pointer;
|
|
.symbol-text {
|
font-size: 4rem;
|
color: $text_color;
|
}
|
|
.down-icon {
|
width: 2rem;
|
margin-left: .5rem;
|
}
|
}
|
|
.favorite-icon {
|
width: 2.4rem;
|
height: 2.4rem;
|
flex-shrink: 0;
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
cursor: pointer;
|
|
img {
|
width: 100%;
|
height: 100%;
|
}
|
}
|
}
|
|
/* Trade Content */
|
.trade-content {
|
padding: 2rem;
|
}
|
|
/* Symbol Modal */
|
.symbol-modal {
|
padding: 2rem 0;
|
height: 100%;
|
display: flex;
|
flex-direction: column;
|
|
/* 与 Market.vue 的 market-tabs 样式一致 */
|
.modal-tabs {
|
display: flex;
|
border: 0.1rem solid #e0e0e0;
|
background: #fff;
|
border-radius: 1rem;
|
padding: 0.3rem;
|
overflow-x: auto;
|
gap: 0.3rem;
|
margin-bottom: 2rem;
|
margin-top: 4rem;
|
margin: 4rem 2rem 2rem;
|
|
.tab-item {
|
font-size: 2.5rem;
|
padding: 1.5rem 0;
|
color: $text_color1;
|
white-space: nowrap;
|
cursor: pointer;
|
transition: all 0.3s ease;
|
border-radius: 0.8rem;
|
flex: 1;
|
text-align: center;
|
font-weight: 500;
|
|
&.active {
|
color: #fff;
|
background: #0a6bfa;
|
border-color: #0a6bfa;
|
font-weight: 600;
|
padding: 1.5rem 2rem;
|
}
|
}
|
}
|
|
.search-bar {
|
display: flex;
|
align-items: center;
|
gap: 1rem;
|
padding: 1.5rem;
|
background: #fff;
|
border-radius: 1rem;
|
margin: 0 2rem 2rem;
|
border: 1px solid #000;
|
|
.search-icon {
|
width: 2rem;
|
height: 2rem;
|
}
|
|
.search-input {
|
flex: 1;
|
border: none;
|
background: transparent;
|
font-size: 2.4rem;
|
color: $text_color;
|
outline: none;
|
|
&::placeholder {
|
color: $text_color1;
|
}
|
}
|
}
|
|
.symbol-list {
|
flex: 1;
|
overflow-y: auto;
|
padding: 2rem 2rem 0;
|
|
.search-loading-tip,
|
.search-empty-tip {
|
text-align: center;
|
font-size: 2rem;
|
color: $text_color1;
|
padding: 3rem 1rem;
|
}
|
|
.symbol-item {
|
display: flex;
|
align-items: center;
|
justify-content: space-between;
|
padding: 2rem 1rem;
|
background: #fff;
|
border-radius: 1.5rem;
|
border: 0.1rem solid #f0f0f0;
|
margin-bottom: 1rem;
|
cursor: pointer;
|
transition: background 0.2s ease;
|
box-shadow: 0rem .24rem 1.7rem 0rem rgba(0, 0, 0, .22);
|
font-size: 2rem;
|
|
&:hover {
|
background: #f9f9f9;
|
}
|
|
.symbol-left {
|
display: flex;
|
align-items: center;
|
gap: 1.5rem;
|
width: 33%;
|
|
.symbol-icon {
|
width: 4rem;
|
height: 4rem;
|
border-radius: 50%;
|
overflow: hidden;
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
background: #f5f5f5;
|
|
img {
|
width: 100%;
|
height: 100%;
|
object-fit: cover;
|
}
|
}
|
|
.symbol-info {
|
.symbol-name {
|
font-weight: 600;
|
color: $text_color;
|
}
|
}
|
}
|
|
|
.symbol-change {
|
font-weight: 600;
|
|
&.up {
|
color: $green;
|
}
|
|
&.down {
|
color: $red;
|
}
|
}
|
|
.symbol-price {
|
font-weight: 600;
|
color: $text_color;
|
width: 33%;
|
text-align: right;
|
}
|
}
|
}
|
}
|
</style>
|