From f6743d34d1b84a6fd64823e6dcf45f1f1cadaf9b Mon Sep 17 00:00:00 2001 From: hrx <18603305412@163.com> Date: Fri, 22 May 2026 11:07:13 +0800 Subject: [PATCH] updata --- f/web-kboss/src/api/gotoYuanJing.js | 9 + f/web-kboss/src/api/model/model.js | 74 ++ f/web-kboss/src/api/newHome.js | 9 + .../modelManagement/ListingConfirmDialog.vue | 186 ++++ .../modelManagement/ModelDetailDialog.vue | 257 +++++ .../modelManagement/ModelFilter.vue | 134 +++ .../components/modelManagement/ModelStats.vue | 126 +++ f/web-kboss/src/layout/components/Navbar.vue | 12 + .../layout/components/Sidebar/SidebarItem.vue | 4 +- .../src/layout/components/Sidebar/index.vue | 103 +- f/web-kboss/src/permission.js | 2 +- f/web-kboss/src/router/index.js | 140 ++- f/web-kboss/src/store/modules/permission.js | 524 ++++++---- f/web-kboss/src/store/modules/user.js | 24 +- f/web-kboss/src/styles/sidebar.scss | 6 +- .../homePage/components/topBox/index.vue | 99 +- .../views/homePage/ncmatch/mainPage/index.vue | 961 ++++++++++++++---- f/web-kboss/src/views/login/indexNew.vue | 7 +- .../views/modelManagement/AddModelDialog.vue | 422 ++++++++ .../src/views/modelManagement/ApiDocument.vue | 243 +++++ .../src/views/modelManagement/Experience.vue | 546 ++++++++++ .../src/views/modelManagement/ModelDetail.vue | 349 +++++++ .../views/modelManagement/modelManagement.vue | 580 +++++++++++ .../views/operation/operationReport/index.vue | 197 ++++ .../src/views/product/allProduct/index.vue | 536 +++++++++- .../src/views/tokenManagement/index.vue | 693 +++++++++++++ 26 files changed, 5784 insertions(+), 459 deletions(-) create mode 100644 f/web-kboss/src/api/gotoYuanJing.js create mode 100644 f/web-kboss/src/api/model/model.js create mode 100644 f/web-kboss/src/components/modelManagement/ListingConfirmDialog.vue create mode 100644 f/web-kboss/src/components/modelManagement/ModelDetailDialog.vue create mode 100644 f/web-kboss/src/components/modelManagement/ModelFilter.vue create mode 100644 f/web-kboss/src/components/modelManagement/ModelStats.vue create mode 100644 f/web-kboss/src/views/modelManagement/AddModelDialog.vue create mode 100644 f/web-kboss/src/views/modelManagement/ApiDocument.vue create mode 100644 f/web-kboss/src/views/modelManagement/Experience.vue create mode 100644 f/web-kboss/src/views/modelManagement/ModelDetail.vue create mode 100644 f/web-kboss/src/views/modelManagement/modelManagement.vue create mode 100644 f/web-kboss/src/views/operation/operationReport/index.vue create mode 100644 f/web-kboss/src/views/tokenManagement/index.vue diff --git a/f/web-kboss/src/api/gotoYuanJing.js b/f/web-kboss/src/api/gotoYuanJing.js new file mode 100644 index 0000000..ee75f2b --- /dev/null +++ b/f/web-kboss/src/api/gotoYuanJing.js @@ -0,0 +1,9 @@ +import request from '@/utils/request' +// 跳转远景 +export function gotoYuanJingAPI(data) { + return request({ + url: `cntoai/get_deerer_header.dspy`, + method: 'get', + params: data + }) +} \ No newline at end of file diff --git a/f/web-kboss/src/api/model/model.js b/f/web-kboss/src/api/model/model.js new file mode 100644 index 0000000..3397ed4 --- /dev/null +++ b/f/web-kboss/src/api/model/model.js @@ -0,0 +1,74 @@ +import request from "@/utils/request"; +// 获取模型列表 +export const reqModelList = (params = {}) => { + return request({ + url: 'cntoai/model_management_search.dspy', + method: 'get', + params + }) +} +// 上架 +export const reqModelUp = (id) => { + return request({ + url: '/cntoai/model_management_list.dspy', + method: 'get', + params: { id } + }) +} +// 下架 +export const reqModelDown = (id) => { + return request({ + url: '/cntoai/model_management_unlist.dspy', + method: 'get', + params: { id } + }) +} +// 模型详情 +export const reqModelDetail = (id) => { + return request({ + url: '/cntoai/model_management_detail.dspy', + method: 'get', + params: { id } + }) +} +// 编辑模型 +export const reqModelEdit = (data) => { + return request({ + url: '/cntoai/model_management_update.dspy', + method: 'get', + params: data + }) +} +// 置顶 +export const reqModelTop = (id) => { + return request({ + url: '/cntoai/model_management_pin_top.dspy', + method: 'get', + params: { id } + }) +} +// 下移 +export const reqModelBottom = (id) => { + return request({ + url: '/cntoai/model_management_move_down.dspy', + method: 'get', + params: { id } + }) +} + +// apikey列表 +export const reqApikeyList = (params = {}) => { + return request({ + url: '/cntoai/get_model_apikey.dspy', + method: 'get', + params + }) +} +// 创建apikey +export const reqCreateApikey = (params = {}) => { + return request({ + url: '/cntoai/create_model_apikey.dspy', + method: 'get', + params + }) +} diff --git a/f/web-kboss/src/api/newHome.js b/f/web-kboss/src/api/newHome.js index a5070db..7165756 100644 --- a/f/web-kboss/src/api/newHome.js +++ b/f/web-kboss/src/api/newHome.js @@ -101,3 +101,12 @@ export const todoCount = () => { method: 'post', }) } + + +// 获取token市集 +export const reqTokenMarket = () => { + return request({ + url: '/cntoai/model_management_customer_search.dspy', + method: 'get', + }) +} \ No newline at end of file diff --git a/f/web-kboss/src/components/modelManagement/ListingConfirmDialog.vue b/f/web-kboss/src/components/modelManagement/ListingConfirmDialog.vue new file mode 100644 index 0000000..52bc09a --- /dev/null +++ b/f/web-kboss/src/components/modelManagement/ListingConfirmDialog.vue @@ -0,0 +1,186 @@ + + + + + diff --git a/f/web-kboss/src/components/modelManagement/ModelDetailDialog.vue b/f/web-kboss/src/components/modelManagement/ModelDetailDialog.vue new file mode 100644 index 0000000..d568b0f --- /dev/null +++ b/f/web-kboss/src/components/modelManagement/ModelDetailDialog.vue @@ -0,0 +1,257 @@ + + + + + diff --git a/f/web-kboss/src/components/modelManagement/ModelFilter.vue b/f/web-kboss/src/components/modelManagement/ModelFilter.vue new file mode 100644 index 0000000..df6ade8 --- /dev/null +++ b/f/web-kboss/src/components/modelManagement/ModelFilter.vue @@ -0,0 +1,134 @@ + + + + + diff --git a/f/web-kboss/src/components/modelManagement/ModelStats.vue b/f/web-kboss/src/components/modelManagement/ModelStats.vue new file mode 100644 index 0000000..4a8f9d2 --- /dev/null +++ b/f/web-kboss/src/components/modelManagement/ModelStats.vue @@ -0,0 +1,126 @@ + + + + + diff --git a/f/web-kboss/src/layout/components/Navbar.vue b/f/web-kboss/src/layout/components/Navbar.vue index 6b32943..e8999f1 100644 --- a/f/web-kboss/src/layout/components/Navbar.vue +++ b/f/web-kboss/src/layout/components/Navbar.vue @@ -731,11 +731,17 @@ export default { }, async logout() { store.commit('tagsView/resetBreadcrumbState'); + store.commit('permission/RESET_ROUTES'); sessionStorage.removeItem("auths"); sessionStorage.removeItem("routes"); sessionStorage.removeItem("user"); sessionStorage.removeItem("userId"); sessionStorage.removeItem("org_type") + sessionStorage.removeItem("userType"); + sessionStorage.removeItem("orgType"); + sessionStorage.removeItem("roles"); + sessionStorage.removeItem("juese"); + sessionStorage.removeItem("jueseNew"); localStorage.removeItem('userId') localStorage.removeItem("auths"); localStorage.removeItem("routes"); @@ -752,10 +758,16 @@ export default { let url = window.location.href; await this.$router.push(`/login`); store.commit('tagsView/resetBreadcrumbState'); + store.commit('permission/RESET_ROUTES'); sessionStorage.removeItem("auths"); sessionStorage.removeItem("routes"); sessionStorage.removeItem("user"); sessionStorage.removeItem("userId"); + sessionStorage.removeItem("userType"); + sessionStorage.removeItem("orgType"); + sessionStorage.removeItem("roles"); + sessionStorage.removeItem("juese"); + sessionStorage.removeItem("jueseNew"); }, changeColor() { this.dialogFormVisible = false diff --git a/f/web-kboss/src/layout/components/Sidebar/SidebarItem.vue b/f/web-kboss/src/layout/components/Sidebar/SidebarItem.vue index da82b10..37615ef 100644 --- a/f/web-kboss/src/layout/components/Sidebar/SidebarItem.vue +++ b/f/web-kboss/src/layout/components/Sidebar/SidebarItem.vue @@ -97,12 +97,12 @@ export default { // 给嵌套菜单添加左边距 ::v-deep .nest-menu { .el-menu-item { - padding-left: 60px !important; // 或者您想要的任何值,比如10px + padding-left: 42px !important; // 子菜单稍微缩进,同时保留蓝底圆角选中态 } // 如果还有更深层的嵌套,可以继续设置 .nest-menu .el-menu-item { - padding-left: 100px !important; + padding-left: 64px !important; } } } diff --git a/f/web-kboss/src/layout/components/Sidebar/index.vue b/f/web-kboss/src/layout/components/Sidebar/index.vue index 9763d74..a977547 100644 --- a/f/web-kboss/src/layout/components/Sidebar/index.vue +++ b/f/web-kboss/src/layout/components/Sidebar/index.vue @@ -11,7 +11,7 @@ :text-color="variables.menuText" :unique-opened="true" :active-text-color="variables.menuActiveText" - :collapse-transition="false" + :collapse-transition="true" :default-active="activeMenu" mode="vertical" class="el-menu-vertical" @@ -25,6 +25,14 @@ /> + @@ -72,6 +80,12 @@ export default { mounted() { console.log("Sidebar mounted - 权限路由:", this.permissionRoutes); + }, + + methods: { + toggleSideBar() { + this.$store.dispatch("app/toggleSideBar"); + } } }; @@ -88,6 +102,8 @@ export default { display: flex; flex-direction: column; box-sizing: border-box; + position: relative; + transition: width 0.25s ease; } .menu-scroll-container { @@ -107,6 +123,36 @@ export default { } } + .sidebar-collapse-btn { + position: absolute; + right: 16px; + bottom: 18px; + z-index: 10; + display: flex; + align-items: center; + justify-content: center; + width: 42px; + height: 42px; + color: #8a94a6; + font-size: 18px; + cursor: pointer; + background: #ffffff; + border: 1px solid #eef2f7; + border-radius: 50%; + box-shadow: 0 8px 22px rgba(31, 45, 61, 0.12); + transition: all 0.25s ease; + + &:hover { + color: #1e6fff; + transform: translateY(-2px); + box-shadow: 0 12px 26px rgba(30, 111, 255, 0.18); + } + + &.collapsed { + right: 11px; + } + } + // 更具体的选择器 ::v-deep .el-menu-vertical { border: none; @@ -117,9 +163,37 @@ export default { .el-submenu__title, .el-menu-item { + height: 56px; + line-height: 56px; + margin: 6px 14px; + padding: 0 18px !important; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; + border-radius: 12px; + transition: all 0.25s ease; + } + + .el-submenu__title span, + .el-menu-item span { + transition: opacity 0.2s ease, transform 0.2s ease; + } + + .el-submenu__title:hover, + .el-menu-item:not(.is-active):hover { + color: #1e6fff !important; + background: #f4f8ff !important; + } + + .el-menu-item.is-active { + color: #ffffff !important; + background: linear-gradient(135deg, #1e6fff, #5d8dff) !important; + + } + + .el-menu-item.is-active i, + .el-menu-item.is-active span { + color: #ffffff !important; } // 子菜单容器 @@ -128,23 +202,18 @@ export default { .el-menu-item { // 激活的子菜单项 &.is-active { - background-color: #d7dafd !important; - color: #296ad9 !important; + background: linear-gradient(135deg, #1e6fff, #5d8dff) !important; + color: #ffffff !important; + &:before { - content: ''; - position: absolute; - left: 0; - top: 0; - bottom: 0; - width: 3px; - background-color: #296ad9; + display: none; } } // 非激活状态的悬停效果 &:not(.is-active):hover { - background-color: #f5f7fa !important; + background-color: #f4f8ff !important; } } } @@ -157,16 +226,24 @@ export default { .el-submenu__title, .el-menu-item { + margin: 6px 8px; + padding: 0 !important; text-overflow: clip; justify-content: center; } + .el-submenu__title span, + .el-menu-item span { + opacity: 0; + transform: translateX(-6px); + } + // 折叠状态下的子菜单激活样式 .el-menu--popup { .el-menu-item { &.is-active { - background-color: #f5f7fa !important; - color: #296ad9 !important; + color: #ffffff !important; + background: linear-gradient(135deg, #1e6fff 0%, #244fbd 100%) !important; } } } diff --git a/f/web-kboss/src/permission.js b/f/web-kboss/src/permission.js index 891d831..7420a79 100644 --- a/f/web-kboss/src/permission.js +++ b/f/web-kboss/src/permission.js @@ -11,7 +11,7 @@ import {getHomePath} from "@/views/setting/tools"; NProgress.configure({showSpinner: false}); // NProgress Configuration -const whiteList = ["/login", "/homePage", "/registrationPage", "/shoppingCart", "/homePageImage","/h5HomePage",'/H5about','/modelProductDetail','/ncmatchHome']; // no redirect whitelist +const whiteList = ["product","/login", "/homePage", "/registrationPage", "/shoppingCart", "/homePageImage","/h5HomePage",'/H5about','/modelProductDetail','/ncmatchHome']; // no redirect whitelist // 获取用户代理字符串 const userAgent = window.navigator.userAgent; diff --git a/f/web-kboss/src/router/index.js b/f/web-kboss/src/router/index.js index cc39ae4..55b152d 100644 --- a/f/web-kboss/src/router/index.js +++ b/f/web-kboss/src/router/index.js @@ -404,11 +404,36 @@ export const constantRoutes = [ * 需要根据用户角色动态加载的路由 */ export const asyncRoutes = [ + // 运营——模型管理 + { + path: "/modelManagement", + component: Layout, + meta: { + // title 是菜单上显示的文字,fullPath 用来和后端权限 path 对权限。 + title: "模型管理", + fullPath: "/modelManagement", + noCache: true, + // icon 是左侧菜单图标,roles 限制只有运营角色能看到。 + icon: "el-icon-cpu", + roles: ["运营"] + }, + children: [ + { + path: "", + component: () => import('@/views/modelManagement/modelManagement.vue'), + name: 'modelManagement', + meta: { + title: "模型管理", + fullPath: "/modelManagement", + noCache: true, + roles: ["运营"] + } + }, + ] + }, - - // 全部产品 - 一级菜单 - // 全部产品 - 一级菜单(无子路由) + // token市集 - 一级菜单(所有登录用户都能看到) { path: "/product", component: Layout, @@ -416,7 +441,7 @@ export const asyncRoutes = [ title: "全部产品", fullPath: "/product", noCache: true, - icon: "el-icon-goods" + icon: "el-icon-coin" }, children: [ { @@ -431,6 +456,66 @@ export const asyncRoutes = [ }, ] }, + // 令牌管理 - 一级菜单(所有登录用户都能看到) + { + path: "/tokenManagement", + component: Layout, + meta: { + title: "令牌管理", + fullPath: "/tokenManagement", + noCache: true, + icon: "el-icon-key" + }, + children: [ + { + path: "", + component: () => import('@/views/tokenManagement/index.vue'), + name: 'TokenManagement', + meta: { + title: "令牌管理", + fullPath: "/tokenManagement", + noCache: true, + icon: "el-icon-key" + } + }, + ] + }, + // 模型体验 + { + path: "/modelExperience", + component: () => import('@/views/modelManagement/Experience.vue'), + hidden: true, + name: 'modelExperience', + meta: { + title: "模型体验", + fullPath: "/modelExperience", + noCache: true + }, + }, + // 模型详情 + { + path: "/modelDetail", + component: () => import('@/views/modelManagement/ModelDetail.vue'), + hidden: true, + name: 'modelDetail', + meta: { + title: "模型详情", + fullPath: "/modelDetail", + noCache: true + }, + }, + // API文档 + { + path: "/modelApiDocument", + component: () => import('@/views/modelManagement/ApiDocument.vue'), + hidden: true, + name: 'modelApiDocument', + meta: { + title: "API文档", + fullPath: "/modelApiDocument", + noCache: true + }, + }, { path: "/overview", component: Layout, @@ -453,6 +538,32 @@ export const asyncRoutes = [ } ] }, + // 运营——运营报表 + { + path: "/operationReport", + component: Layout, + meta: { + title: "运营报表", + fullPath: "/operationReport", + noCache: true, + icon: "el-icon-data-analysis", + roles: ["运营"] + }, + children: [ + { + path: "", + component: () => import('@/views/operation/operationReport/index.vue'), + name: 'operationReport', + meta: { + title: "运营报表", + fullPath: "/operationReport", + noCache: true, + icon: "el-icon-data-analysis", + roles: ["运营"] + } + } + ] + }, { path: "/orderManagement", @@ -565,13 +676,15 @@ export const asyncRoutes = [ path: "/consultingMangement", name: 'ConsultingMangement', component: Layout, - meta: { title: "咨询表单", fullPath: "/consultingMangement", noCache: true, icon: "el-icon-s-platform" }, + // 咨询表单是表单/订单类入口,所以菜单图标用 el-icon-s-order。 + meta: { title: "咨询表单", fullPath: "/consultingMangement", noCache: true, icon: "el-icon-s-order" }, children: [ { path: "index", component: () => import('@/views/operation/consultingMangement/index.vue'), name: 'ConsultingMangement', - meta: { title: "咨询表单", fullPath: "/consultingMangement/index", noCache: true, icon: "el-icon-s-platform" }, + // 子路由也带 icon,单子菜单折叠成一级菜单时能继续显示图标。 + meta: { title: "咨询表单", fullPath: "/consultingMangement/index", noCache: true, icon: "el-icon-s-order" }, } ] }, @@ -1180,7 +1293,8 @@ export const asyncRoutes = [ component: Layout, name: "qualificationReview", redirect: "/qualificationReview/index", - meta: { fullPath: "/qualificationReview", title: "资质审核", noCache: true, icon: 'el-icon-s-home' }, + // 资质审核是审核/校验类菜单,所以用 el-icon-s-check。 + meta: { fullPath: "/qualificationReview", title: "资质审核", noCache: true, icon: 'el-icon-s-check' }, children: [ { path: "noApproveInfo", @@ -1203,7 +1317,8 @@ export const asyncRoutes = [ component: Layout, name: "approveMangement", redirect: "/approveMangement/index", - meta: { fullPath: "/approveMangement", title: "供需审核", noCache: true, icon: 'el-icon-s-home' }, + // 供需审核表示供给和需求两边协作审核,所以用 el-icon-s-cooperation。 + meta: { fullPath: "/approveMangement", title: "供需审核", noCache: true, icon: 'el-icon-s-cooperation' }, children: [ { path: "pendingPro", @@ -1263,12 +1378,14 @@ export const asyncRoutes = [ }, ] }, + { path: "/menuMangement", component: Layout, name: "menuMangement", redirect: "/menuMangement/index", - meta: { fullPath: "/menuMangement", title: "菜单管理", noCache: true, icon: 'el-icon-s-home' }, + // 菜单管理就是维护菜单配置,用 Element UI 的菜单图标。 + meta: { fullPath: "/menuMangement", title: "菜单管理", noCache: true, icon: 'el-icon-menu' }, children: [ { path: "index", @@ -1435,7 +1552,8 @@ export const asyncRoutes = [ { path: "/operation", component: Layout, redirect: "/operation/supplierManagement", meta: { - title: "运营", icon: "el-icon-s-tools", noCache: true, fullPath: "/operation", + // 运营是运营后台入口,用操作/运营类图标。 + title: "运营", icon: "el-icon-s-operation", noCache: true, fullPath: "/operation", }, children: [ { @@ -1559,7 +1677,7 @@ export const asyncRoutes = [ hidden: true, path: "colony", - title: "管理集群", + title: "管理集群", component: () => import("@/views/operation/computingCenterManagement/colony/index.vue"), name: "supplierManagement", meta: { diff --git a/f/web-kboss/src/store/modules/permission.js b/f/web-kboss/src/store/modules/permission.js index d302399..b59a9f4 100644 --- a/f/web-kboss/src/store/modules/permission.js +++ b/f/web-kboss/src/store/modules/permission.js @@ -1,24 +1,197 @@ -// permission.js - 修改后的完整代码 import { asyncRoutes, constantRoutes } from "@/router"; -// 获取用户代理字符串 -const userAgent = window.navigator.userAgent; +// 用浏览器 UA 判断当前是不是手机端,后面会按 PC / 手机过滤菜单。 +const MOBILE_UA_REGEXP = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i; -// 判断是否为移动设备 -const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(userAgent); +// 项目里用到的固定角色名,集中放这里,避免代码里到处写字符串。 +const CUSTOMER_ROLE = '客户'; +const OPERATION_ROLE = '运营'; -// 如果是移动设备,添加移动端首页路由和根路径重定向 +// 这个用户能看到订单管理里的特殊子菜单,比如历史订单和订单详情。 +const SPECIAL_ORDER_USER = 'ZhipuHZ'; + +// 超级管理员只放行这个一级菜单。 +const SUPER_ADMIN_ROUTE_PATH = '/superAdministrator'; + +// 所有登录用户都能访问的公共路由,不依赖后端 auths 和角色。hidden 路由不会显示在菜单里。 +const COMMON_ROUTE_PATHS = ['/product', '/tokenManagement', '/modelExperience', '/modelDetail', '/modelApiDocument']; + +// 运营角色需要额外补出来的菜单。 +const OPERATION_EXTRA_ROUTE_PATHS = ['/modelManagement', '/operationReport']; + +// 普通客户账号默认要补出来的基础菜单。 +const BASE_USER_ROUTE_PATHS = ['/orderManagement', '/resourceManagement']; + +// 客户角色额外能看到的一级菜单。 +const CUSTOMER_EXTRA_ROUTE_PATHS = [ + '/unsubscribeManagement', + '/informationPerfect', + '/rechargeManagement', + '/invoiceManagement', + '/workOrderManagement' +]; + +// 这些菜单只允许客户角色看到,非客户就算后端给了权限也不展示。 +const CUSTOMER_ONLY_ROUTE_PATHS = [ + '/overview', + ...CUSTOMER_EXTRA_ROUTE_PATHS +]; + +// 客户登录后必须能看到的入口菜单,不完全依赖后端 auths 返回。 +const CUSTOMER_ALWAYS_VISIBLE_ROUTE_PATHS = ['/overview']; + +// 订单管理里只给 SPECIAL_ORDER_USER 看的子菜单 path。 +const ORDER_CHILDREN_ONLY_FOR_SPECIAL_USER = ['HistoricalOrders', 'orderDetails']; + +const isMobile = MOBILE_UA_REGEXP.test(window.navigator.userAgent); + +// 把角色统一整理成数组,兼容 undefined、数组、逗号字符串这几种写法。 +function normalizeRoles(roles) { + if (!roles) { + return []; + } + + if (Array.isArray(roles)) { + return roles; + } + + if (typeof roles === 'string') { + return roles.split(',').filter(Boolean); + } + + return []; +} + +// 从 sessionStorage 里取 roles,取不到或格式坏了就当成没有角色。 +function getSessionRoles() { + try { + return JSON.parse(sessionStorage.getItem('roles') || '[]'); + } catch (error) { + console.warn('读取 roles 失败:', error); + return []; + } +} + +// 汇总当前用户的所有角色来源:接口参数、vuex、sessionStorage、新旧字段。 +function getCurrentRoles(params, rootState) { + return [ + ...normalizeRoles(params.roles), + ...normalizeRoles(rootState.user.roles), + ...normalizeRoles(getSessionRoles()), + ...normalizeRoles(sessionStorage.getItem('jueseNew')) + ]; +} + +// 判断当前用户是不是客户角色。 +function isCustomer(userRoles = []) { + return userRoles.includes(CUSTOMER_ROLE); +} + +// 把布尔值转成更好读的设备类型,后面的判断都用 pc / mobile。 +function getDeviceType(isMobileDevice) { + return isMobileDevice ? 'mobile' : 'pc'; +} + +// 判断路由 meta.roles 是否满足。没写 roles 的路由默认所有角色都能继续往下判断。 +function hasRouteRole(route, userRoles = []) { + const routeRoles = route.meta?.roles; + + if (!routeRoles || routeRoles.length === 0) { + return true; + } + + return routeRoles.some(role => userRoles.includes(role)); +} + +// 根据设备过滤路由:手机只要手机路由,PC 不要手机专用路由。 +function isRouteAllowedByDevice(route, deviceType) { + if (deviceType === 'mobile') { + return route.meta?.isMobile || route.meta?.isMobile === true; + } + + return route.meta?.isMobile !== true; +} + +// 在一组路由里按 path 找某个路由。 +function findRouteByPath(routes, path) { + return routes.find(route => route.path === path); +} + +// 后端 auths 里的 path 要和路由 meta.fullPath 对上,对上才算有权限。 +function routeHasPermission(route, permissions) { + return permissions.some(permission => permission.path === route.meta?.fullPath); +} + +// 客户专属菜单要再卡一层客户角色,防止非客户误展示。 +function canShowCustomerOnlyRoute(route, userRoles) { + return !CUSTOMER_ONLY_ROUTE_PATHS.includes(route.path) || isCustomer(userRoles); +} + +// 把所有动态路由的 fullPath 收集出来。后端返回 path 为空时,表示拥有全部权限。 +function getAllRoutePermissions(routes) { + const permissions = []; + + routes.forEach(route => { + if (route.meta?.fullPath) { + permissions.push({ path: route.meta.fullPath }); + } + + if (route.children) { + permissions.push(...getAllRoutePermissions(route.children)); + } + }); + + return permissions; +} + +// 复制路由对象。这里不能用 JSON 深拷贝,因为路由里的 component 是函数,会被 JSON 丢掉。 +function cloneRoute(route) { + const clonedRoute = { ...route }; + + if (route.meta) { + clonedRoute.meta = { ...route.meta }; + } + + if (route.children) { + clonedRoute.children = route.children.map(cloneRoute); + } + + return clonedRoute; +} + +// 根据 path 列表批量找到对应路由,没找到的自动过滤掉。 +function getRoutesByPath(routes, paths) { + return paths + .map(path => findRouteByPath(routes, path)) + .filter(Boolean); +} + +// 判断这个一级路由是不是已经加过了,避免菜单重复出现。 +function shouldAppendRoute(accessedRoutes, route) { + return !accessedRoutes.some(item => item.path === route.path); +} + +// 把缺少的路由补到最终菜单里,补之前会先去重并复制一份。 +function appendMissingRoutes(accessedRoutes, routesToAppend) { + routesToAppend.forEach(route => { + if (shouldAppendRoute(accessedRoutes, route)) { + accessedRoutes.push(cloneRoute(route)); + } + }); + + return accessedRoutes; +} + +// 如果是手机访问,额外把根路径导到 H5 首页,并注册 H5 首页菜单。 if (isMobile) { console.log("检测到移动设备,添加移动端路由"); - // 先添加根路径重定向到移动端首页 constantRoutes.unshift({ path: '/', redirect: '/h5HomePage', hidden: true }); - // 添加移动端首页路由 constantRoutes.push({ path: '/h5HomePage', name: 'H5HomePage', @@ -82,106 +255,165 @@ if (isMobile) { }); } -// 修复:更全面的路由过滤逻辑 +// 核心过滤函数:拿后端权限、角色和设备类型,一层层筛出最终可访问路由。 function filterAsyncRoutes(routes, permissions, userRoles = [], deviceType = 'pc') { const res = []; - // 定义需要客户角色才能访问的路由 - const customerOnlyRoutes = [ - "/product", "/overview", "/workOrderManagement", - "/unsubscribeManagement", "/informationPerfect", - "/rechargeManagement", "/invoiceManagement" - ]; - routes.forEach(route => { - // 创建路由副本 - const tmpRoute = { ...route }; + // 先复制一份,避免直接改原始 asyncRoutes。 + const tmpRoute = cloneRoute(route); - // 检查当前路由是否在权限列表中 - const hasPermission = permissions.some(p => p.path === route.meta?.fullPath); - - // 特殊处理:确保"全部产品"和"资源概览"这两个一级路由在客户角色下显示 - const isCriticalRoute = route.path === "/product" || route.path === "/overview"; - - // 检查是否为仅客户可访问的路由 - const isCustomerOnlyRoute = customerOnlyRoutes.includes(route.path); - - // 如果路由需要客户角色,但用户不是客户,则跳过 - if (isCustomerOnlyRoute && !userRoles.includes('客户')) { - return; // 跳过当前路由 + // 第一步:角色不符合,或者客户专属菜单但当前用户不是客户,直接跳过。 + if (!hasRouteRole(tmpRoute, userRoles) || !canShowCustomerOnlyRoute(route, userRoles)) { + return; } - // 新增:根据设备类型过滤路由 - if (deviceType === 'mobile' && !(route.meta?.isMobile || route.meta?.isMobile === true)) { - return; // 移动设备跳过非移动端路由 - } - if (deviceType === 'pc' && route.meta?.isMobile === true) { - return; // PC设备跳过移动端路由 + // 第二步:设备不符合也跳过,比如 PC 端不展示 H5 专用路由。 + if (!isRouteAllowedByDevice(route, deviceType)) { + return; } - // 如果当前路由有权限,则加入结果 - if (hasPermission) { + // 第三步:看后端 auths 里有没有当前路由的 fullPath。 + const hasPermission = routeHasPermission(route, permissions); + + // 第四步:客户首页入口特殊处理,客户登录后默认展示。 + const isAlwaysVisibleCustomerRoute = + CUSTOMER_ALWAYS_VISIBLE_ROUTE_PATHS.includes(route.path) && isCustomer(userRoles); + + // 有权限,或者是客户默认入口,就把这个路由放进最终菜单。 + if (hasPermission || isAlwaysVisibleCustomerRoute) { res.push(tmpRoute); - } - // 如果是关键路由且用户是客户,也要加入结果 - else if (isCriticalRoute && userRoles.includes('客户')) { - res.push(tmpRoute); - } - // 如果没有直接权限,但有子路由,递归处理子路由 - else if (tmpRoute.children) { + } else if (tmpRoute.children) { + // 父级没权限时继续看子级。只要子级有权限,父级也要保留,否则子菜单没地方挂。 const filteredChildren = filterAsyncRoutes(tmpRoute.children, permissions, userRoles, deviceType); + if (filteredChildren.length > 0) { tmpRoute.children = filteredChildren; - res.push(tmpRoute); // 即使父路由本身没有权限,只要有子路由有权限,也要保留父路由 + res.push(tmpRoute); } } - // 如果当前路由既没有权限,也没有有权限的子路由,则不添加到结果中 }); + return res; } -// 新增:为普通用户添加订单管理和资源管理路由 +// 给普通用户和客户补充固定菜单:订单、资源,以及客户专属的工单/充值/发票等。 function addUserRoutes(routes, userType, orgType, userRoles = [], deviceType = 'pc') { console.log("addUserRoutes - userType:", userType, "orgType:", orgType, "userRoles:", userRoles); const userRoutes = []; - // 修复:包含 orgType 为 2 和 3 的情况(公司客户和个人客户) - if (userType === 'user' || orgType == 2 || orgType == 3) { - const orderManagementRoute = routes.find(route => route.path === "/orderManagement"); - const resourceManagementRoute = routes.find(route => route.path === "/resourceManagement"); + // orgType 为 2 或 3 时也按客户账号处理。 + const isUserAccount = userType === 'user' || orgType == 2 || orgType == 3; - // 新增:根据设备类型过滤 - if (orderManagementRoute && (deviceType === 'pc' || orderManagementRoute.meta?.isMobile === true)) { - console.log("添加订单管理路由"); - userRoutes.push(JSON.parse(JSON.stringify(orderManagementRoute))); // 深拷贝 - } + if (isUserAccount) { + // 普通客户账号默认补订单管理和资源管理。 + const baseUserRoutes = getRoutesByPath(routes, BASE_USER_ROUTE_PATHS) + .filter(route => isRouteAllowedByDevice(route, deviceType)); - if (resourceManagementRoute && (deviceType === 'pc' || resourceManagementRoute.meta?.isMobile === true)) { - console.log("添加资源管理路由"); - userRoutes.push(JSON.parse(JSON.stringify(resourceManagementRoute))); // 深拷贝 - } + console.log("添加基础用户菜单路由:", baseUserRoutes.map(route => route.path)); + userRoutes.push(...baseUserRoutes); } - // 新增:为所有用户添加五个新的客户菜单,但只有客户角色才能看到 - const newCustomerRoutes = [ - routes.find(route => route.path === "/unsubscribeManagement"), - routes.find(route => route.path === "/informationPerfect"), - routes.find(route => route.path === "/rechargeManagement"), - routes.find(route => route.path === "/invoiceManagement"), - routes.find(route => route.path === "/workOrderManagement") - ].filter(route => { - // 过滤掉undefined,并且只有客户角色才能看到这些路由 - return route && userRoles.includes('客户') && - (deviceType === 'pc' || route.meta?.isMobile === true); - }); + if (isCustomer(userRoles)) { + // 只有客户角色才补客户专属菜单。 + const customerRoutes = getRoutesByPath(routes, CUSTOMER_EXTRA_ROUTE_PATHS) + .filter(route => isRouteAllowedByDevice(route, deviceType)); - console.log("添加新的客户菜单路由:", newCustomerRoutes.map(r => r.path)); - userRoutes.push(...newCustomerRoutes); + console.log("添加客户菜单路由:", customerRoutes.map(route => route.path)); + userRoutes.push(...customerRoutes); + } return userRoutes; } +// 运营角色额外补模型管理菜单,目前只在 PC 端展示。 +function addOperationRoutes(accessedRoutes, routes, userRoles = [], deviceType = 'pc') { + if (!userRoles.includes(OPERATION_ROLE) || deviceType !== 'pc') { + return accessedRoutes; + } + + return appendMissingRoutes(accessedRoutes, getRoutesByPath(routes, OPERATION_EXTRA_ROUTE_PATHS)); +} + +// token市集是公共菜单,所有登录用户都要能看到。 +function addCommonRoutes(accessedRoutes, routes, deviceType = 'pc') { + const commonRoutes = getRoutesByPath(routes, COMMON_ROUTE_PATHS) + .filter(route => isRouteAllowedByDevice(route, deviceType)); + + return appendMissingRoutes(accessedRoutes, commonRoutes); +} + +// 订单管理有两个特殊子菜单,只有 SPECIAL_ORDER_USER 能看到,其他用户过滤掉。 +function filterOrderChildrenByUser(routes, username) { + if (username === SPECIAL_ORDER_USER) { + console.log(`用户 ${username} 是 ${SPECIAL_ORDER_USER},保留所有订单子路由`); + return routes; + } + + return routes.map(route => { + const nextRoute = cloneRoute(route); + + // 找到订单管理后,移除特殊用户专属的子菜单。 + if (nextRoute.path === '/orderManagement' && nextRoute.children) { + console.log(`用户 ${username} 不是 ${SPECIAL_ORDER_USER},过滤订单管理子路由`); + nextRoute.children = nextRoute.children.filter(child => + !ORDER_CHILDREN_ONLY_FOR_SPECIAL_USER.includes(child.path) + ); + console.log('过滤后订单子路由:', nextRoute.children.map(child => child.path)); + } + + if (nextRoute.children) { + // 子路由里如果还有订单管理,也继续递归处理。 + nextRoute.children = filterOrderChildrenByUser(nextRoute.children, username); + } + + return nextRoute; + }); +} + +// 整理后端权限列表。如果包含空 path,就按“拥有全部动态路由权限”处理。 +function getPermissionList(auths = []) { + const permissions = JSON.parse(JSON.stringify(auths)); + const permissionPaths = permissions.map(item => item.path); + + if (permissionPaths.includes('')) { + return getAllRoutePermissions(asyncRoutes); + } + + return permissions; +} + +// 根据后端 auths 生成第一版可访问路由。没有 auths 就不展示动态菜单。 +function getAccessedRoutesByPermission(auths, userRoles, deviceType) { + if (!auths.length) { + return []; + } + + const permissions = getPermissionList(auths); + return filterAsyncRoutes(asyncRoutes, permissions, userRoles, deviceType); +} + +// 判断是不是超级管理员账号:用户名包含 admin,并且不是客户组织。 +function isSuperAdminUser(username, orgType) { + return username && username.includes('admin') && orgType != 2 && orgType != 3; +} + +// 超级管理员只拿超级管理员菜单;手机端不展示这个菜单。 +function getSuperAdminRoutes(deviceType) { + if (deviceType !== 'pc') { + return []; + } + + return getRoutesByPath(asyncRoutes, [SUPER_ADMIN_ROUTE_PATH]).map(cloneRoute); +} + +// 在已有权限菜单基础上,补充用户类型/客户角色需要固定展示的菜单。 +function addUserSpecificRoutes(accessedRoutes, userType, orgType, userRoles, deviceType) { + const userSpecificRoutes = addUserRoutes(asyncRoutes, userType, orgType, userRoles, deviceType); + return appendMissingRoutes(accessedRoutes, userSpecificRoutes); +} + const state = { routes: [], addRoutes: [], @@ -192,12 +424,19 @@ const state = { const mutations = { SET_ROUTES: (state, routes) => { console.log("MUTATION SET_ROUTES - received routes:", routes); + // addRoutes 只保存动态生成的菜单,方便 router.addRoutes 使用。 state.addRoutes = routes; sessionStorage.setItem("routes", JSON.stringify(routes)); - // 将移动端首页路由也包含在内 + // routes 是侧边栏最终读取的数据:基础路由 + 动态权限路由。 state.routes = constantRoutes.concat(routes); console.log("MUTATION SET_ROUTES - final state.routes:", state.routes); }, + RESET_ROUTES: (state) => { + // 退出登录或切换账号时,必须清掉内存里的旧菜单,否则不刷新页面会继续显示上个角色的菜单。 + state.routes = []; + state.addRoutes = []; + sessionStorage.removeItem("routes"); + }, SETUSERS: (state, user) => { state.users = user; }, @@ -226,131 +465,46 @@ const actions = { generateRoutes({ commit, rootState, state }, params) { console.log("ACTION generateRoutes - params:", params); return new Promise((resolve) => { - let accessedRoutes; - - // 从参数或sessionStorage中获取用户类型和组织类型 + // 1. 先拿到用户基础信息,优先用传进来的参数,没有就从 sessionStorage / vuex 兜底。 const userType = params.userType || sessionStorage.getItem('userType') || ''; const orgType = params.orgType || parseInt(sessionStorage.getItem('orgType')) || 0; - - // 获取用户角色(从store或sessionStorage) - const userRoles = rootState.user.roles || JSON.parse(sessionStorage.getItem('roles') || '[]'); - console.log("用户角色:", userRoles); - - // 获取用户名 const username = params.user || rootState.user.user || ''; - console.log("当前用户名:", username, "检查是否是ZhipuHZ:", username === 'ZhipuHZ'); + const userRoles = getCurrentRoles(params, rootState); + const deviceType = getDeviceType(state.isMobile); + const auths = params.auths ? JSON.parse(JSON.stringify(params.auths)) : []; - console.log("用户类型:", userType, "orgType:", orgType); + // 2. 判断是不是超级管理员,超级管理员走单独菜单逻辑。 + const isSuperAdmin = isSuperAdminUser(params.user, orgType); - // 确定设备类型 - const deviceType = state.isMobile ? 'mobile' : 'pc'; - console.log("设备类型:", deviceType); + console.log("用户角色:", userRoles); + console.log("当前用户名:", username, `检查是否是${SPECIAL_ORDER_USER}:`, username === SPECIAL_ORDER_USER); + console.log("用户类型:", userType, "orgType:", orgType, "设备类型:", deviceType); + console.log("ACTION generateRoutes - auths:", auths); - // 修复:包含 orgType 为 2 和 3 的情况 - if (params.user && params.user.includes("admin") && orgType != 2 && orgType != 3) { - // 管理员:只显示超级管理员菜单(仅PC端) - if (deviceType === 'pc') { - accessedRoutes = asyncRoutes.filter(item => item.path === '/superAdministrator'); - } else { - accessedRoutes = []; - } - } else { - const auths = params.auths ? JSON.parse(JSON.stringify(params.auths)) : []; - console.log("ACTION generateRoutes - auths:", auths); + // 3. 先生成第一版菜单:超级管理员只拿超管菜单,普通用户按后端 auths 过滤。 + let accessedRoutes = isSuperAdmin + ? getSuperAdminRoutes(deviceType) + : getAccessedRoutesByPermission(auths, userRoles, deviceType); - if (auths.length) { - // 确保 auths 中的 path 与路由 meta.fullPath 匹配 - const paths = auths.map((item) => { - return item.path; - }); - console.log("ACTION generateRoutes - paths from auths:", paths); + // 4. token市集是公共入口,所有登录用户都补上。 + accessedRoutes = addCommonRoutes(accessedRoutes, asyncRoutes, deviceType); - if (paths.includes("")) { - // 如果权限列表包含空路径,认为用户有所有权限 - accessedRoutes = asyncRoutes || []; - } else { - // 传入用户角色和设备类型 - accessedRoutes = filterAsyncRoutes(asyncRoutes, auths, userRoles, deviceType); - } - } else { - // 如果没有权限列表,不显示任何动态路由 - accessedRoutes = []; - } - - // 为普通用户添加订单管理和资源管理路由以及新的五个客户菜单 + if (!isSuperAdmin) { + // 5. 普通用户再补一些固定入口,比如订单、资源、客户专属菜单。 console.log("为用户添加特定路由"); - const userSpecificRoutes = addUserRoutes(asyncRoutes, userType, orgType, userRoles, deviceType); - - // 确保不重复添加路由,同时检查角色权限 - userSpecificRoutes.forEach(route => { - const isCustomerRoute = [ - "/workOrderManagement", "/unsubscribeManagement", "/informationPerfect", - "/rechargeManagement", "/invoiceManagement" - ].includes(route.path); - - // 如果是客户路由但用户不是客户,则不添加 - if (isCustomerRoute && !userRoles.includes('客户')) { - return; - } - - if (!accessedRoutes.some(r => r.path === route.path)) { - accessedRoutes.push(route); - } - }); - + accessedRoutes = addUserSpecificRoutes(accessedRoutes, userType, orgType, userRoles, deviceType); console.log("添加用户特定路由后的accessedRoutes:", accessedRoutes); } - // ========== 暴力过滤:直接修改 accessedRoutes ========== - // 遍历所有路由,找到 /orderManagement 路由,然后过滤它的子路由 - accessedRoutes = accessedRoutes.map(route => { - if (route.path === "/orderManagement") { - console.log("找到订单管理路由,准备过滤子路由,用户名:", username); + // 6. 运营角色额外补模型管理。 + accessedRoutes = addOperationRoutes(accessedRoutes, asyncRoutes, userRoles, deviceType); - // 创建路由副本 - const newRoute = { ...route }; - - if (newRoute.children) { - // 如果不是 ZhipuHZ 用户,过滤掉 HistoricalOrders 和 orderDetails 路由 - if (username !== 'ZhipuHZ') { - console.log(`用户 ${username} 不是 ZhipuHZ,过滤订单管理子路由`); - newRoute.children = newRoute.children.filter(child => - child.path !== 'HistoricalOrders' && child.path !== 'orderDetails' - ); - console.log(`过滤后子路由:`, newRoute.children.map(c => c.path)); - } else { - console.log(`用户 ${username} 是 ZhipuHZ,保留所有子路由`); - } - } - - return newRoute; - } - - // 对于其他路由,保持原样 - return route; - }); - - // 再次检查,确保没有遗漏的任何 orderManagement 路由 - accessedRoutes.forEach(route => { - if (route.children) { - route.children = route.children.filter(child => { - // 如果子路由是 orderManagement,也需要处理 - if (child.path === "/orderManagement") { - console.log("在子路由中找到订单管理路由,准备过滤,用户名:", username); - - if (child.children && username !== 'ZhipuHZ') { - child.children = child.children.filter(grandChild => - grandChild.path !== 'HistoricalOrders' && grandChild.path !== 'orderDetails' - ); - } - } - return true; - }); - } - }); + // 7. 最后处理订单管理里的特殊子菜单权限。 + accessedRoutes = filterOrderChildrenByUser(accessedRoutes, username); console.log("ACTION generateRoutes - 最终 calculated accessedRoutes:", accessedRoutes); + // 8. 保存到 vuex 和 sessionStorage,侧边栏会读取 state.permission.routes。 commit("SET_ROUTES", accessedRoutes); resolve(accessedRoutes); }); diff --git a/f/web-kboss/src/store/modules/user.js b/f/web-kboss/src/store/modules/user.js index 22b8fec..c91ee87 100644 --- a/f/web-kboss/src/store/modules/user.js +++ b/f/web-kboss/src/store/modules/user.js @@ -13,6 +13,13 @@ const safeToString = (value, defaultValue = '') => { return value.toString(); }; +const normalizeLoginRoles = (roles) => { + if (!roles || roles === 'None') return []; + if (Array.isArray(roles)) return roles; + if (typeof roles === 'string') return roles.split(',').filter(Boolean); + return []; +}; + // 从sessionStorage恢复状态 const getStoredState = () => { return { @@ -130,8 +137,11 @@ const actions = { // 修复:org_type 为 2 或 3 都表示客户 const userType = (org_type == 2 || org_type == 3) ? 'user' : 'admin'; - // 设置用户角色 - 如果是客户,则添加'客户'角色 - const userRoles = (org_type == 2 || org_type == 3) ? ['客户'] : ['管理员']; + // 使用接口返回的真实角色生成菜单;客户组织兜底补上“客户”角色。 + const userRoles = normalizeLoginRoles(response.roles); + if ((org_type == 2 || org_type == 3) && !userRoles.includes('客户')) { + userRoles.push('客户'); + } commit("SET_USER_TYPE", userType); // 确保 org_type 不为 undefined @@ -141,6 +151,8 @@ const actions = { console.log("登录用户类型:", userType, "org_type:", org_type, "用户角色:", userRoles); data ? commit("SET_AUTHS", data) : commit("SET_AUTHS", []); + resetRouter(); + commit("permission/RESET_ROUTES", null, { root: true }); const accessRoutes = await store.dispatch( "permission/generateRoutes", { @@ -151,7 +163,6 @@ const actions = { roles: userRoles // 新增:传递角色信息 } ) - resetRouter(); router.addRoutes(accessRoutes); resolve(response); } @@ -215,6 +226,7 @@ const actions = { commit("SET_AUTHS", []); removeToken(); resetRouter(); + commit("permission/RESET_ROUTES", null, { root: true }); // 清除sessionStorage sessionStorage.removeItem('user'); @@ -223,6 +235,8 @@ const actions = { sessionStorage.removeItem('orgType'); sessionStorage.removeItem('mybalance'); sessionStorage.removeItem('roles'); // 新增:清除角色信息 + sessionStorage.removeItem('juese'); + sessionStorage.removeItem('jueseNew'); // reset visited views and cached views // to fixed https://github.com/PanJiaChen/vue-element-admin/issues/2485 @@ -243,6 +257,8 @@ const actions = { commit("SET_USER", ""); commit("SET_AUTHS", []); removeToken(); + resetRouter(); + commit("permission/RESET_ROUTES", null, { root: true }); // 清除sessionStorage sessionStorage.removeItem('user'); @@ -250,6 +266,8 @@ const actions = { sessionStorage.removeItem('userType'); sessionStorage.removeItem('orgType'); sessionStorage.removeItem('roles'); // 新增:清除角色信息 + sessionStorage.removeItem('juese'); + sessionStorage.removeItem('jueseNew'); resolve(); }); }, diff --git a/f/web-kboss/src/styles/sidebar.scss b/f/web-kboss/src/styles/sidebar.scss index 8fd79e6..875de5f 100644 --- a/f/web-kboss/src/styles/sidebar.scss +++ b/f/web-kboss/src/styles/sidebar.scss @@ -25,7 +25,7 @@ // reset element-ui css .horizontal-collapse-transition { - transition: 0s width ease-in-out, 0s padding-left ease-in-out, 0s padding-right ease-in-out; + transition: width .28s ease, padding-left .28s ease, padding-right .28s ease; } .scrollbar-wrapper { @@ -100,11 +100,11 @@ .hideSidebar { .sidebar-container { - width: 54px !important; + width: 64px !important; } .main-container { - margin-left: 54px; + margin-left: 64px; } .submenu-title-noDropdown { diff --git a/f/web-kboss/src/views/homePage/components/topBox/index.vue b/f/web-kboss/src/views/homePage/components/topBox/index.vue index 6a4af0d..4de7b51 100644 --- a/f/web-kboss/src/views/homePage/components/topBox/index.vue +++ b/f/web-kboss/src/views/homePage/components/topBox/index.vue @@ -20,9 +20,9 @@

- 产品与服务 + 基础云

- +

@@ -116,7 +116,11 @@