| | |
| | | close-icon-position="top-right"> |
| | | <div class="symbol-modal"> |
| | | <!-- Modal Tabs:同一 activeTab 的 label 与 value 统一配置 --> |
| | | <div class="modal-tabs"> |
| | | <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="activeTab = tab.value"> |
| | | :class="{ active: activeTab === tab.value }" @click="switchTab(tab.value)"> |
| | | {{ $t(tab.label) }} |
| | | </div> |
| | | </div> |
| | |
| | | @click="selectSymbol(item)"> |
| | | <div class="symbol-left"> |
| | | <div class="symbol-info"> |
| | | <div class="symbol-name">{{ (item.symbol).toUpperCase() }}</div> |
| | | <div class="symbol-name">{{ getSymbolDisplayName(item) }}</div> |
| | | </div> |
| | | </div> |
| | | <div class="symbol-change" :class="item.change_ratio >= 0 ? 'up' : 'down'"> |
| | |
| | | import OptionsContract from './components/OptionsContract.vue' |
| | | import { _getHomeList } from '@/service/home.api' |
| | | import { _getRealtimeByType, _isItemHasAddGlobal } from '@/service/quotes.api' |
| | | import { _listItemsById, _itemUserOptionalList, _getQuotes } from '@/service/quotes.api' |
| | | import { _collect, _deleteCollect } from '@/service/cryptos.api' |
| | | import { useUserStore } from '@/store/user' |
| | | import { useQuotesStore } from '@/store/quotes.store' |
| | | import { OPCIONA_LIST } from '@/store/types.store' |
| | | import { HOST_URL } from '@/config' |
| | | |
| | | const route = useRoute() |
| | | const { t } = useI18n() |
| | | const useStore = useUserStore() |
| | | const quotesStore = useQuotesStore() |
| | | |
| | | const tradeType = ref('options') // 'options' 期权 / 'contract' 合约,默认期权交易 |
| | | const currentSymbol = ref('btc') |
| | |
| | | const symbolListRef = ref(null) |
| | | const symbolInitialLoading = ref(false) // tab 切换时首屏请求中,避免 @load 重复请求 pageNo=1 |
| | | const SYMBOL_PAGE_SIZE = 10 |
| | | const TAB_TYPES = ['cryptos', 'US-stocks', 'indices', 'forex'] |
| | | const DEFAULT_SYMBOL = { |
| | | cryptos: 'btc', |
| | | 'US-stocks': 'AAPL', |
| | | indices: 'GlobalETF500', |
| | | forex: 'EURUSD', |
| | | } |
| | | |
| | | // 允许的 activeTab 值(与 modalTabs.value 一致) |
| | | const VALID_ACTIVE_TABS = ['optional', 'forex', 'cryptos', 'US-stocks', 'indices'] |
| | | |
| | | // 从路由 path(params) 或 query 同步 symbol 与 activeTab |
| | | // 从路由 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 ?? q.tab ?? p.activeTab ?? p.tab |
| | | 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() |
| | | } |
| | | if (tab != null && VALID_ACTIVE_TABS.includes(String(tab))) { |
| | | activeTab.value = tab |
| | | } |
| | | } |
| | | |
| | | // 弹窗 Tab:label 为多语言 key,value 与接口类型统一(optional 仅自选列表用) |
| | | const modalTabs = [ |
| | | // { label: '自选', value: 'optional' }, |
| | | { label: '外汇', value: 'forex' }, |
| | | { label: '加密货币', value: 'cryptos' }, |
| | | { label: '股票', value: 'US-stocks' }, |
| | | { label: 'ETF', value: 'indices' } |
| | | { label: 'ETF', value: 'indices' }, |
| | | { label: '外汇', value: 'forex' }, |
| | | ] |
| | | |
| | | // 头部显示:关联选择项的 name,无则用 symbol |
| | |
| | | |
| | | // 嵌入合约:1=永续(合约交易),2=交割(期权交易) |
| | | const embedSelectIndex = computed(() => tradeType.value === 'contract' ? 1 : 2) |
| | | // 嵌入合约品种类型(value 已与接口类型统一,仅 optional 需映射) |
| | | const embedType = computed(() => { |
| | | return activeTab.value === 'optional' ? 'forex' : activeTab.value |
| | | }) |
| | | // 嵌入合约品种类型 |
| | | const embedType = computed(() => activeTab.value) |
| | | |
| | | 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 { |
| | | symbol: item.symbol, |
| | | name: item.enName || item.name || item.symbol, |
| | | close: item.close ?? item.lastPrice ?? '--', |
| | | change_ratio: item.changeRatio ?? item.change_ratio ?? 0, |
| | | } |
| | | } |
| | | |
| | | function switchTab(tab) { |
| | | if (activeTab.value === tab) return |
| | | activeTab.value = tab |
| | | currentSymbol.value = DEFAULT_SYMBOL[tab] || 'btc' |
| | | fetchData() |
| | | } |
| | | |
| | | // 图标路径 |
| | | const starIcon = new URL('@/assets/image/icon-star.png', import.meta.url).href |
| | |
| | | return `${HOST_URL}/symbol/${baseSymbol.toLowerCase()}.png` |
| | | } |
| | | |
| | | // 获取自选数据 |
| | | const fetchOptionalData = async () => { |
| | | try { |
| | | if (!useStore.userInfo.token) { |
| | | symbolList.value = [] |
| | | return |
| | | } |
| | | |
| | | const listData = await _itemUserOptionalList() |
| | | if (listData && listData.list && listData.list.length > 0) { |
| | | const firstList = listData.list[0] |
| | | const itemsData = await _listItemsById({ id: firstList.listId }) |
| | | |
| | | if (itemsData && Array.isArray(itemsData)) { |
| | | const symbols = itemsData.map(item => ({ symbol: item.symbol })) |
| | | quotesStore[OPCIONA_LIST](symbols) |
| | | |
| | | if (symbols.length > 0) { |
| | | const symbolStr = symbols.map(s => s.symbol).join(',') |
| | | const data = await _getHomeList(symbolStr) |
| | | if (data && Array.isArray(data)) { |
| | | symbolList.value = data |
| | | } else { |
| | | symbolList.value = [] |
| | | } |
| | | } else { |
| | | symbolList.value = [] |
| | | } |
| | | } else { |
| | | symbolList.value = [] |
| | | } |
| | | } else { |
| | | symbolList.value = [] |
| | | } |
| | | } catch (error) { |
| | | console.error('获取自选数据失败:', error) |
| | | symbolList.value = [] |
| | | } |
| | | } |
| | | |
| | | // 获取交易数据(activeTab.value 已与接口 type 统一);pageNo 页码,append 是否追加 |
| | | // 获取交易数据;pageNo 页码,append 是否追加 |
| | | const fetchTradingData = async (pageNo = 1, append = false) => { |
| | | const type = activeTab.value === 'optional' ? 'forex' : activeTab.value |
| | | const category = type === 'forex' ? 'forex' : null |
| | | |
| | | try { |
| | | const type = activeTab.value |
| | | const params = { |
| | | type: type, |
| | | type, |
| | | pageNo: pageNo, |
| | | pageSize: SYMBOL_PAGE_SIZE |
| | | } |
| | | if (category) { |
| | | params.category = category |
| | | } |
| | | |
| | | const data = await _getRealtimeByType(params) |
| | | |
| | | if (data && Array.isArray(data)) { |
| | | const symbols = data.map(item => item.symbol).join(',') |
| | | let list = [] |
| | | if (symbols) { |
| | | const homeData = await _getHomeList(symbols) |
| | | if (homeData && Array.isArray(homeData)) { |
| | | if (append) { |
| | | symbolList.value = [...symbolList.value, ...homeData] |
| | | } else { |
| | | symbolList.value = homeData |
| | | } |
| | | } else if (!append) { |
| | | symbolList.value = [] |
| | | if (homeData && Array.isArray(homeData) && homeData.length) { |
| | | list = homeData |
| | | } else { |
| | | list = data.map(mapSymbolItem) |
| | | } |
| | | } else if (!append) { |
| | | symbolList.value = [] |
| | | } else { |
| | | list = data.map(mapSymbolItem) |
| | | } |
| | | if (append) { |
| | | symbolList.value = [...symbolList.value, ...list] |
| | | } else { |
| | | symbolList.value = list |
| | | } |
| | | if (data.length <= 0) { |
| | | symbolFinished.value = true |
| | |
| | | |
| | | // 上拉触底加载更多(仅非自选 tab 分页,pageSize=10) |
| | | const loadMoreSymbols = async () => { |
| | | if (activeTab.value === 'optional') { |
| | | symbolFinished.value = true |
| | | symbolLoading.value = false |
| | | return |
| | | } |
| | | if (symbolInitialLoading.value) { |
| | | symbolLoading.value = false |
| | | return |
| | |
| | | symbolFinished.value = false |
| | | symbolInitialLoading.value = true |
| | | try { |
| | | if (activeTab.value === 'optional') { |
| | | await fetchOptionalData() |
| | | symbolFinished.value = true |
| | | } else { |
| | | await fetchTradingData(1, false) |
| | | symbolPage.value = 2 |
| | | } |
| | | await fetchTradingData(1, false) |
| | | symbolPage.value = 2 |
| | | // tab 切换后列表滚动回顶部 |
| | | await nextTick() |
| | | if (symbolListRef.value) symbolListRef.value.scrollTop = 0 |
| | |
| | | const keyword = searchKeyword.value.toLowerCase() |
| | | return symbolList.value.filter(item => |
| | | item.symbol.toLowerCase().includes(keyword) || |
| | | (item.name && item.name.toLowerCase().includes(keyword)) |
| | | (item.name && item.name.toLowerCase().includes(keyword)) || |
| | | (item.enName && item.enName.toLowerCase().includes(keyword)) |
| | | ) |
| | | }) |
| | | |
| | |
| | | showSymbolModal.value = false |
| | | searchKeyword.value = '' |
| | | } |
| | | |
| | | // 查询当前交易对是否已收藏(与 add-currency / trade-head 一致) |
| | | function checkFavorite() { |
| | | if (!useStore.userInfo?.token || !currentSymbol.value) { |
| | | isFavorite.value = false |
| | |
| | | } |
| | | } |
| | | |
| | | // 监听 tab 切换 |
| | | watch(activeTab, () => { |
| | | fetchData() |
| | | }) |
| | | |
| | | // 当前交易对变化时刷新收藏状态 |
| | | watch(currentSymbol, () => { |
| | | checkFavorite() |
| | | }) |
| | | |
| | | // 监听路由 query 变化(同一页带不同 query 时同步) |
| | | watch(() => route.query, () => applyFromRoute(), { deep: true }) |
| | | watch(() => route.query, () => { |
| | | applyFromRoute() |
| | | fetchData() |
| | | }, { deep: true }) |
| | | |
| | | // 组件挂载时:先从 path/query 取 symbol、activeTab,再拉数据并刷新收藏状态 |
| | | onMounted(() => { |