浏览代码

添加raw页面为弹窗,菜单点击切换首条数据

liqingshan 1 月之前
父节点
当前提交
fa3e816de5

+ 102 - 0
code/src/App.vue

@@ -5,13 +5,17 @@ import TheHeader from '@/components/layout/TheHeader.vue'
 import TheFooter from '@/components/layout/TheFooter.vue'
 import AppToast from '@/components/common/AppToast.vue'
 import SystemAnnouncementModal from '@/components/common/SystemAnnouncementModal.vue'
+import RwaView from '@/views/RwaView.vue'
 import { useAuthStore } from '@/stores/auth'
 import { useSystemAnnouncementStore } from '@/stores/systemAnnouncement'
+import { useRwaModalStore } from '@/stores/rwaModal'
 import { registerSessionExpiredHandler } from '@/utils/request'
 
 const authStore = useAuthStore()
 const { isLoggedIn } = storeToRefs(authStore)
 const sysAnnStore = useSystemAnnouncementStore()
+const rwaModalStore = useRwaModalStore()
+const { showModal: showRwaModal } = storeToRefs(rwaModalStore)
 
 // 当后端返回 4000(token 过期)或 HTTP 401 时,清除本地登录态
 registerSessionExpiredHandler(() => authStore.clearLocalSession())
