web iframe结构实现

This commit is contained in:
2025-12-13 14:13:31 +08:00
parent e002f0d989
commit 1776aa2d1e
53 changed files with 3280 additions and 275 deletions

View File

@@ -1,192 +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/icons-vue": async () => {
let pkg = await import("__mf__virtual/shared__prebuild___mf_0_element_mf_2_plus_mf_1_icons_mf_2_vue__prebuild__.js");
return pkg;
}
,
"axios": async () => {
let pkg = await import("__mf__virtual/shared__prebuild__axios__prebuild__.js");
return pkg;
}
,
"element-plus": async () => {
let pkg = await import("__mf__virtual/shared__prebuild__element_mf_2_plus__prebuild__.js");
return pkg;
}
,
"vue": async () => {
let pkg = await import("__mf__virtual/shared__prebuild__vue__prebuild__.js");
return pkg;
}
,
"vue-router": async () => {
let pkg = await import("__mf__virtual/shared__prebuild__vue_mf_2_router__prebuild__.js");
return pkg;
}
}
const usedShared = {
"@element-plus/icons-vue": {
name: "@element-plus/icons-vue",
version: "2.3.2",
scope: ["default"],
loaded: false,
from: "shared",
async get () {
if (false) {
throw new Error(`Shared module '${"@element-plus/icons-vue"}' must be provided by host`);
}
usedShared["@element-plus/icons-vue"].loaded = true
const {"@element-plus/icons-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: "^2.3.2",
}
}
,
"axios": {
name: "axios",
version: "1.13.2",
scope: ["default"],
loaded: false,
from: "shared",
async get () {
if (false) {
throw new Error(`Shared module '${"axios"}' must be provided by host`);
}
usedShared["axios"].loaded = true
const {"axios": 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: "^1.13.2",
}
}
,
"element-plus": {
name: "element-plus",
version: "2.12.0",
scope: ["default"],
loaded: false,
from: "shared",
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: "shared",
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: "shared",
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 = [
]
export {
usedShared,
usedRemotes
}

View File

@@ -0,0 +1,90 @@
<template>
<div class="iframe-view">
<iframe
v-if="iframeUrl"
:src="iframeUrl"
class="iframe-content"
frameborder="0"
@load="handleLoad"
/>
<div v-else class="iframe-error">
<el-icon class="error-icon"><WarningFilled /></el-icon>
<p>无效的 iframe 地址</p>
</div>
<div v-if="loading" class="iframe-loading">
<el-icon class="is-loading"><Loading /></el-icon>
<span>加载中...</span>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { useRoute } from 'vue-router'
import { Loading, WarningFilled } from '@element-plus/icons-vue'
const route = useRoute()
const loading = ref(true)
// 从路由 meta 中获取 iframe URL
const iframeUrl = computed(() => {
return route.meta.iframeUrl as string || ''
})
function handleLoad() {
loading.value = false
}
onMounted(() => {
console.log('[IframeView] 加载 iframe:', iframeUrl.value)
})
</script>
<style lang="scss" scoped>
.iframe-view {
position: relative;
width: 100%;
height: 100%;
overflow: hidden;
}
.iframe-content {
width: 100%;
height: 100%;
border: none;
}
.iframe-error {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
color: var(--el-text-color-secondary);
.error-icon {
font-size: 48px;
margin-bottom: 16px;
color: var(--el-color-warning);
}
}
.iframe-loading {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: var(--el-bg-color);
gap: 12px;
.el-icon {
font-size: 32px;
color: var(--el-color-primary);
}
}
</style>

View File

@@ -1,3 +1,6 @@
export * from './fileupload'
export * from './base'
export * from './dynamicFormItem'
export * from './dynamicFormItem'
// 通用视图组件
export { default as IframeView } from './iframe/IframeView.vue'

View File

@@ -43,6 +43,12 @@ export interface AppRuntimeConfig {
};
publicImgPath: string;
publicWebPath: string;
// 单点登录配置
sso?: {
platformUrl: string; // platform 平台地址
workcaseUrl: string; // workcase 服务地址
biddingUrl: string; // bidding 服务地址
};
features?: {
enableDebug?: boolean;
enableMockData?: boolean;
@@ -92,6 +98,15 @@ const devConfig: AppRuntimeConfig = {
publicImgPath: 'http://localhost:5173/img',
publicWebPath: 'http://localhost:5173',
// 单点登录配置
// 推荐开发环境也通过nginx访问http://localhost
// 备选直接访问各服务端口platformUrl: 'http://localhost:5001'
sso: {
platformUrl: '/', // 通过nginx访问platform
workcaseUrl: '/workcase', // 通过nginx访问workcase
biddingUrl: '/bidding' // 通过nginx访问bidding
},
features: {
enableDebug: true,
enableMockData: false
@@ -132,6 +147,13 @@ const prodDefaultConfig: AppRuntimeConfig = {
publicImgPath: '/img',
publicWebPath: '/',
// 单点登录配置生产环境通过nginx代理
sso: {
platformUrl: '/',
workcaseUrl: '/workcase',
biddingUrl: '/bidding'
},
features: {
enableDebug: false,
enableMockData: false
@@ -218,6 +240,13 @@ export const APP_CONFIG = {
publicImgPath: config.publicImgPath,
publicWebPath: config.publicWebPath,
// 单点登录配置
sso: config.sso || {
platformUrl: '/',
workcaseUrl: '/workcase',
biddingUrl: '/bidding'
},
// 功能开关
features: config.features || {}
};

View File

@@ -0,0 +1,18 @@
<template>
<div class="blank-layout">
<router-view />
</div>
</template>
<script setup lang="ts">
// BlankLayout空白布局只显示内容无侧边栏、无header
// 适用于全屏页面,如聊天页面、独立功能页等
</script>
<style scoped lang="scss">
.blank-layout {
width: 100%;
height: 100vh;
overflow: hidden;
}
</style>

View File

@@ -0,0 +1 @@
export { default as BlankLayout } from './BlankLayout/BlankLayout.vue'

View File

@@ -128,6 +128,8 @@ export interface TbSysViewDTO extends BaseDTO {
type?: number;
/** 视图类型 route\iframe*/
viewType?: string;
/** 所属服务 platform\workcase\bidding */
service?: string;
/** 布局 */
layout?: string;
/** 排序 */

View File

@@ -184,15 +184,22 @@ function generateRouteFromMenu(
route.component = component
} else {
// 组件加载失败,使用 404
route.component = config.notFoundComponent || (() => Promise.resolve({ default: { template: '<div>404</div>' } }))
route.component = config.notFoundComponent || (() => import('vue').then(({ h }) => ({
default: {
render() { return h('div', '404') }
}
})))
}
} else {
// 使用路由占位组件
route.component = () => Promise.resolve({
route.component = () => import('vue').then(({ h, resolveComponent }) => ({
default: {
template: '<router-view />'
render() {
const RouterView = resolveComponent('RouterView')
return h(RouterView)
}
}
})
}))
}
}
@@ -677,17 +684,29 @@ function generateSimpleRoute(
let component: any
if (isIframe) {
// iframe 类型:使用占位组件
component = iframePlaceholder || (() => Promise.resolve({
default: {
template: '<div class="iframe-placeholder"></div>'
}
}))
// iframe 类型:使用占位组件用于显示iframe内容
// 路由路径使用 url 字段(应该设置为不冲突的路径,如 /app/workcase
component = iframePlaceholder || (() => import('vue').then(({ h }) => ({
default: {
render() { return h('div', { class: 'iframe-placeholder' }, 'Loading...') }
}
})))
} else if (view.component) {
// route 类型:加载实际组件
component = config.viewLoader(view.component)
if (!component) {
if (verbose) console.warn('[路由生成] 组件加载失败:', view.component)
if (verbose) console.warn('[路由生成] 组件加载失败:', view.component, '使用占位组件')
// 使用占位组件,避免路由无效
const errorMsg = `组件加载失败: ${view.component}`
component = () => import('vue').then(({ h }) => ({
default: {
render() {
return h('div', {
style: { padding: '20px', color: 'red' }
}, errorMsg)
}
}
}))
}
}
@@ -753,11 +772,14 @@ function generateSimpleRoute(
route.component = component
} else if (!component && hasChildren) {
// 没有组件,只有子视图(路由容器)
route.component = () => Promise.resolve({
route.component = () => import('vue').then(({ h, resolveComponent }) => ({
default: {
template: '<router-view />'
render() {
const RouterView = resolveComponent('RouterView')
return h(RouterView)
}
}
})
}))
route.children = []
// 添加子路由
@@ -785,5 +807,51 @@ function generateSimpleRoute(
return null
}
// 处理layout如果视图指定了layout且不是作为Root的子路由且有有效组件需要包裹layout
const viewLayout = (view as any).layout
if (viewLayout && !asRootChild && route.component && config.layoutMap[viewLayout]) {
if (verbose) {
console.log('[路由生成] 为视图添加布局:', view.name, '布局:', viewLayout, '路径:', routePath)
}
// 创建layout路由将原路由的组件作为其子路由
const layoutRoute: RouteRecordRaw = {
path: routePath,
name: view.viewId,
component: config.layoutMap[viewLayout],
meta: {
...route.meta,
layout: viewLayout // 标记使用的布局
},
children: [
{
path: '',
name: `${view.viewId}_content`,
component: route.component,
meta: route.meta
}
]
}
// 如果原路由有其他children子视图也添加到layout路由的children中
if (route.children && route.children.length > 0) {
// 跳过第一个空路径的子路由(如果存在)
const otherChildren = route.children.filter((child: any) => child.path !== '')
if (otherChildren.length > 0) {
layoutRoute.children!.push(...otherChildren)
}
}
if (verbose) {
console.log('[路由生成] Layout路由生成完成:', {
path: layoutRoute.path,
name: layoutRoute.name,
childrenCount: layoutRoute.children?.length
})
}
return layoutRoute
}
return route
}

View File

@@ -36,7 +36,8 @@ export default defineConfig({
'./components': './src/components/index.ts',
'./components/FileUpload': './src/components/fileupload/FileUpload.vue',
'./components/DynamicFormItem': './src/components/dynamicFormItem/DynamicFormItem.vue',
'./components/iframe/IframeView.vue': './src/components/iframe/IframeView.vue',
// ========== API 模块 ==========
'./api': './src/api/index.ts',
'./api/auth': './src/api/auth/auth.ts',
@@ -54,7 +55,13 @@ export default defineConfig({
'./types/base': './src/types/base/index.ts',
'./types/auth': './src/types/auth/index.ts',
'./types/file': './src/types/file/index.ts',
'./types/sys': './src/types/sys/index.ts'
'./types/sys': './src/types/sys/index.ts',
// ========== Config 配置模块 ==========
'./config': './src/config/index.ts',
// ========== Layouts 布局模块 ==========
'./layouts': './src/layouts/index.ts'
},
// 共享依赖(重要:避免重复加载)
shared: {