App.vue 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245
  1. <template>
  2. <div id="app-container">
  3. <el-container class="main-container">
  4. <!-- 侧边栏导航 -->
  5. <el-aside width="176px" class="sidebar">
  6. <div class="sidebar-logo">
  7. <el-icon :size="26"><TrendCharts /></el-icon>
  8. <span class="sidebar-logo-text">分析助手</span>
  9. </div>
  10. <!-- 不用 el-menu collapse:其在部分环境下会把除第一项外的图标槽位挤没;改用 router-link 稳定渲染 -->
  11. <nav class="sidebar-nav" aria-label="主导航">
  12. <router-link
  13. v-for="nav in sidebarNav"
  14. :key="nav.index"
  15. :to="nav.index"
  16. class="sidebar-link"
  17. :class="{ 'is-active': activeMenu === nav.index }"
  18. :title="nav.title"
  19. @mouseenter="prefetchRouteByNavPath(nav.index)"
  20. >
  21. <el-icon class="sidebar-nav-icon" :size="20">
  22. <component :is="nav.icon" />
  23. </el-icon>
  24. <span class="sidebar-link-text">{{ nav.title }}</span>
  25. </router-link>
  26. </nav>
  27. </el-aside>
  28. <!-- 主内容区 -->
  29. <el-container>
  30. <el-header class="app-header">
  31. <div class="header-left">
  32. <h2 class="app-title">智能股票分析助手</h2>
  33. </div>
  34. <div class="header-right">
  35. <el-input
  36. v-model="searchText"
  37. placeholder="输入股票代码或名称..."
  38. class="header-search"
  39. clearable
  40. >
  41. <template #prefix>
  42. <el-icon><SearchIcon /></el-icon>
  43. </template>
  44. </el-input>
  45. <el-button type="primary" circle @click="handleSearch">
  46. <el-icon><SearchIcon /></el-icon>
  47. </el-button>
  48. </div>
  49. </el-header>
  50. <el-main class="app-main">
  51. <!-- KeepAlive:已访问过的页面保留实例,切换回来时无需重新挂载/拉脚本 -->
  52. <!-- :key 绑定路由名,避免 KeepAlive + 异步组件时切换页面视图不更新 -->
  53. <router-view v-slot="{ Component }">
  54. <KeepAlive :include="cachedViews">
  55. <component
  56. :is="Component"
  57. v-if="Component"
  58. :key="route.name ?? route.path"
  59. />
  60. </KeepAlive>
  61. </router-view>
  62. </el-main>
  63. </el-container>
  64. </el-container>
  65. </div>
  66. </template>
  67. <script setup>
  68. import { ref, computed } from 'vue'
  69. import { useRoute, useRouter } from 'vue-router'
  70. import { prefetchRouteByNavPath } from '@/router/route-loaders.js'
  71. import {
  72. TrendCharts,
  73. Odometer,
  74. DataAnalysis,
  75. Search as SearchIcon,
  76. Notebook,
  77. Money,
  78. } from '@element-plus/icons-vue'
  79. /** 与各页面 <script> export default { name } 一致,供 KeepAlive 命中缓存 */
  80. const cachedViews = ['Dashboard', 'StockAnalysis', 'StockScreener', 'NewsCenter', 'Simulation']
  81. /** 侧栏项:icon 为组件对象,供 :is 渲染(避免标签名与原生 HTML 冲突) */
  82. const sidebarNav = [
  83. { index: '/dashboard', title: '仪表盘', icon: Odometer },
  84. { index: '/analysis', title: '股票分析', icon: DataAnalysis },
  85. { index: '/screener', title: '智能选股', icon: SearchIcon },
  86. { index: '/news', title: '新闻舆情', icon: Notebook },
  87. { index: '/simulation', title: '模拟交易', icon: Money },
  88. ]
  89. const route = useRoute()
  90. const router = useRouter()
  91. const searchText = ref('')
  92. /** 子路由(如 /analysis/600519)仍高亮对应菜单项 */
  93. const activeMenu = computed(() => {
  94. const p = route.path
  95. if (p === '/' || p.startsWith('/dashboard')) return '/dashboard'
  96. if (p.startsWith('/analysis')) return '/analysis'
  97. if (p.startsWith('/screener')) return '/screener'
  98. if (p.startsWith('/news')) return '/news'
  99. if (p.startsWith('/simulation')) return '/simulation'
  100. return p
  101. })
  102. function handleSearch() {
  103. if (searchText.value.trim()) {
  104. router.push(`/analysis/${searchText.value.trim()}`)
  105. }
  106. }
  107. </script>
  108. <style>
  109. * {
  110. margin: 0;
  111. padding: 0;
  112. box-sizing: border-box;
  113. }
  114. html, body, #app, #app-container {
  115. height: 100%;
  116. font-family: 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;
  117. }
  118. .main-container {
  119. height: 100%;
  120. }
  121. /* 侧边栏:图标 + 文字标题,固定宽度避免挤压 */
  122. .sidebar {
  123. flex-shrink: 0;
  124. min-width: 176px;
  125. width: 176px !important;
  126. background: linear-gradient(180deg, #1a1a2e 0%, #16213e 100%);
  127. overflow-x: visible;
  128. overflow-y: auto;
  129. }
  130. .sidebar-logo {
  131. height: 64px;
  132. display: flex;
  133. align-items: center;
  134. justify-content: flex-start;
  135. gap: 10px;
  136. padding: 0 14px;
  137. color: #409eff;
  138. border-bottom: 1px solid rgba(255, 255, 255, 0.1);
  139. }
  140. .sidebar-logo-text {
  141. font-size: 14px;
  142. font-weight: 600;
  143. color: rgba(255, 255, 255, 0.92);
  144. letter-spacing: 0.02em;
  145. white-space: nowrap;
  146. }
  147. .sidebar-nav {
  148. display: flex;
  149. flex-direction: column;
  150. align-items: stretch;
  151. padding: 10px 8px;
  152. gap: 4px;
  153. }
  154. .sidebar-link {
  155. display: flex;
  156. flex-direction: row;
  157. align-items: center;
  158. justify-content: flex-start;
  159. gap: 10px;
  160. width: 100%;
  161. min-height: 44px;
  162. padding: 8px 12px;
  163. border-radius: 8px;
  164. color: rgba(255, 255, 255, 0.72);
  165. text-decoration: none;
  166. transition: background 0.15s, color 0.15s;
  167. box-sizing: border-box;
  168. }
  169. .sidebar-link-text {
  170. font-size: 13px;
  171. font-weight: 500;
  172. line-height: 1.3;
  173. white-space: nowrap;
  174. }
  175. .sidebar-link:hover {
  176. color: #fff;
  177. background: rgba(255, 255, 255, 0.08);
  178. }
  179. .sidebar-link.is-active {
  180. color: #409eff;
  181. background: rgba(64, 158, 255, 0.18);
  182. }
  183. .sidebar-nav-icon {
  184. display: inline-flex;
  185. }
  186. .sidebar-nav-icon svg {
  187. fill: currentColor;
  188. }
  189. /* 顶部栏 */
  190. .app-header {
  191. display: flex;
  192. align-items: center;
  193. justify-content: space-between;
  194. background: #fff;
  195. border-bottom: 1px solid #e6e6e6;
  196. padding: 0 20px;
  197. height: 64px;
  198. }
  199. .app-title {
  200. font-size: 18px;
  201. color: #303133;
  202. font-weight: 600;
  203. }
  204. .header-right {
  205. display: flex;
  206. align-items: center;
  207. gap: 10px;
  208. }
  209. .header-search {
  210. width: 280px;
  211. }
  212. /* 主内容区:flex 子项默认可溢出隐藏,需 min-width:0 + 横向滚动,图表左边才不会被「吃掉」 */
  213. .app-main {
  214. background: #f5f7fa;
  215. padding: 20px;
  216. min-width: 0;
  217. overflow-x: auto;
  218. }
  219. </style>