@@ -34,4 +38,102 @@ watch(isLoggedIn, (val) => {
   <TheFooter />
   <AppToast />
   <SystemAnnouncementModal />
+  <Teleport to="body">
+    <Transition name="modal-fade">
+      <div v-if="showRwaModal" class="rwa-modal-overlay" @click.self="rwaModalStore.close()">
+        <div class="rwa-modal-content">
+          <button class="rwa-modal-close" @click="rwaModalStore.close()">
+            <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
+              <line x1="18" y1="6" x2="6" y2="18"/>
+              <line x1="6" y1="6" x2="18" y2="18"/>
+            </svg>
+          </button>
+          <RwaView />
+        </div>
+      </div>
+    </Transition>
+  </Teleport>
 </template>
+
+<style>
+.modal-fade-enter-active,
+.modal-fade-leave-active {
+  transition: opacity 0.3s ease;
+}
+
+.modal-fade-enter-from,
+.modal-fade-leave-to {
+  opacity: 0;
+}
+
+.rwa-modal-overlay {
+  position: fixed;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  background: rgba(0, 0, 0, 0.75);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  z-index: 1000;
+  padding: 20px;
+}
+
+.rwa-modal-content {
+  position: relative;
+  background: #1a1a1a;
+  border-radius: 16px;
+  width: 60%;
+  min-height: 50vh;
+  max-height: 80vh;
+  overflow-y: auto;
+  border: 1px solid rgba(255, 255, 255, 0.1);
+  box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
+  display: flex;
+  flex-direction: column;
+}
+
+.rwa-modal-close {
+  position: absolute;
+  top: 16px;
+  right: 16px;
+  width: 36px;
+  height: 36px;
+  background: rgba(255, 255, 255, 0.08);
+  border: none;
+  border-radius: 50%;
+  color: rgba(255, 255, 255, 0.7);
+  cursor: pointer;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  transition: all 0.2s;
+  z-index: 10;
+}
+
+.rwa-modal-close:hover {
+  background: rgba(255, 255, 255, 0.15);
+  color: #fff;
+  transform: rotate(90deg);
+}
+
+.rwa-modal-content :deep(.rwa-view) {
+  min-height: auto;
+  padding: 48px 24px;
+}
+
+.rwa-modal-content :deep(.rwa-content) {
+  padding: 0;
+}
+
+.rwa-modal-content :deep(.rwa-title) {
+  font-size: 28px;
+  margin-bottom: 16px;
+}
+
+.rwa-modal-content :deep(.rwa-desc) {
+  font-size: 15px;
+  line-height: 1.6;
+}
+</style>

+ 33 - 8
code/src/components/layout/TheHeader.vue

@@ -2,6 +2,7 @@
 import { ref, computed, watch } from 'vue'
 import { storeToRefs } from 'pinia'
 import { useAuthStore } from '@/stores/auth'
+import { useRwaModalStore } from '@/stores/rwaModal'
 import { useBrokerEntry } from '@/composables/useBrokerEntry'
 import { useRoute, useRouter } from 'vue-router'
 import { useWindowScroll } from '@vueuse/core'
@@ -14,6 +15,7 @@ const { t } = useI18n()
 const route = useRoute()
 const router = useRouter()
 const authStore = useAuthStore()
+const rwaModalStore = useRwaModalStore()
 const { isLoggedIn, email, memberProfile } = storeToRefs(authStore)
 const { logout } = authStore
 const { y: scrollY } = useWindowScroll()
@@ -65,12 +67,13 @@ interface NavChild {
   key?: string
   label?: string
   to: string
-  icon?: 'ido' | 'gift'
+  icon?: 'ido' | 'gift' | ''
 }
 
 interface NavItem {
   key: string
   to?: string
+  action?: string
   children?: NavChild[]
 }
 
@@ -80,17 +83,17 @@ const navItems = computed<NavItem[]>(() => {
     { key: 'nav.claimAirdrop', to: '/finance/airdrop', icon: 'gift' },
   ]
   const futuresChildren: NavChild[] = [
-    { key: 'futuresPage.pairSearchTag.oneType', to: '/futures/BTC-USDT?filter=1' },
-    { key: 'futuresPage.pairSearchTag.twoType', to: '/futures/BTC-USDT?filter=2' },
-    { key: 'futuresPage.pairSearchTag.threeType', to: '/futures/BTC-USDT?filter=3' },
-    { key: 'futuresPage.pairSearchTag.fourType', to: '/futures/BTC-USDT?filter=4' },
+    { key: 'futuresPage.pairSearchTag.oneType', to: '/futures/BTC-USDT?filter=1', icon: '' },
+    { key: 'futuresPage.pairSearchTag.threeType', to: '/futures/BTC-USDT?filter=3', icon: '' },
+     { key: 'futuresPage.pairSearchTag.fiveType', to: '/futures/BTC-USDT?filter=2', icon: '' },
+    { key: 'futuresPage.pairSearchTag.sixType', to: '/futures/BTC-USDT?filter=4', icon: '' },
   ]
   return [
     { key: 'nav.home', to: '/' },
     { key: 'nav.market', to: '/market' },
     { key: 'nav.spot', to: '/spot/BTC-USDT' },
     { key: 'nav.futures', to: '/futures/BTC-USDT', children: futuresChildren },
-    { key: 'nav.rwa', to: '/rwa' },
+    { key: 'nav.rwa', action: 'openRwaModal' },
     { key: 'nav.copyTrade', to: '/copy-trade' },
     { key: 'nav.finance', children: financeChildren },
     { key: 'nav.announcements', to: '/announcements' },
@@ -134,6 +137,12 @@ function navigate(to: string) {
   router.push(to)
 }
 
+function handleRwaClick() {
+  menuOpen.value = false
+  mobileExpandedItem.value = null
+  rwaModalStore.open()
+}
+
 function toggleMobileExpand(key: string) {
   mobileExpandedItem.value = mobileExpandedItem.value === key ? null : key
 }
@@ -182,6 +191,14 @@ async function handleLogout() {
           >
             {{ t(item.key) }}
           </RouterLink>
+          <!-- RWA 特殊处理 -->
+          <span
+            v-else-if="item.action === 'openRwaModal'"
+            class="nav-item"
+            @click="handleRwaClick"
+          >
+            {{ t(item.key) }}
+          </span>
           <!-- 既有to又有子菜单(合约) -->
           <div v-else class="nav-item-wrap--both">
             <RouterLink
@@ -199,7 +216,7 @@ async function handleLogout() {
           <div v-if="item.children" class="dropdown-panel">
             <template v-for="(child, idx) in item.children" :key="child.to + (child.key ?? child.label ?? '')">
               <div
-                v-if="child.icon === 'gift' && idx > 0"
+                v-if="idx > 0"
                 class="dropdown-divider"
               />
               <RouterLink
@@ -295,9 +312,17 @@ async function handleLogout() {
       <div class="mobile-menu-body">
         <nav class="mobile-nav">
           <template v-for="item in navItems" :key="item.key">
+            <!-- RWA 特殊处理 -->
+            <button
+              v-if="item.action === 'openRwaModal'"
+              class="mobile-nav-item"
+              @click="handleRwaClick"
+            >
+              {{ t(item.key) }}
+            </button>
             <!-- 无子菜单 -->
             <button
-              v-if="item.to"
+              v-else-if="item.to"
               class="mobile-nav-item"
               :class="{ 'is-active': isActive(item) }"
               @click="navigate(item.to)"

+ 9 - 2
code/src/components/market/MarketRow.vue

@@ -4,7 +4,10 @@ import { useI18n } from 'vue-i18n'
 import { futuresPairToRouteParam, normalizeFuturesPairSymbol } from '@/utils/futuresSymbol'
 import type { MarketItem } from '@/types'
 
-const props = defineProps<{ item: MarketItem }>()
+const props = defineProps<{ 
+  item: MarketItem 
+  stockType?: number 
+}>()
 const { t } = useI18n()
 
 const router = useRouter()
@@ -14,7 +17,11 @@ function goDetail() {
     const sym = props.item.symbol.replace('/', '-')
     router.push({ name: 'spot', params: { symbol: sym } })
   } else {
-    router.push({ name: 'futures', params: { symbol: futuresPairToRouteParam(props.item.symbol) } })
+    router.push({ 
+      name: 'futures', 
+      params: { symbol: futuresPairToRouteParam(props.item.symbol) },
+      query: props.stockType ? { stockTypeTag: String(props.stockType) } : {}
+    })
   }
 }
 

+ 1 - 0
code/src/components/market/MarketTable.vue

@@ -169,6 +169,7 @@ const sortedList = computed(() => {
               v-for="item in sortedList"
               :key="`${item.symbol}-${item.type}`"
               :item="item"
+              :stock-type="currentSearchTag"
             />
           </template>
           <tr v-else>

+ 8 - 0
code/src/locales/futuresPage.i18n.ts

@@ -29,6 +29,8 @@ export type FuturesPageMessages = {
     twoType: string;
     threeType: string;
     fourType: string;
+    fiveType: string;
+    sixType: string;
   };
   pairEmpty: string;
   stats: {
@@ -1069,6 +1071,8 @@ function buildZhCN(): FuturesPageMessages {
       twoType: "股票",
       threeType: "大宗贵金属",
       fourType: "外汇",
+      fiveType: "股票(代币化)",
+      sixType: "外汇(代币化)",
     },
     pairEmpty: "无匹配结果",
     stats: {
@@ -1379,6 +1383,8 @@ function buildEn(): FuturesPageMessages {
       twoType: "Stock",
       threeType: "Commodity",
       fourType: "Forex",
+      fiveType: "Stock (Tokenized)",
+      sixType: "Forex (Tokenized)",
     },
     pairEmpty: "No matching pairs",
     stats: {
@@ -1689,6 +1695,8 @@ function buildZhTW(): FuturesPageMessages {
       twoType: "股票",
       threeType: "大宗貴金屬",
       fourType: "外匯",
+      fiveType: "股票(代幣化)",
+      sixType: "外匯(代幣化)",
     },
     pairEmpty: "無符合結果",
     stats: {

+ 2 - 0
code/src/locales/futuresPage/overlays/de.ts

@@ -9,6 +9,8 @@ export const deUiOverlay: DeepPartial<FuturesPageMessages> = {
     twoType: 'Aktien',
     threeType: 'Edelmetalle',
     fourType: 'Forex',
+    fiveType: 'Aktien (Tokenisiert)',
+    sixType: 'Forex (Tokenisiert)',
   },
   pairEmpty: 'Keine Treffer',
   stats: {

+ 2 - 0
code/src/locales/futuresPage/overlays/es.ts

@@ -10,6 +10,8 @@ export const esUiOverlay: DeepPartial<FuturesPageMessages> = {
     twoType: "Acciones",
     threeType: "Metales",
     fourType: "Forex",
+    fiveType: "Acciones (Tokeniz)",
+    sixType: "Forex (Tokenizado)",
   },
   pairEmpty: "Sin coincidencias",
   stats: {

+ 2 - 0
code/src/locales/futuresPage/overlays/fr.ts

@@ -9,6 +9,8 @@ export const frUiOverlay: DeepPartial<FuturesPageMessages> = {
     twoType: 'Actions',
     threeType: 'Métaux',
     fourType: 'Forex',
+    fiveType: 'Actions (Tokenisées)',
+    sixType: 'Forex (Tokenisées)',
   },
   pairEmpty: 'Aucun résultat',
   stats: {

+ 2 - 0
code/src/locales/futuresPage/overlays/hi.ts

@@ -10,6 +10,8 @@ export const hiUiOverlay: DeepPartial<FuturesPageMessages> = {
     twoType: "स्टॉक",
     threeType: "कीमती धातुएं",
     fourType: "विदेशी मुद्रा",
+    fiveType: "स्टॉक (करेंसी)",
+    sixType: "विदेशी मुद्रा (करेंसी)",
   },
   pairEmpty: "कोई पेयर नहीं",
   stats: {

+ 2 - 0
code/src/locales/futuresPage/overlays/id.ts

@@ -10,6 +10,8 @@ export const idUiOverlay: DeepPartial<FuturesPageMessages> = {
     twoType: "Saham",
     threeType: "Logam Mulia",
     fourType: "Forex",
+    fiveType: "Saham (Tokenis)",
+    sixType: "Forex (Tokenis)",
   },
   pairEmpty: "Tidak ada pasangan",
   stats: {

+ 2 - 0
code/src/locales/futuresPage/overlays/ja.ts

@@ -10,6 +10,8 @@ export const jaUiOverlay: DeepPartial<FuturesPageMessages> = {
     twoType: '株式',
     threeType: '貴金属',
     fourType: '外国為替',
+    fiveType: '株式 (トークン化)',
+    sixType: '外国為替 (トークン化)',
   },
   pairEmpty: '該当ペアなし',
   stats: {

+ 2 - 0
code/src/locales/futuresPage/overlays/ko.ts

@@ -10,6 +10,8 @@ export const koUiOverlay: DeepPartial<FuturesPageMessages> = {
     twoType: "주식",
     threeType: "귀금속",
     fourType: "외환",
+    fiveType: "주식 (토큰화폐)",
+    sixType: "외환 (토큰화폐)",
   },
   pairEmpty: "일치하는 페어 없음",
   stats: {

+ 2 - 0
code/src/locales/futuresPage/overlays/pt.ts

@@ -9,6 +9,8 @@ export const ptUiOverlay: DeepPartial<FuturesPageMessages> = {
     twoType: "Ações",
     threeType: "Metais",
     fourType: "Forex",
+    fiveType: "Ações (Tokenizadas)",
+    sixType: "Forex (Tokenizadas)",
   },
   pairEmpty: "Sem resultados",
   stats: {

+ 2 - 0
code/src/locales/futuresPage/overlays/ru.ts

@@ -9,6 +9,8 @@ export const ruUiOverlay: DeepPartial<FuturesPageMessages> = {
     twoType: "Акции",
     threeType: "Металлы",
     fourType: "Форекс",
+    fiveType: "Акции (Tokenizadas)",
+    sixType: "Форекс (Tokenizadas)",
   },
   pairEmpty: "Совпадений не найдено",
   stats: {

+ 2 - 0
code/src/locales/futuresPage/overlays/tr.ts

@@ -9,6 +9,8 @@ export const trUiOverlay: DeepPartial<FuturesPageMessages> = {
     twoType: "Hisse Senedi",
     threeType: "Madenler",
     fourType: "Foreks",
+    fiveType: "Hisse Senedi (Tokenizadas)",
+    sixType: "Foreks (Tokenizadas)",
   },
   pairEmpty: "Sonuç bulunamadı",
   stats: {

+ 2 - 0
code/src/locales/futuresPage/overlays/vi.ts

@@ -9,6 +9,8 @@ export const viUiOverlay: DeepPartial<FuturesPageMessages> = {
     twoType: "Cổ Phiếu",
     threeType: "Kim Loại Quý",
     fourType: "Ngoại Hối",
+    fiveType: "Cổ Phiếu (Tokenizadas)",
+    sixType: "Ngoại Hối (Tokenizadas)",
   },
   pairEmpty: "Không tìm thấy kết quả",
   stats: {

+ 51 - 21
code/src/stores/coins.ts

@@ -49,6 +49,15 @@ interface RawCoinInfo {
   coinScale: number;
   baseCoinScale: number;
   stockType: number;
+  // API 返回的 ticket 行情数据,字段与 WebSocket ticker 一致
+  ticket?: {
+    c: string; // close
+    o: string; // open
+    h: string; // high
+    l: string; // low
+    v: string; // volume
+    q: string; // turnover
+  };
 }
 
 // 内部维护的交易对对象
@@ -116,6 +125,21 @@ export const useCoinsStore = defineStore("coins", () => {
     return sym.toLowerCase().replace(/[/-]/g, "");
   }
 
+  /** 安全解析 ticket:支持对象或 JSON 字符串,内部字段为 null/undefined 时返回 "0" */
+  function safeTicket(raw: unknown): Record<string, string> | null {
+    if (!raw) return null;
+    let obj: any;
+    if (typeof raw === "string") {
+      try { obj = JSON.parse(raw); } catch { return null; }
+    } else if (typeof raw === "object") {
+      obj = raw;
+    } else {
+      return null;
+    }
+    const f = (v: unknown) => (v == null ? "0" : String(v));
+    return { c: f(obj.c), o: f(obj.o), h: f(obj.h), l: f(obj.l), v: f(obj.v), q: f(obj.q) };
+  }
+
   // Step 1: 请求接口获取交易对列表(名称、图标等静态信息)
   async function fetchCoins() {
     if (fetched.value && coins.value.length > 0) {
@@ -135,27 +159,33 @@ export const useCoinsStore = defineStore("coins", () => {
       if (list.length > 0) {
         coins.value = list
           .sort((a, b) => a.sort - b.sort)
-          .map((raw) => ({
-            id: raw.id,
-            symbol: raw.symbol,
-            coinSymbol: raw.coinSymbol,
-            baseSymbol: raw.baseSymbol,
-            name: raw.name,
-            icon: raw.icon ?? COIN_ICONS[raw.coinSymbol] ?? "",
-            sort: raw.sort,
-            coinScale: raw.coinScale ?? 2,
-            baseCoinScale: raw.baseCoinScale ?? 4,
-            // 价格字段初始为空,等 WebSocket 推送
-            open: "0",
-            high: "0",
-            low: "0",
-            close: "0",
-            chg: "0",
-            change: "0",
-            volume: "0",
-            turnover: "0",
-            stockType: raw.stockType,
-          }));
+          .map((raw) => {
+            const t = safeTicket(raw.ticket);
+            const closeN = t ? parseFloat(t.c) : 0;
+            const openN = t ? parseFloat(t.o) : 0;
+            const chgVal = openN > 0 ? (closeN - openN) / openN : 0;
+            return {
+              id: raw.id,
+              symbol: raw.symbol,
+              coinSymbol: raw.coinSymbol,
+              baseSymbol: raw.baseSymbol,
+              name: raw.name,
+              icon: raw.icon ?? COIN_ICONS[raw.coinSymbol] ?? "",
+              sort: raw.sort,
+              coinScale: raw.coinScale ?? 2,
+              baseCoinScale: raw.baseCoinScale ?? 4,
+              // 优先使用 API 返回的 ticket 行情数据,避免初始渲染零值闪烁
+              open: t?.o ?? "0",
+              high: t?.h ?? "0",
+              low: t?.l ?? "0",
+              close: t?.c ?? "0",
+              chg: String(chgVal),
+              change: String(closeN - openN),
+              volume: t?.v ?? "0",
+              turnover: t?.q ?? "0",
+              stockType: raw.stockType,
+            };
+          });
         fetched.value = true;
         // Step 2: 订阅 WebSocket 获取实时价格和涨跌幅
         startLivePrices();

+ 9 - 0
code/src/stores/market.ts

@@ -138,8 +138,16 @@ export const useMarketStore = defineStore("market", () => {
     };
   }
 
+  function applySpotInitialTickets() {
+    const tickets = spotStore.initialTickets;
+    if (tickets.size > 0) {
+      spotTicks.value = new Map(tickets);
+    }
+  }
+
   async function loadSpotSymbols() {
     if (spotStore.symbolList.length > 0) {
+      applySpotInitialTickets();
       void spotStore.fetchCoinIcons();
       startSpotWs(spotStore.symbolList.map((s) => s.symbol));
       return;
@@ -147,6 +155,7 @@ export const useMarketStore = defineStore("market", () => {
     spotLoading.value = true;
     try {
       await spotStore.loadSymbols();
+      applySpotInitialTickets();
       void spotStore.fetchCoinIcons();
       if (spotStore.symbolList.length > 0) {
         startSpotWs(spotStore.symbolList.map((s) => s.symbol));

+ 20 - 0
code/src/stores/rwaModal.ts

@@ -0,0 +1,20 @@
+import { defineStore } from 'pinia'
+import { ref } from 'vue'
+
+export const useRwaModalStore = defineStore('rwaModal', () => {
+  const showModal = ref(false)
+
+  function open() {
+    showModal.value = true
+  }
+
+  function close() {
+    showModal.value = false
+  }
+
+  return {
+    showModal,
+    open,
+    close,
+  }
+})

+ 28 - 1
code/src/stores/spot.ts

@@ -289,6 +289,9 @@ export const useSpotStore = defineStore('spot', () => {
   const accountRecordPage = ref(1)
   const accountRecordNoMore = ref(false)
 
+  /** API 返回的 ticket 行情快照(初始渲染用,随后由 WebSocket 热更新覆盖) */
+  const initialTickets = ref<Map<string, { close: number; open: number; high: number; low: number; turnover: number }>>(new Map())
+
   const symbolMap = computed(() => {
     const m = new Map<string, SpotSymbolInfo>()
     for (const s of symbolList.value) {
@@ -344,10 +347,33 @@ export const useSpotStore = defineStore('spot', () => {
     try {
       const raw = await get<unknown>('spot/v1/config/symbol/list')
       const arr = extractConfigArray(raw)
+      const tickets = new Map<string, { close: number; open: number; high: number; low: number; turnover: number }>()
       symbolList.value = arr
         .filter((x): x is Record<string, unknown> => !!x && typeof x === 'object')
-        .map(x => mapSymbol(x))
+        .map((x) => {
+          // 提取 API 返回的 ticket 行情快照(可能是对象或 JSON 字符串,字段可能为 null)
+          const rawTicket = x.ticket
+          let t: Record<string, unknown> | null = null
+          if (typeof rawTicket === 'string') {
+            try { t = JSON.parse(rawTicket) } catch { /* ignore */ }
+          } else if (rawTicket && typeof rawTicket === 'object') {
+            t = rawTicket as Record<string, unknown>
+          }
+          if (t) {
+            const sym = mapSymbol(x).symbol
+            const n = (v: unknown) => { const p = parseFloat(String(v ?? '')); return Number.isFinite(p) ? p : 0 }
+            tickets.set(sym, {
+              close: n(t.c ?? t.close),
+              open: n(t.o ?? t.open),
+              high: n(t.h ?? t.high),
+              low: n(t.l ?? t.low),
+              turnover: n(t.q ?? t.amount ?? t.turnover),
+            })
+          }
+          return mapSymbol(x)
+        })
         .filter(s => s.symbol.length > 0)
+      initialTickets.value = tickets
     } catch (e) {
       symbolsError.value = e instanceof Error ? e.message : '加载失败'
       throw e
@@ -545,6 +571,7 @@ export const useSpotStore = defineStore('spot', () => {
   return {
     symbolList,
     symbolMap,
+    initialTickets,
     symbolsLoading,
     symbolsError,
     assetSummary,

+ 32 - 3
code/src/views/FuturesView.vue

@@ -763,6 +763,17 @@ const serchTagItems = ref([
   { key: "futuresPage.pairSearchTag.fourType", value: 4 },
 ]);
 
+// selectSearchTag 选择筛选标签
+function selectSearchTag(item: any) {
+  currentSearchTag.value = item.value;
+  // 手动点击标签时自动选中筛选后的第一条
+  nextTick(() => {
+    if (filteredTickers.value.length > 0 && showPairSelector.value) {
+      selectPair(filteredTickers.value[0].symbol);
+    }
+  });
+}
+
 function coinIcon(sym: string): string {
   return coinsStore.coins.find((c) => c.symbol === sym)?.icon ?? "";
 }
@@ -2030,6 +2041,12 @@ const bookMidPrice = computed(() => {
 onMounted(async () => {
   const sym = String(route.params.symbol);
   await store.init(sym);
+  // 从 头菜单跳转过来时, URL 参数获取对比类型并自动打开对比类型搜索
+  const stockTypeTag = route.query.stockTypeTag as string;
+  if (stockTypeTag) {
+    currentSearchTag.value = parseInt(stockTypeTag);
+    showPairSelector.value = true;
+  }
   if (isLoggedIn.value) {
     store.fetchPositions();
     store.fetchCurrentOrders();
@@ -2051,13 +2068,19 @@ onMounted(async () => {
   countdownTimer = setInterval(updateCountdown, 1000);
   window.addEventListener("resize", onWindowResize);
   
-  // 从 URL 参数获取筛选类型并自动打开搜索框
+  // 从 头菜单跳转过来时, URL 参数获取筛选类型并自动打开搜索框
   const filter = route.query.filter as string;
   if (filter) {
     const filterNum = parseInt(filter);
     if (!isNaN(filterNum) && filterNum >= 1 && filterNum <= 4) {
-      currentSearchTag.value = filterNum;
       showPairSelector.value = true;
+      currentSearchTag.value = filterNum;
+      // 头菜单跳转自动选中筛选后的第一条
+      nextTick(() => {
+        if (filteredTickers.value.length > 0 && showPairSelector.value) {
+          selectPair(filteredTickers.value[0].symbol);
+        }
+      });
     }
   }
 });
@@ -2070,6 +2093,12 @@ watch(
       if (!isNaN(filterNum) && filterNum >= 1 && filterNum <= 4) {
         currentSearchTag.value = filterNum;
         showPairSelector.value = true;
+        // 自动选中筛选后的第一条
+        nextTick(() => {
+          if (filteredTickers.value.length > 0 && showPairSelector.value) {
+            selectPair(filteredTickers.value[0].symbol);
+          }
+        });
       }
     }
   }
@@ -2293,7 +2322,7 @@ onUnmounted(() => {
                 :key="item.key"
                 class="type-item"
                 :class="{ 'is-active': item.value == currentSearchTag }"
-                @click.stop="currentSearchTag = item.value"
+                @click.stop="selectSearchTag(item)"
               >
                 {{ t(item.key) }}
               </div>

+ 12 - 5
code/src/views/RwaView.vue

@@ -7,7 +7,9 @@ const { t } = useI18n()
 <template>
   <div class="rwa-view page-container">
     <div class="rwa-content">
-      <div class="rwa-icon">🔄</div>
+      <div class="rwa-icon">
+        <img src="@/assets/logo.png" alt="iBit" class="logo-img" />
+      </div>
       <h1 class="rwa-title">{{ t('nav.rwa') }}</h1>
       <p class="rwa-desc">{{ t('rwa.comingSoon') }}</p>
     </div>
@@ -16,21 +18,26 @@ const { t } = useI18n()
 
 <style scoped>
 .rwa-view {
-  min-height: calc(100vh - var(--header-height));
+  flex: 1;
   display: flex;
   align-items: center;
   justify-content: center;
-  padding: var(--space-8) 0;
 }
 
 .rwa-content {
   text-align: center;
-  padding: var(--space-8);
 }
 
 .rwa-icon {
-  font-size: 64px;
   margin-bottom: var(--space-4);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.logo-img {
+  width: 80px;
+  height: 80px;
 }
 
 .rwa-title {