Files
xiaotutongxue/frontend-pc/src/layout/LayoutMix.vue
T
lovife 87ece5a971 feat(frontend): 管理端改用 Gi Admin Pro 框架 + 腾讯云主题
前端此前只有本地实现、从未入库,本次首次纳入版本管理。

框架切换
  · 以 Gi Admin Pro(Vue3 + Vite + TS + Arco Design Vue + Pinia)为底
  · 剥离 14 个演示页面目录 / 121 个文件(demo 28、file 20、form 19、system 18…)
  · 依赖 dependencies 35 → 19,devDependencies 31 → 27
  · 保留框架核心:layout、Gi* 组件库、hooks、权限指令、错误页、登录页

主题:腾讯云 / TDesign 官方色板
  · 主色 #0052D9(腾讯蓝)、成功 #00A870、警告 #ED7B2F、危险 #E34D59
  · 页面底色 #f3f3f3、文字 rgba(0,0,0,90/60/40/26%)
  · 覆盖 Arco 底层色板 + 显式语义别名(primary/success/warning/danger/link/neutral)
  · 三个改了不生效也不报错的坑已记入注释:
      1) Arco 色值是逗号分隔 RGB 三元组,不是 #RRGGBB
      2) Arco 把色板定义在 body 而非 :root,必须写 `:root, body`
      3) setting.json 的 themeColor 会以内联样式写进 body,优先级高于样式表

对接后端(三处契约差异在 @/utils/http 统一适配)
  · 响应信封 {code,msg,data} → {code,success,message,data}
  · Token 头 token → Authorization: Bearer
  · 分页 {list,total} → 补一份 records,list 原样保留
  · 40101 → 登录页;登录密码 RSA 加密(公钥走 /auth/public-key,轮换无需重建前端)
  · 路由:后端只下发菜单键,前端在本地路由表上过滤(有哪些页面归代码,谁能看归后端)

已实现页面(浏览器实测通过)
  · 登录:RSA 加密 + 自适应验证码/租户码 + 账号锁定提示
  · 工作台:真实数据(组织 6 个 / 角色 1 个 / 权限 25 项)
  · 组织架构:三级树 + CRUD + 占用校验
  · 租户管理:开通租户 + 初始密码只显示一次的强确认
  · 待建页:未实现菜单显示所属里程碑
  · 权限菜单:学校管理员看不到「租户管理」

视觉资产按腾讯云主题重做
  · logo.svg(渐变圆角方 + 填)、favicon.svg(小尺寸做减法)、login-hero.svg(材料归集)
  · 登录页点阵纹理走主题变量,换主色自动跟随
  · 文案改为产品实际能力:自动改名 / 强制压缩 / 隐私保护

清理模板遗留(约 17MB + 三处外部依赖)
  · 百度统计脚本(上报到 gi-demo 作者的统计账号)
  · 用户菜单「项目地址」(跳转模板作者的 Gitee 仓库)
  · 两份 8.8MB mp3、388KB favicon.ico、57KB 模板插画、未引用字体与 loading.gif
  · 「个人中心」指向已删除的 /system/account(点了 404)
  · 头部头像在无 avatarUrl 时渲染成裂图 → 改为姓名首字兜底

后端修复
  · 新增 GET /auth/public-key(前端加密登录密码用)
  · DataSeeder.ancestor_path 生成错误:子节点本该是 /16/17/ 却写成 /17/
    该字段是「按院系统计」的索引,错了会漏人/多人且不报错 —— 改为从库里读父节点路径
  · seedDemoOrgs 曾放在租户作用域外 + admin 早退分支之后,导致启动失败 / 永不执行
  · 示范租户补一套三级组织树(1 学校 / 2 院系 / 3 班级),供界面演示
2026-09-28 20:49:55 +08:00

