From 3442f962148b7a65d26f703b1b4af9ce8d04c711 Mon Sep 17 00:00:00 2001
From: wangys <3401275564@qq.com>
Date: Sat, 13 Dec 2025 14:23:40 +0800
Subject: [PATCH] =?UTF-8?q?admin=E8=B7=B3=E8=BD=AC?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../platform/localSharedImportMap.js | 130 --------
.../AdminSidebarLayout.scss | 264 ++++++++++++++++
.../AdminSidebarLayout/AdminSidebarLayout.vue | 292 ++++++++++++++++++
.../src/layouts/AdminSidebarLayout/README.md | 214 +++++++++++++
.../layouts/SidebarLayout/SidebarLayout.vue | 28 +-
应用对应服务.drawio.xml | 136 --------
6 files changed, 795 insertions(+), 269 deletions(-)
delete mode 100644 urbanLifelineWeb/packages/platform/.__mf__temp/platform/localSharedImportMap.js
create mode 100644 urbanLifelineWeb/packages/platform/src/layouts/AdminSidebarLayout/AdminSidebarLayout.scss
create mode 100644 urbanLifelineWeb/packages/platform/src/layouts/AdminSidebarLayout/AdminSidebarLayout.vue
create mode 100644 urbanLifelineWeb/packages/platform/src/layouts/AdminSidebarLayout/README.md
delete mode 100644 应用对应服务.drawio.xml
diff --git a/urbanLifelineWeb/packages/platform/.__mf__temp/platform/localSharedImportMap.js b/urbanLifelineWeb/packages/platform/.__mf__temp/platform/localSharedImportMap.js
deleted file mode 100644
index 963040dd..00000000
--- a/urbanLifelineWeb/packages/platform/.__mf__temp/platform/localSharedImportMap.js
+++ /dev/null
@@ -1,130 +0,0 @@
-
-// Windows temporarily needs this file, https://github.com/module-federation/vite/issues/68
-
- import {loadShare} from "@module-federation/runtime";
- const importMap = {
-
- "element-plus": async () => {
- let pkg = await import("__mf__virtual/platform__prebuild__element_mf_2_plus__prebuild__.js");
- return pkg;
- }
- ,
- "vue": async () => {
- let pkg = await import("__mf__virtual/platform__prebuild__vue__prebuild__.js");
- return pkg;
- }
- ,
- "vue-router": async () => {
- let pkg = await import("__mf__virtual/platform__prebuild__vue_mf_2_router__prebuild__.js");
- return pkg;
- }
-
- }
- const usedShared = {
-
- "element-plus": {
- name: "element-plus",
- version: "2.12.0",
- scope: ["default"],
- loaded: false,
- from: "platform",
- async get () {
- if (false) {
- throw new Error(`Shared module '${"element-plus"}' must be provided by host`);
- }
- usedShared["element-plus"].loaded = true
- const {"element-plus": pkgDynamicImport} = importMap
- const res = await pkgDynamicImport()
- const exportModule = {...res}
- // All npm packages pre-built by vite will be converted to esm
- Object.defineProperty(exportModule, "__esModule", {
- value: true,
- enumerable: false
- })
- return function () {
- return exportModule
- }
- },
- shareConfig: {
- singleton: false,
- requiredVersion: "^2.12.0",
-
- }
- }
- ,
- "vue": {
- name: "vue",
- version: "3.5.25",
- scope: ["default"],
- loaded: false,
- from: "platform",
- async get () {
- if (false) {
- throw new Error(`Shared module '${"vue"}' must be provided by host`);
- }
- usedShared["vue"].loaded = true
- const {"vue": pkgDynamicImport} = importMap
- const res = await pkgDynamicImport()
- const exportModule = {...res}
- // All npm packages pre-built by vite will be converted to esm
- Object.defineProperty(exportModule, "__esModule", {
- value: true,
- enumerable: false
- })
- return function () {
- return exportModule
- }
- },
- shareConfig: {
- singleton: false,
- requiredVersion: "^3.5.25",
-
- }
- }
- ,
- "vue-router": {
- name: "vue-router",
- version: "4.6.3",
- scope: ["default"],
- loaded: false,
- from: "platform",
- async get () {
- if (false) {
- throw new Error(`Shared module '${"vue-router"}' must be provided by host`);
- }
- usedShared["vue-router"].loaded = true
- const {"vue-router": pkgDynamicImport} = importMap
- const res = await pkgDynamicImport()
- const exportModule = {...res}
- // All npm packages pre-built by vite will be converted to esm
- Object.defineProperty(exportModule, "__esModule", {
- value: true,
- enumerable: false
- })
- return function () {
- return exportModule
- }
- },
- shareConfig: {
- singleton: false,
- requiredVersion: "^4.6.3",
-
- }
- }
-
- }
- const usedRemotes = [
- {
- entryGlobalName: "shared",
- name: "shared",
- type: "module",
- entry: "http://localhost:5000/remoteEntry.js",
- shareScope: "default",
- }
-
- ]
- export {
- usedShared,
- usedRemotes
- }
-
\ No newline at end of file
diff --git a/urbanLifelineWeb/packages/platform/src/layouts/AdminSidebarLayout/AdminSidebarLayout.scss b/urbanLifelineWeb/packages/platform/src/layouts/AdminSidebarLayout/AdminSidebarLayout.scss
new file mode 100644
index 00000000..c3a40ef7
--- /dev/null
+++ b/urbanLifelineWeb/packages/platform/src/layouts/AdminSidebarLayout/AdminSidebarLayout.scss
@@ -0,0 +1,264 @@
+.sidebar-layout {
+ display: flex;
+ width: 100%;
+ height: 100vh;
+ overflow: hidden;
+}
+
+// ==================== 侧边栏 ====================
+.sidebar {
+ width: 220px;
+ height: 100%;
+ background: #F0EAF4;
+ display: flex;
+ flex-direction: column;
+ color: #333;
+ flex-shrink: 0;
+ transition: width 0.3s ease;
+ border-right: 1px solid rgba(0, 0, 0, 0.08);
+
+ &.collapsed {
+ width: 64px;
+
+ .sidebar-header {
+ padding: 16px 12px;
+ justify-content: center;
+
+ .logo {
+ justify-content: center;
+ }
+
+ .collapse-btn {
+ position: static;
+ margin-left: 0;
+ }
+ }
+
+ .nav-item {
+ justify-content: center;
+ padding: 12px;
+ }
+
+ .user-section {
+ justify-content: center;
+ padding: 16px 12px;
+ }
+ }
+}
+
+// 侧边栏头部
+.sidebar-header {
+ padding: 16px 20px;
+ border-bottom: 1px solid rgba(0, 0, 0, 0.08);
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+}
+
+.collapse-btn {
+ width: 28px;
+ height: 28px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: 6px;
+ cursor: pointer;
+ color: #888;
+ transition: all 0.2s;
+
+ &:hover {
+ background: rgba(124, 58, 237, 0.1);
+ color: #7c3aed;
+ }
+}
+
+.logo {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+
+ .logo-img {
+ width: 40px;
+ height: 40px;
+ border-radius: 6px;
+ object-fit: contain;
+ background: #fff;
+ padding: 2px;
+ }
+
+ .logo-text {
+ font-size: 16px;
+ font-weight: 600;
+ color: #333;
+ }
+}
+
+// 导航菜单
+.nav-menu {
+ flex: 1;
+ overflow-y: auto;
+ padding: 12px 0;
+
+ &::-webkit-scrollbar {
+ width: 4px;
+ }
+
+ &::-webkit-scrollbar-thumb {
+ background: rgba(0, 0, 0, 0.2);
+ border-radius: 4px;
+ }
+}
+
+.nav-section {
+ padding: 8px 0;
+}
+
+.nav-item {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ padding: 14px 20px;
+ margin-bottom: 4px;
+ cursor: pointer;
+ transition: all 0.2s ease;
+ color: #555;
+ font-size: 14px;
+
+ &:hover {
+ background: rgba(124, 58, 237, 0.1);
+ color: #7c3aed;
+ }
+
+ &.active {
+ background: rgba(124, 58, 237, 0.15);
+ color: #7c3aed;
+ font-weight: 500;
+ }
+
+ .el-icon {
+ font-size: 18px;
+ flex-shrink: 0;
+ }
+
+ span {
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+}
+
+// 用户信息
+.user-section {
+ padding: 16px 20px;
+ border-top: 1px solid rgba(0, 0, 0, 0.08);
+ cursor: pointer;
+ transition: background 0.2s;
+
+ &:hover {
+ background: rgba(124, 58, 237, 0.05);
+ }
+
+ .user-info-wrapper {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ }
+
+ .user-avatar {
+ flex-shrink: 0;
+ }
+
+ .user-name {
+ font-size: 14px;
+ font-weight: 500;
+ color: #333;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+}
+
+// ==================== 主内容区 ====================
+.main-content {
+ flex: 1;
+ height: 100%;
+ overflow: hidden;
+ background: #fff;
+ position: relative;
+}
+
+// iframe 容器
+.iframe-container {
+ width: 100%;
+ height: 100%;
+ display: flex;
+ flex-direction: column;
+ position: relative;
+}
+
+.iframe-header {
+ height: 56px;
+ padding: 0 24px;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ border-bottom: 1px solid #e5e7eb;
+ background: #fafafa;
+ flex-shrink: 0;
+}
+
+.iframe-title {
+ font-size: 16px;
+ font-weight: 600;
+ color: #333;
+}
+
+.content-iframe {
+ flex: 1;
+ width: 100%;
+ height: 100%;
+ border: none;
+ background: #fff;
+}
+
+.iframe-loading {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 12px;
+ color: #7c3aed;
+ font-size: 14px;
+ z-index: 10;
+
+ .el-icon {
+ font-size: 32px;
+ }
+}
+
+// ==================== 响应式 ====================
+@media (max-width: 768px) {
+ .sidebar {
+ width: 64px;
+
+ &:not(.collapsed) {
+ width: 220px;
+ position: fixed;
+ left: 0;
+ top: 0;
+ z-index: 1000;
+ box-shadow: 2px 0 8px rgba(0, 0, 0, 0.1);
+ }
+ }
+
+ .iframe-header {
+ padding: 0 16px;
+
+ .iframe-title {
+ font-size: 14px;
+ }
+ }
+}
diff --git a/urbanLifelineWeb/packages/platform/src/layouts/AdminSidebarLayout/AdminSidebarLayout.vue b/urbanLifelineWeb/packages/platform/src/layouts/AdminSidebarLayout/AdminSidebarLayout.vue
new file mode 100644
index 00000000..24209620
--- /dev/null
+++ b/urbanLifelineWeb/packages/platform/src/layouts/AdminSidebarLayout/AdminSidebarLayout.vue
@@ -0,0 +1,292 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/urbanLifelineWeb/packages/platform/src/layouts/AdminSidebarLayout/README.md b/urbanLifelineWeb/packages/platform/src/layouts/AdminSidebarLayout/README.md
new file mode 100644
index 00000000..c05051ff
--- /dev/null
+++ b/urbanLifelineWeb/packages/platform/src/layouts/AdminSidebarLayout/README.md
@@ -0,0 +1,214 @@
+# SidebarLayout 侧边栏布局组件
+
+## 功能特性
+
+### 🎯 核心功能
+- **侧边栏菜单导航**:左侧固定侧边栏,支持折叠/展开
+- **双模式内容区**:
+ - **路由模式**:普通路由页面渲染
+ - **iframe 模式**:嵌入外部应用(招标助手、泰豪小电、智能体编排)
+- **用户信息展示**:底部用户头像和下拉菜单
+- **响应式设计**:支持移动端适配
+
+### 📱 菜单配置
+
+```typescript
+const menuItems: MenuItem[] = [
+ {
+ key: 'home',
+ label: '工作台',
+ icon: 'Grid',
+ path: '/home',
+ type: 'route'
+ },
+ {
+ key: 'bidding',
+ label: '招标助手',
+ icon: 'Document',
+ iframeUrl: 'http://localhost:5002',
+ type: 'iframe'
+ },
+ {
+ key: 'service',
+ label: '泰豪小电',
+ icon: 'Service',
+ iframeUrl: 'http://localhost:5003',
+ type: 'iframe'
+ },
+ {
+ key: 'workflow',
+ label: '智能体编排',
+ icon: 'Connection',
+ iframeUrl: 'http://localhost:3000', // Dify 地址
+ type: 'iframe'
+ }
+]
+```
+
+## 使用方式
+
+### 在路由中使用
+
+```typescript
+// router/index.ts
+import { SidebarLayout } from '@/layouts'
+
+const routes = [
+ {
+ path: '/',
+ component: SidebarLayout,
+ children: [
+ {
+ path: '/home',
+ component: () => import('@/views/Home.vue')
+ },
+ {
+ path: '/chat',
+ component: () => import('@/views/Chat.vue')
+ }
+ ]
+ }
+]
+```
+
+### 在 App.vue 中使用
+
+```vue
+
+
+
+
+
+```
+
+## 菜单项类型
+
+```typescript
+interface MenuItem {
+ key: string // 唯一标识
+ label: string // 显示名称
+ icon: string // Element Plus 图标组件名
+ path?: string // 路由路径(route 类型必需)
+ iframeUrl?: string // iframe URL(iframe 类型必需)
+ type: 'route' | 'iframe' // 菜单类型
+}
+```
+
+## iframe 应用说明
+
+### 1. 招标助手 (Bidding)
+- **端口**:5002
+- **URL**:http://localhost:5002
+- **说明**:招投标业务管理系统
+
+### 2. 泰豪小电 (Service)
+- **端口**:5003
+- **URL**:http://localhost:5003
+- **说明**:智能客服工单管理系统
+
+### 3. 智能体编排 (Workflow)
+- **端口**:3000
+- **URL**:http://localhost:3000
+- **说明**:Dify 智能体编排界面
+
+## 样式自定义
+
+### 主题色调整
+
+```scss
+// 修改侧边栏背景色
+.sidebar {
+ background: #F0EAF4; // 当前淡紫色背景
+}
+
+// 修改激活项颜色
+.nav-item.active {
+ background: rgba(124, 58, 237, 0.15);
+ color: #7c3aed;
+}
+```
+
+### 侧边栏宽度
+
+```scss
+.sidebar {
+ width: 220px; // 展开宽度
+
+ &.collapsed {
+ width: 64px; // 折叠宽度
+ }
+}
+```
+
+## 功能说明
+
+### 侧边栏折叠
+- 点击头部箭头图标可切换折叠/展开状态
+- 折叠后只显示图标,展开后显示图标+文字
+
+### iframe 加载
+- 自动显示加载中状态
+- 支持刷新按钮重新加载
+- 显示当前应用标题
+
+### 用户操作
+- **个人中心**:跳转到 /profile
+- **系统设置**:跳转到 /settings
+- **退出登录**:跳转到 /login
+
+## 注意事项
+
+1. **跨域问题**:确保 iframe 应用允许被嵌入
+ ```nginx
+ # nginx 配置
+ add_header X-Frame-Options "SAMEORIGIN";
+ # 或者
+ add_header Content-Security-Policy "frame-ancestors 'self' http://localhost:5001";
+ ```
+
+2. **端口配置**:确保对应服务已启动
+ - platform: 5001
+ - bidding: 5002
+ - workcase: 5003
+ - dify: 3000
+
+3. **路由同步**:iframe 模式不会改变浏览器 URL
+
+4. **通信机制**:如需与 iframe 通信,使用 postMessage API
+
+## 扩展建议
+
+### 添加新菜单项
+
+```typescript
+// 在 menuItems 数组中添加
+{
+ key: 'new-app',
+ label: '新应用',
+ icon: 'Plus',
+ iframeUrl: 'http://localhost:5004',
+ type: 'iframe'
+}
+```
+
+### 动态菜单加载
+
+```typescript
+// 从 API 获取菜单配置
+const loadMenus = async () => {
+ const response = await fetch('/api/menus')
+ menuItems.value = await response.json()
+}
+```
+
+### 权限控制
+
+```typescript
+const menuItems = computed(() => {
+ return allMenuItems.filter(item =>
+ hasPermission(item.key)
+ )
+})
+```
diff --git a/urbanLifelineWeb/packages/platform/src/layouts/SidebarLayout/SidebarLayout.vue b/urbanLifelineWeb/packages/platform/src/layouts/SidebarLayout/SidebarLayout.vue
index ce97dbac..0c149be4 100644
--- a/urbanLifelineWeb/packages/platform/src/layouts/SidebarLayout/SidebarLayout.vue
+++ b/urbanLifelineWeb/packages/platform/src/layouts/SidebarLayout/SidebarLayout.vue
@@ -44,9 +44,9 @@
个人中心
-
+
- 系统设置
+ 管理后台
@@ -125,6 +125,7 @@ const collapsed = ref(false)
const activeMenu = ref('home')
const iframeLoading = ref(false)
const iframeRef = ref()
+const hasAdmin = ref(false)
// 从 LocalStorage 获取用户名
function getUserName(): string {
@@ -163,6 +164,13 @@ function loadMenuFromStorage(): MenuItem[] {
view.service === 'platform' && // 只显示 platform 服务的视图
!view.url?.startsWith('/admin') // 排除 admin 路由(由 AdminSidebar 管理)
)
+ hasAdmin.value = userViews.filter((view: any) =>
+ view.layout === 'SidebarLayout' &&
+ !view.parentId &&
+ view.type === 1 && // type 1 是侧边栏菜单
+ view.service === 'platform' && // 只显示 platform 服务的视图
+ view.url?.startsWith('/admin') // 排除 admin 路由(由 AdminSidebar 管理)
+ ).length>0
// 按 orderNum 排序
sidebarViews.sort((a: any, b: any) => (a.orderNum || 0) - (b.orderNum || 0))
@@ -251,7 +259,21 @@ const handleUserCommand = (command: string) => {
router.push('/profile')
break
case 'settings':
- router.push('/settings')
+ // 跳转到管理后台(AdminSidebarLayout)
+ // 查找第一个 admin 路由
+ const loginDomainStr = localStorage.getItem('loginDomain')
+ if (loginDomainStr) {
+ const loginDomain = JSON.parse(loginDomainStr)
+ const userViews = loginDomain.userViews || []
+ const adminViews = userViews.filter((view: any) =>
+ view.service === 'platform' && view.url?.startsWith('/admin')
+ )
+ if (adminViews.length > 0) {
+ // 按 orderNum 排序,跳转到第一个
+ adminViews.sort((a: any, b: any) => (a.orderNum || 0) - (b.orderNum || 0))
+ router.push(adminViews[0].url)
+ }
+ }
break
case 'logout':
localStorage.clear()
diff --git a/应用对应服务.drawio.xml b/应用对应服务.drawio.xml
deleted file mode 100644
index fb0e8d5a..00000000
--- a/应用对应服务.drawio.xml
+++ /dev/null
@@ -1,136 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-