Эх сурвалжийг харах

feat(web-app): 实现左侧导航栏 SideNav 组件

- 新增 components/SideNav.vue,对应原型 pc/02-fundamentals.html 左侧深色侧边栏
- 三组分区:主导航 / 基本面 7 大板块 / 模型工具,含角标与激活态
- App.vue 接入 SideNav,业务布局改为 grid(56px 1fr) + (208px 1fr) 双轴滚动
- 清理 main.css 中已废弃的 .side-nav/.content 旧样式
ligb 3 долоо хоног өмнө
parent
commit
2ed5e57d76

+ 7 - 0
web-app/src/App.vue

@@ -3,6 +3,12 @@
     <template v-if="isAppLayout">
       <div class="app-shell">
         <TopNavBar @logout="onLogout" />
+        <div class="app-body">
+          <SideNav />
+          <main class="content">
+            <router-view />
+          </main>
+        </div>
       </div>
     </template>
     <router-view v-else />
@@ -15,6 +21,7 @@ import { useRoute, useRouter } from 'vue-router'
 import { useAuth } from '@/store/auth'
 import { useSyncDocumentTitle } from '@/composables/useSyncDocumentTitle'
 import TopNavBar from '@/components/TopNavBar.vue'
+import SideNav from '@/components/SideNav.vue'
 
 const route = useRoute()
 const router = useRouter()

+ 15 - 58
web-app/src/assets/main.css

