MainLayout.vue 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306
  1. <template>
  2. <div class="layout">
  3. <!-- 左侧菜单 -->
  4. <aside class="sidebar">
  5. <div class="brand">
  6. <!-- <div class="brand-icon">申</div> -->
  7. <div class="brand-icon"><img :src="logo" alt="logo" style="width: 40px; height: 40px;" /></div>
  8. <div>
  9. <strong>申勤物业</strong>
  10. <span>数字化平台</span>
  11. </div>
  12. </div>
  13. <nav class="nav">
  14. <div v-for="item in navItems" :key="item.path" @click="handleClick(item.path)">
  15. <router-link
  16. v-if="item.path !== 'nav-label'"
  17. :class="{ active: $route.path === item.path }"
  18. :to="item.path"
  19. >
  20. {{ item.name }}
  21. </router-link>
  22. <div v-else class="nav-label">
  23. {{ item.name }}
  24. </div>
  25. </div>
  26. </nav>
  27. </aside>
  28. <!-- 右侧内容区 -->
  29. <div class="main">
  30. <!-- 顶部导航 -->
  31. <header class="topbar">
  32. <div>
  33. <h1>{{ currentTitle }}</h1>
  34. <p>{{ currentSubtitle }}</p>
  35. </div>
  36. <!-- <el-input
  37. v-model="searchText"
  38. placeholder="搜索项目、人员…"
  39. class="search-input"
  40. /> -->
  41. <div class="sidebar-footer">
  42. <el-dropdown @command="handleLogout">
  43. <span class="el-dropdown-link">
  44. <el-icon :size="20" color="var(--text2)">
  45. <UserFilled />
  46. </el-icon>
  47. <span style="padding: 0 6px">
  48. {{userName}}
  49. </span>
  50. </span>
  51. <template #dropdown>
  52. <el-dropdown-menu>
  53. <!-- <el-dropdown-item command="login">账号登录</el-dropdown-item> -->
  54. <el-dropdown-item command="logout">退出登录</el-dropdown-item
  55. >
  56. </el-dropdown-menu>
  57. </template>
  58. </el-dropdown>
  59. </div>
  60. </header>
  61. <!-- 路由内容 -->
  62. <div :class="isGis == '/onemap' ? 'content-gis' : 'content'">
  63. <router-view />
  64. </div>
  65. </div>
  66. </div>
  67. </template>
  68. <script setup lang="ts">
  69. import { computed, ref } from 'vue'
  70. import { useRouter } from 'vue-router'
  71. import { ElMessage } from 'element-plus'
  72. import { useUserStore } from '@/stores/user'
  73. import { UserFilled } from '@element-plus/icons-vue'
  74. const logo = new URL('/static/image/logo.png', import.meta.url).href
  75. const userStore = useUserStore()
  76. const router = useRouter()
  77. const userInfo = JSON.parse(localStorage.getItem('userInfo') || '{}')
  78. const userName = userInfo?.username =='user_xtw' ? '徐天炜' : userInfo?.username || ''
  79. const navItems = [
  80. { path: '/dashboard', name: '工作台' },
  81. { path: '/onemap', name: '空间一张图' },
  82. { path: '/projects', name: '项目台账' },
  83. { path: 'nav-label', name: '市场拓展' },
  84. { path: '/market', name: '市场拓展中心' },
  85. { path: '/renewal', name: '标书编制' },
  86. { path: '/tender-pool', name: '招标监测' },
  87. { path: '/bid-result', name: '开标与中标' }
  88. ]
  89. const isGis = ref('')
  90. const currentTitle = computed(() => {
  91. // sessionStorage.setItem('activeTab','ongoing')
  92. const item = navItems.find(n => n.path === router.currentRoute.value.path)
  93. isGis.value = item?.path ?? ''
  94. if(!item){
  95. return sessionStorage.getItem('name')
  96. }else{
  97. return item ? item.name : '工作台'
  98. }
  99. })
  100. const handleClick = (_path: string) => {
  101. sessionStorage.setItem('activeTab','ongoing')
  102. }
  103. const currentSubtitle = computed(() => {
  104. if (router.currentRoute.value.path === '/dashboard') {
  105. return '今日待办与关键指标 · 续标预警优先展示'
  106. }else if (router.currentRoute.value.path === '/onemap') {
  107. return '数据可视化平台 · 空间一张图'
  108. }else if (router.currentRoute.value.path === '/projects') {
  109. return '在管项目一览'
  110. }else if (router.currentRoute.value.path === '/market') {
  111. return '招标监测 · 标书编制 · 投标履约 · 项目续标'
  112. }else if (router.currentRoute.value.path === '/renewal') {
  113. return '合同到期预警 · 续标全流程跟进'
  114. }else if (router.currentRoute.value.path === '/tender-pool') {
  115. return '政府采购网抓取 · 续标监控 · 智能匹配'
  116. }else if (router.currentRoute.value.path === '/bid-workbench') {
  117. return '需求拆解 · 数据映射 · 分步确认 · 生成导出'
  118. }else if (router.currentRoute.value.path === '/bid-result') {
  119. return '投标登记 · 开标监控 · 结果抓取 · 中标推送'
  120. }else if (router.currentRoute.value.path.includes('project-detail/') || router.currentRoute.value.path.includes('renewal-detail/') || router.currentRoute.value.path.includes('renewal-expire-detail/')) {
  121. const addr = (sessionStorage.getItem('projectName'))
  122. return addr
  123. }else {
  124. return '欢迎使用申勤物业数字化平台'
  125. }
  126. })
  127. const handleLogout = () => {
  128. userStore.logout()
  129. ElMessage.success('已退出登录')
  130. router.push('/')
  131. }
  132. </script>
  133. <style scoped>
  134. .layout {
  135. display: flex;
  136. min-height: 100vh;
  137. }
  138. .sidebar {
  139. width: 220px;
  140. flex-shrink: 0;
  141. background: var(--surface);
  142. border-right: 1px solid var(--border);
  143. padding: 16px 12px;
  144. display: flex;
  145. flex-direction: column;
  146. }
  147. .brand {
  148. display: flex;
  149. align-items: center;
  150. gap: 10px;
  151. padding: 8px 10px 20px;
  152. }
  153. .brand-icon {
  154. width: 36px;
  155. height: 36px;
  156. /* background: var(--primary); */
  157. border-radius: 8px;
  158. color: #fff;
  159. display: flex;
  160. align-items: center;
  161. justify-content: center;
  162. font-weight: 700;
  163. font-size: 14px;
  164. }
  165. .brand strong {
  166. display: block;
  167. font-size: 14px;
  168. }
  169. .brand span {
  170. font-size: 11px;
  171. color: var(--text3);
  172. }
  173. .nav {
  174. flex: 1;
  175. }
  176. .nav a {
  177. display: block;
  178. padding: 8px 12px;
  179. border-radius: 6px;
  180. color: var(--text2);
  181. font-size: 13px;
  182. margin-bottom: 2px;
  183. text-decoration: none;
  184. }
  185. .nav a:hover {
  186. background: #f8fafb;
  187. color: var(--text);
  188. }
  189. .nav a.active {
  190. background: var(--primary-soft);
  191. color: var(--primary);
  192. font-weight: 500;
  193. }
  194. .nav-label {
  195. font-size: 11px;
  196. color: var(--text3);
  197. padding: 12px 12px 6px;
  198. font-weight: 600;
  199. }
  200. .sidebar-footer {
  201. display: flex;
  202. cursor: pointer;
  203. align-items: center;
  204. font-size: 16px;
  205. color: var(--text3);
  206. }
  207. .sidebar-footer:hover {
  208. color: var(--primary);
  209. }
  210. .sidebar-footer .el-dropdown-link {
  211. display: flex;
  212. align-items: center;
  213. justify-content: center;
  214. }
  215. .main {
  216. flex: 1;
  217. display: flex;
  218. flex-direction: column;
  219. min-width: 0;
  220. background: var(--bg);
  221. overflow: hidden;
  222. }
  223. .topbar {
  224. min-height: 52px;
  225. background: var(--surface);
  226. border-bottom: 1px solid var(--border);
  227. display: flex;
  228. align-items: center;
  229. justify-content: space-between;
  230. padding: 10px 24px;
  231. gap: 16px;
  232. flex-wrap: wrap;
  233. }
  234. .topbar h1 {
  235. font-size: 17px;
  236. font-weight: 600;
  237. }
  238. .topbar p {
  239. font-size: 12px;
  240. color: var(--text3);
  241. }
  242. .search-input {
  243. width: 240px;
  244. }
  245. .content {
  246. padding: 20px 24px 32px;
  247. /* flex: 1; */
  248. height: calc(100vh - 70px);
  249. overflow-y: auto;
  250. }
  251. .content-gis {
  252. padding: 0 0;
  253. /* flex: 1; */
  254. /* height: calc(70px); */
  255. height: 100%;
  256. overflow-y: auto;
  257. }
  258. .el-dropdown-link {
  259. cursor: pointer;
  260. color: var(--text3);
  261. display: flex;
  262. align-items: center;
  263. border: 0;
  264. }
  265. .el-dropdown,
  266. .el-dropdown * {
  267. outline: none !important; /* 确保下拉触发器没有轮廓 */
  268. }
  269. @media (max-width: 768px) {
  270. .sidebar {
  271. width: 60px;
  272. }
  273. .brand span, .brand strong, .nav a, .sidebar-footer span {
  274. display: none;
  275. }
  276. }
  277. </style>