166 lines
4.2 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!--
@file LayoutMix 组件
@description 混合布局组件,支持顶部导航和左侧菜单组合的布局方式
-->
<template>
<div class="layout-mix">
<!-- 左侧菜单区域 -->
<section v-if="isDesktop" class="layout-mix-left asider" :class="{ 'app-menu-dark': appStore.isMenuDark }"
:style="getMenuStyle">
<a-layout-sider class="asider__menu" collapsible breakpoint="xl" hide-trigger :width="200"
:collapsed="appStore.isMenuCollapsed">
<Logo :collapsed="appStore.isMenuCollapsed" />
<a-scrollbar outer-class="asider__menu-scroll-view" style="height: 100%; overflow: auto">
<a-menu :theme="menuTheme" mode="vertical" auto-open-selected :collapsed="shouldCollapse"
:selected-keys="[twoActivePath]" breakpoint="xl" :accordion="appStore.isMenuAccordion"
@menu-item-click="handleTwoMenuItemClick">
<MenuItem v-for="(item, index) in twoLevelMenus" :key="item.path + index" :item="item" />
</a-menu>
</a-scrollbar>
</a-layout-sider>
</section>
<!-- 右侧内容区域 -->
<section class="layout-mix-right">
<header class="header" :class="{ 'app-menu-dark': appStore.isMenuDark }" :style="getMenuStyle">
<MenuFoldBtn />
<a-menu mode="horizontal" :theme="menuTheme" :selected-keys="[oneActivePath]"
@menu-item-click="handleOneMenuItemClick">
<MenuItem v-for="(item, index) in topMenus" :key="item.path + index" :item="item" />
</a-menu>
<HeaderRightBar />
</header>
<Tabs v-if="appStore.isTabVisible" />
<Main />
</section>
</div>
</template>
<script setup lang="ts">
import { useDevice } from '@/hooks'
import { useLevelMenu } from '@/layout/hooks/useLevelMenu'
import { useAppStore } from '@/stores'
import HeaderRightBar from './components/HeaderRightBar/index.vue'
import Logo from './components/Logo.vue'
import Main from './components/Main.vue'
import MenuItem from './components/Menu/MenuItem.vue'
import MenuFoldBtn from './components/MenuFoldBtn.vue'
import Tabs from './components/Tabs/index.vue'
/** 组件名称 */
defineOptions({ name: 'LayoutMix' })
const appStore = useAppStore()
const { isDesktop } = useDevice()
const { menuTheme, getMenuStyle, oneLevelMenus, twoLevelMenus, oneActivePath, twoActivePath, getOneLevelMenus, handleOneMenuItemClick, handleTwoMenuItemClick } = useLevelMenu()
getOneLevelMenus()
const topMenus = computed(() => {
const arr = JSON.parse(JSON.stringify(oneLevelMenus.value))
arr.forEach((i) => {
delete i.children
})
return arr
})
// 是否折叠菜单
const shouldCollapse = computed(() =>
!isDesktop.value ? false : appStore.isMenuCollapsed
)
</script>
<style lang="scss" scoped>
:deep(.arco-menu-pop) {
white-space: nowrap;
}
:deep(.arco-menu.arco-menu-vertical.arco-menu-collapsed) {
// Menu菜单组件修改
.arco-menu-icon {
padding: 10px 0;
margin-right: 0;
}
.arco-menu-has-icon {
justify-content: center;
padding: 0;
}
.arco-menu-title {
display: none;
}
}
:deep(.arco-menu-horizontal) {
flex: 1;
overflow: hidden;
.arco-menu-inner {
padding-left: 0;
.arco-menu-overflow-wrap {
white-space: nowrap;
}
}
}
.layout-mix {
display: flex;
align-items: stretch;
height: 100%;
overflow: hidden;
&-left {
overflow: hidden;
}
&-right {
display: flex;
flex: 1;
flex-direction: column;
overflow: hidden;
}
}
.header {
display: flex;
align-items: center;
justify-content: space-between;
height: 56px;
padding: 0 var(--padding);
overflow: hidden;
color: var(--color-text-1);
background-color: var(--color-bg-1);
border-bottom: 1px solid var(--color-border);
}
.asider {
z-index: 1000;
box-sizing: border-box;
display: flex;
flex-direction: column;
color: var(--color-text-1);
background-color: var(--color-bg-1);
border-right: 1px solid var(--color-border-2);
:deep(.arco-layout-sider-children) {
display: flex;
flex-direction: column;
overflow: hidden;
}
&__menu {
flex: 1;
overflow: hidden;
background-color: inherit;
}
&__menu-scroll-view {
flex: 1;
overflow: hidden;
}
}
</style>