@@ -17,16 +17,26 @@ body {
 
 /* 业务布局外壳:顶部导航 + 主体(侧边栏 + 内容区) */
 .app-shell {
-  display: flex;
-  flex-direction: column;
+  display: grid;
+  grid-template-rows: 56px 1fr;
   width: 100%;
-  min-height: 100vh;
+  height: 100vh;
+  overflow: hidden;
 }
 
 .app-body {
-  display: flex;
-  flex: 1;
+  display: grid;
+  grid-template-columns: 208px 1fr;
   min-height: 0;
+  overflow: hidden;
+}
+
+.layout-app .content {
+  flex: 1;
+  min-width: 0;
+  overflow-y: auto;
+  background: #f2f5fa;
+  padding: 18px 24px 32px;
 }
 
 /* blank 布局(登录页):避免作为 flex item 收缩到内容宽度,
@@ -36,59 +46,6 @@ body {
   width: 100%;
 }
 
-.side-nav {
-  width: 200px;
-  padding: 16px 12px;
-  background: #1f2937;
-  color: #fff;
-}
-
-.side-nav h3 {
-  margin: 0 0 16px 8px;
-  font-size: 16px;
-}
-
-.side-nav a {
-  display: block;
-  padding: 10px 14px;
-  margin-bottom: 4px;
-  color: #d1d5db;
-  text-decoration: none;
-  border-radius: 6px;
-}
-
-.side-nav a.router-link-active {
-  background: #1d4ed8;
-  color: #fff;
-}
-
-.side-nav .logout-btn {
-  margin-top: 16px;
-  width: 100%;
-  padding: 8px 14px;
-  font-size: 13px;
-  color: #d1d5db;
-  background: transparent;
-  border: 1px solid #374151;
-  border-radius: 6px;
-  cursor: pointer;
-}
-
-.side-nav .logout-btn:hover {
-  background: #b91c1c;
-  border-color: #b91c1c;
-  color: #fff;
-}
-
-.content {
-  flex: 1;
-  padding: 24px;
-  background: #fff;
-  margin: 16px;
-  border-radius: 10px;
-  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
-}
-
 .demo-table {
   width: 100%;
   border-collapse: collapse;

+ 157 - 0
web-app/src/components/SideNav.vue

@@ -0,0 +1,157 @@
+<template>
+  <aside class="sidebar">
+    <div v-for="group in groups" :key="group.label" class="sb-section">
+      <div class="sb-label">{{ group.label }}</div>
+      <div
+        v-for="item in group.items"
+        :key="item.key"
+        :class="['sb-item', { active: isActive(item) }]"
+        @click="onClick(item)"
+      >
+        <span class="ico">{{ item.icon }}</span>
+        <span class="label">{{ item.label }}</span>
+        <span v-if="item.badge" class="num">{{ item.badge }}</span>
+      </div>
+      <div v-if="group.divider" class="sb-divider"></div>
+    </div>
+  </aside>
+</template>
+
+<script setup>
+import { useRouter, useRoute } from 'vue-router'
+
+const router = useRouter()
+const route = useRoute()
+
+const groups = [
+  {
+    label: '主导航',
+    divider: true,
+    items: [
+      { key: 'dashboard', label: '工作台', icon: '◉', to: '/' },
+      { key: 'board', label: '看板广场', icon: '▦' },
+      { key: 'watch', label: '自选股', icon: '★', badge: 12 },
+      { key: 'alert', label: '预警中心', icon: '⏰', badge: 3 },
+      { key: 'download', label: '数据下载', icon: '⬇' },
+    ],
+  },
+  {
+    label: '基本面 · 7 大板块',
+    divider: true,
+    items: [
+      { key: 'macro', label: '宏观总览', icon: '●', to: '/fundamentals', match: 'fundamentals' },
+      { key: 'gdp', label: 'GDP 与增长', icon: '●' },
+      { key: 'invest', label: '投资分析', icon: '●' },
+      { key: 'trade', label: '进出口贸易', icon: '●' },
+      { key: 'consume', label: '消费与就业', icon: '●' },
+      { key: 'cpi', label: '通胀 (CPI/PPI)', icon: '●' },
+      { key: 'supply', label: '工业与供给', icon: '●' },
+      { key: 'pmi', label: '景气度 (PMI/BCI)', icon: '●' },
+    ],
+  },
+  {
+    label: '模型工具',
+    divider: false,
+    items: [
+      { key: 'gdp-now', label: 'GDP 周频预测', icon: '⚙' },
+      { key: 'cpi-fcst', label: '通胀预测模型', icon: '⚙' },
+      { key: 'pmi-mix', label: 'PMI 合成', icon: '⚙' },
+    ],
+  },
+]
+
+function isActive(item) {
+  if (item.to) return route.path === item.to
+  if (item.match) return route.path.startsWith(`/${item.match}`)
+  return false
+}
+
+function onClick(item) {
+  if (item.to) {
+    router.push(item.to)
+  }
+}
+</script>
+
+<style scoped>
+.sidebar {
+  width: 208px;
+  flex: none;
+  background: #0c2b5e;
+  color: rgba(255, 255, 255, 0.78);
+  padding: 14px 0;
+  overflow-y: auto;
+}
+
+.sb-section {
+  padding: 0 12px;
+  margin-bottom: 16px;
+}
+
+.sb-label {
+  font-size: 10.5px;
+  color: rgba(255, 255, 255, 0.4);
+  padding: 6px 14px;
+  margin-bottom: 6px;
+  letter-spacing: 1px;
+}
+
+.sb-item {
+  display: flex;
+  align-items: center;
+  gap: 11px;
+  padding: 9px 14px;
+  border-radius: 6px;
+  font-size: 12.5px;
+  cursor: pointer;
+  color: rgba(255, 255, 255, 0.78);
+  margin-bottom: 2px;
+  transition: 0.15s;
+}
+
+.sb-item:hover {
+  background: #143a80;
+  color: #fff;
+}
+
+.sb-item.active {
+  background: #fff;
+  color: #0c2b5e;
+  font-weight: 600;
+}
+
+.sb-item.active .ico {
+  color: #1552d0;
+}
+
+.sb-item .ico {
+  width: 14px;
+  height: 14px;
+  font-size: 14px;
+  display: inline-grid;
+  place-items: center;
+  color: rgba(255, 255, 255, 0.6);
+}
+
+.sb-item .label {
+  flex: 1;
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+}
+
+.sb-item .num {
+  font-size: 10px;
+  color: #fff;
+  background: #e13b3b;
+  padding: 1px 5px;
+  border-radius: 8px;
+  font-weight: 600;
+}
+
+.sb-divider {
+  height: 1px;
+  background: rgba(255, 255, 255, 0.1);
+  margin: 8px 14px;
+}
+</style>