feat/vtk-3d-view #7
|
|
@ -0,0 +1,341 @@
|
|||
# 原版 Web 系统「子页面嵌入挂载」最小侵入改造设计
|
||||
|
||||
- 日期:2026-06-17
|
||||
- 状态:**设计稿 v2(已经 opus 双评审 + 代码实测修订,可据以实现)**
|
||||
- 涉及仓库:
|
||||
- **Web 端(被改造方)**:`D:\Git\lanbingtech\commercial-admin`(Vue3 + Vite + Arco,hash 路由)
|
||||
- **客户端(接入方)**:`D:\Git\lanbingtech\geopro`(Qt + `QWebEngineView`)
|
||||
- 需求背景:客户端需把原版 web 系统的**单个子功能页**(如"系统管理"下的组织/用户/角色,以及项目空间下的数据视图等)**裸挂**进客户端窗口,不带原系统的"标题栏 + 左菜单 + 页签"外壳,复用既有页面与鉴权。
|
||||
- 设计约束(用户已确认):
|
||||
1. **尽可能少改动原有代码**——既有函数保持行为零变化,优先"新增"而非"修改"。
|
||||
2. token 注入:客户端用 `QWebEngineProfile` 预置 localStorage(首选)或 `loadStarted` 注入,token **不进 URL**。
|
||||
3. 挂载范围:**同时支持租户空间页面(`space=2`)与项目空间页面(`space=3`,需 `projectId`)**。
|
||||
|
||||
> v2 修订摘要(详见 §10):EmbedLayout 必须写成**懒加载函数**(否则被 `cloneDeep` 拷坏);`generateEmbedRoutes` 必须定义在 **store 闭包内**;**独立 `QWebEngineProfile` 由建议升为强制**,并在生成路由前清理 pinia 持久化残留;token 注入首选 profile 预置。
|
||||
|
||||
---
|
||||
|
||||
## 0. 为什么不能直接挂 URL(约束根因,已对照代码)
|
||||
|
||||
| 阻碍 | 证据 | 含义 |
|
||||
|---|---|---|
|
||||
| 所有业务页面都是 `Layout` 子路由 | `stores/modules/route.js` `transformComponentView`:`'layout'→Layout`;`layout/index.vue` = Asider+Header+Tabs+Main | 任何路径渲染都带整套外壳 |
|
||||
| 业务路由运行时才动态注册 | `router/guard.js` `beforeEach` → `generateRoutes()`/`generateSpaceRoutes()` → `router.addRoute` | 不跑守卫,目标路由不存在,直接访问 404 |
|
||||
| 进入需登录态 + 空间上下文 | `getToken()` 读 `localStorage['token']`;`space==2/3`;项目空间依赖 `projectStore.projectId`(`computed(projectItem.id)`) | 必须先备好 token / projectId 再生成路由 |
|
||||
|
||||
> 结论:裸挂子页 = 必须同时解决 **①去外壳、②触发动态路由生成、③补登录态/projectId** 三件事。
|
||||
|
||||
---
|
||||
|
||||
## 1. 总体方案:新增「embed 引导入口 + EmbedLayout」,叠加而非改造
|
||||
|
||||
新增固定路由 `/embed` 作为引导页:读 URL 参数 → 备好上下文 → 调用**新增的** `generateEmbedRoutes`(用 `EmbedLayout` 包裹,无外壳)→ `router.replace` 到目标子页。正常登录链路**完全不经过**这些新增物。
|
||||
|
||||
### 1.1 嵌入 URL 规范(统一入口,参数驱动)
|
||||
|
||||
hash 路由下统一入口为 `/#/embed`,目标子页靠 query 指定:
|
||||
|
||||
```
|
||||
http://<host>/#/embed?space=<2|3>&projectId=<pid>&target=<encodeURIComponent(叶子页路径)>
|
||||
```
|
||||
|
||||
| 参数 | 必填 | 说明 |
|
||||
|---|---|---|
|
||||
| `space` | 是 | `2`=租户空间,`3`=项目空间 |
|
||||
| `projectId` | 仅 `space=3` | 项目空间页面所属项目 id |
|
||||
| `target` | 是 | 叶子菜单路由路径,经 `encodeURIComponent` 编码 |
|
||||
|
||||
示例:
|
||||
- 系统管理·用户列表(租户):`http://<host>/#/embed?space=2&target=%2ForganiMange%2FuserList`
|
||||
- 项目空间·数据视图:`http://<host>/#/embed?space=3&projectId=123&target=%2FprojectSpace%2FdataView`
|
||||
|
||||
> token 不进 URL(由客户端 profile 预置注入,见 §4)。`target` 即 `/organiMange/userList` 这类原叶子菜单路径,`encodeURIComponent` 后 `/`→`%2F`。
|
||||
|
||||
### 1.2 改造性质:一次性框架级,非逐页改造
|
||||
|
||||
§2 列的 5 处改动全部是**框架级**(路由层 + 一个引导页 + 一个空壳布局),**与任何具体业务页无关**。改造完成后:
|
||||
|
||||
- **嵌入任意叶子菜单页 = 只改 URL 的 `target`/`space`/`projectId`**,业务页代码一行不动。
|
||||
- 不存在"为某个页面单独做嵌入适配"的工作。
|
||||
|
||||
唯一两个"非纯 URL"例外且均已收敛、非逐页:
|
||||
- **D 类**(极少数读 `currentSpace` 的页):引导页对 `space=2` **统一**补 `getEnterpriseUserInfoFun`(§3.2),一处兜底覆盖,非逐页。
|
||||
- **详情页带参**:属行点击详情、**不在叶子菜单范围**(§11)。
|
||||
|
||||
---
|
||||
|
||||
## 2. 改动分级(核心:既有逻辑零修改)
|
||||
|
||||
| 项 | 文件 | 性质 | 是否改既有逻辑 |
|
||||
|---|---|---|---|
|
||||
| ① 新增 `EmbedLayout.vue` | `commercial-admin/src/layout/EmbedLayout.vue` | 全新文件 | 否 |
|
||||
| ② 新增引导页 `embed/index.vue` | `commercial-admin/src/views/embed/index.vue` | 全新文件 | 否 |
|
||||
| ③ 注册 `/embed` 固定路由 | `commercial-admin/src/router/route.js` | 在 `constantRoutes` **插入一项(404 兜底之前)** | 否(不动既有项) |
|
||||
| ④ 新增 `generateEmbedRoutes` | `commercial-admin/src/stores/modules/route.js` | **store 闭包内新增函数** + 给 `formatAsyncRoutes` 加 `export` | 既有函数体零修改 |
|
||||
| ⑤ guard 顶部早返回 | `commercial-admin/src/router/guard.js` | **唯一的"修改"**:`beforeEach` 顶部加 `if(embed) return` | 既有分支原样保留 |
|
||||
| ⑥ 客户端注入 token + 拼 URL + 独立 profile | `geopro` 客户端新增 `QWebEngineView` | 客户端侧新增 | 否 |
|
||||
|
||||
> 唯一动到既有执行路径的是 ⑤,做成"顶部早返回"。验收基线:**不带 `/embed`、且 `EMBED_MODE` 未点亮时,执行路径与改造前一致**。
|
||||
|
||||
---
|
||||
|
||||
## 3. Web 端详细设计
|
||||
|
||||
### 3.1 ① `EmbedLayout.vue`(新文件)
|
||||
|
||||
仅保留 `<a-config-provider>` + `<router-view>`,**不引入** Asider/Header/Tabs/DkFooter,**也不要 keep-alive**(`cacheList` 由 Tabs 组件填充,embed 无 Tabs 故恒为空,keep-alive 是死代码,去掉更简单——见 §10 M2)。
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<a-config-provider :locale="arcoLocale">
|
||||
<a-layout class="embed-main">
|
||||
<router-view v-slot="{ Component, route }">
|
||||
<component :is="Component" v-if="Component" :key="route.path" />
|
||||
</router-view>
|
||||
</a-layout>
|
||||
</a-config-provider>
|
||||
</template>
|
||||
<script setup>
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { arcoLocales } from '@/plugins/locales/i18n.js'
|
||||
defineOptions({ name: 'EmbedLayout' })
|
||||
const { locale } = useI18n()
|
||||
const arcoLocale = computed(() => arcoLocales[locale.value])
|
||||
</script>
|
||||
<style scoped>.embed-main{width:100%;height:100%;overflow:hidden}</style>
|
||||
```
|
||||
|
||||
> `useI18n`/`arcoLocales` 是全局插件,裸挂可用(评审已核实)。
|
||||
|
||||
### 3.2 ② 引导页 `views/embed/index.vue`(新文件)
|
||||
|
||||
无业务渲染(仅 loading/错误占位)。`onMounted` 内按序引导,**关键顺序**:先清理 pinia 持久化残留 → 写 `projectItem.id`(项目空间)→ 生成路由 → replace。
|
||||
|
||||
```js
|
||||
onMounted(async () => {
|
||||
try {
|
||||
sessionStorage.setItem('EMBED_MODE', '1') // 点亮 embed 标志(仅本 profile/tab)
|
||||
const { space, projectId, target } = route.query // token 已由客户端预置/注入 localStorage
|
||||
const sp = Number(space)
|
||||
|
||||
// ① 清理持久化残留,防止跨会话/复用 profile 时脏读(见 §10 S3/M3)
|
||||
routeStore.$reset()
|
||||
projectStore.$reset()
|
||||
|
||||
// ② 设置空间标识(廉价、无害,避免子页/外壳读到错误值)
|
||||
routeStore.isProjectSpace = (sp === 3)
|
||||
|
||||
await userStore.getInfo() // 复用既有;401 时 /auth/user/info 被拦截器排除→仅 reject
|
||||
|
||||
if (sp === 3) {
|
||||
projectStore.projectItem.id = projectId // 先写!generateEmbedRoutes(3) 读 projectId=computed(id)
|
||||
} else {
|
||||
await appStore.getEnterpriseUserInfoFun() // space=2 默认补企业信息(写 currentSpace),覆盖"企业空间"类叶子页
|
||||
}
|
||||
await routeStore.generateEmbedRoutes(sp) // 新增函数
|
||||
|
||||
router.replace(decodeURIComponent(target)) // 进入目标子页,EmbedLayout 生效
|
||||
} catch (e) {
|
||||
console.error('[embed] bootstrap failed:', e)
|
||||
// 显示错误占位,不主动跳登录(embed 无登录交互)
|
||||
errorMsg.value = '页面加载失败,请检查登录态或权限'
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
### 3.3 ③ 注册 `/embed`(`route.js`,插入 404 兜底之前)
|
||||
|
||||
`constantRoutes` 中 `/:pathMatch(.*)*` 是兜底项,`/embed` 必须**插在它之前**(精确路由优先,避免匹配歧义):
|
||||
|
||||
```js
|
||||
export const constantRoutes = [
|
||||
{ path: '/redirect', /* ...既有不动... */ },
|
||||
{
|
||||
path: '/embed',
|
||||
name: 'Embed',
|
||||
component: () => import('@/views/embed/index.vue'),
|
||||
meta: { hidden: false },
|
||||
},
|
||||
{ path: '/:pathMatch(.*)*', /* 404 兜底,保持在最后 */ },
|
||||
{ path: '/403', /* ... */ },
|
||||
]
|
||||
```
|
||||
|
||||
### 3.4 ④ `generateEmbedRoutes`(`route.js` **store 闭包内**新增)
|
||||
|
||||
复用既有 `formatAsyncRoutes`/`flatMultiLevelRoutes`,**仅把顶层路由的 `component` 由 `Layout` 换成 `EmbedLayout`**(动态路由顶层节点恒为外壳包裹层;`flatMultiLevelRoutes` 只展平 children、不动顶层 component——评审已核实机制成立)。
|
||||
|
||||
**两处必须遵守(否则跑不起来,见 §10 S1/必错-1):**
|
||||
1. `EmbedLayout` 写成**懒加载函数**,与 `Layout` 一致。原因:`generateEmbedRoutes` 内沿用既有 `cloneDeep`,lodash 对**函数**按引用返回、对**组件 options 对象**会深拷贝并拷坏。静态 `import EmbedLayout` 是对象 → 必被拷坏。
|
||||
2. 函数定义在 `storeSetup()` **闭包内**(与 `generateRoutes`/`generateSpaceRoutes` 并列),否则 `setRoutes`/`router`/`projectStore` 未定义(它们是闭包内符号)。
|
||||
|
||||
```js
|
||||
// 模块顶层:给既有 formatAsyncRoutes 加 export(纯导出,零行为变化)
|
||||
// 并新增懒加载的 EmbedLayout(不要静态 import!)
|
||||
const EmbedLayout = () => import('@/layout/EmbedLayout.vue')
|
||||
|
||||
// —— 以下定义在 storeSetup() 内部,与 generateRoutes/generateSpaceRoutes 并列 ——
|
||||
const generateEmbedRoutes = (space) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const p = space === 3
|
||||
? getProjectRouteLimts({ projectId: projectStore.projectId })
|
||||
: getUserRoute()
|
||||
p.then((res) => {
|
||||
try {
|
||||
const tree = space === 3
|
||||
? toArrayTree(res.data)
|
||||
: searchTree(res.data, (i) => Number.parseInt(i.clientType) === 2)
|
||||
const asyncRoutes = formatAsyncRoutes(tree) // 既有函数,零修改
|
||||
asyncRoutes.forEach((r) => { r.component = EmbedLayout }) // ← 去外壳关键一步(顶层换壳)
|
||||
setRoutes(asyncRoutes)
|
||||
const flat = flatMultiLevelRoutes(cloneDeep(asyncRoutes)) // cloneDeep 对函数式组件按引用,安全
|
||||
for (const route of flat) {
|
||||
if (!isHttp(route.path) && !router.hasRoute(route.name)) router.addRoute(route)
|
||||
}
|
||||
resolve()
|
||||
} catch (e) { reject(e) }
|
||||
}).catch(reject)
|
||||
})
|
||||
}
|
||||
// return { ...原有, generateEmbedRoutes }
|
||||
```
|
||||
|
||||
> `generateRoutes`/`generateSpaceRoutes`/`transformComponentView` **函数体一行不动**,正常登录仍走它们(带 `Layout`)。
|
||||
> `router.hasRoute(route.name)` 是对既有 `generateSpaceRoutes` 里 `hasRoute(route)`(误传对象)的修正用法,仅用于新增函数内部,不影响既有。
|
||||
|
||||
### 3.5 ⑤ guard 顶部早返回(`guard.js` 唯一修改)
|
||||
|
||||
`router.beforeEach` **最顶部**(`NProgress.start()` 之后)加:
|
||||
|
||||
```js
|
||||
// —— embed 嵌入模式:放行,鉴权 + 路由生成由 /embed 引导页自管 ——
|
||||
if (to.path === '/embed' || sessionStorage.getItem('EMBED_MODE') === '1') {
|
||||
NProgress.done()
|
||||
return next()
|
||||
}
|
||||
```
|
||||
|
||||
既有所有分支(登录判断/空间检测/白名单/版本检测)**整段保留**。
|
||||
**注意(见 §10 S2)**:早返回是 embed 下唯一放行路径——`router.replace(target)` 的二次 `beforeEach` 靠 `EMBED_MODE==='1'` 兜住,跳过 `generateSpaceRoutes`(避免再生成 Layout 版路由覆盖 EmbedLayout 版)。因此 `EMBED_MODE` 的可靠性是硬约束,必须配合独立 profile(§4)与生成前 `$reset`(§3.2)。
|
||||
|
||||
---
|
||||
|
||||
## 4. 客户端 ↔ Web「嵌入契约」(geopro 侧)
|
||||
|
||||
**强制:每个 embed 视图使用独立 `QWebEngineProfile`**,与系统浏览器登录态、与正常登录链路隔离,避免 pinia 持久化(routeStore/projectStore/userStore 均 `persist` 到 localStorage)跨链路污染。
|
||||
|
||||
token 注入**首选 profile 预置**(彻底消除时序竞态),`loadStarted` 注入为备选:
|
||||
|
||||
```cpp
|
||||
// 首选:独立 profile + 预置脚本,在文档创建早期写 localStorage
|
||||
auto* profile = new QWebEngineProfile(QStringLiteral("geopro-embed"), parent); // 独立 profile
|
||||
QWebEngineScript s;
|
||||
s.setInjectionPoint(QWebEngineScript::DocumentCreation);
|
||||
s.setWorldId(QWebEngineScript::MainWorld);
|
||||
s.setSourceCode(QStringLiteral(
|
||||
"localStorage.setItem('token','%1');"
|
||||
"localStorage.setItem('refleshToken','%2');").arg(token, refleshToken));
|
||||
profile->scripts()->insert(s);
|
||||
auto* page = new QWebEnginePage(profile, parent);
|
||||
auto* view = new QWebEngineView(parent);
|
||||
view->setPage(page);
|
||||
|
||||
// 目标页/空间/项目id 走 query(非敏感);hash 路由下 query 在 # 之后
|
||||
// 租户(系统管理类): http://<host>/#/embed?space=2&target=%2ForganiMange%2FuserList
|
||||
// 项目空间(数据视图): http://<host>/#/embed?space=3&projectId=<pid>&target=%2FprojectSpace%2FdataView
|
||||
view->setUrl(QUrl(url));
|
||||
```
|
||||
|
||||
约定:`target` 用 `encodeURIComponent` 编码(含 `/`),引导页 `decodeURIComponent` 还原。
|
||||
|
||||
---
|
||||
|
||||
## 5. 关键边界与风险(已对照代码确认)
|
||||
|
||||
1. **项目空间生成顺序**:`generateEmbedRoutes(3)` → `getProjectRouteLimts({projectId: projectStore.projectId})`,`projectId=computed(projectItem.id)`。**必须先 `projectItem.id=` 再生成**,否则拉空菜单 → `target` 404。`projectItem` 是 reactive,直接赋值即触发 computed(评审已背书)。
|
||||
2. **target 须在该账号权限内**:后端按角色过滤菜单,无权限页生成后仍无该路由 → 仍 404。属权限问题。
|
||||
3. **EMBED_MODE 隔离(安全关键)**:用 `sessionStorage`,**严禁持久化**;并**强制独立 profile**。否则普通访问可能误进 embed 分支、跳过鉴权(见 §3.5/§10 S3)。
|
||||
4. **pinia 持久化残留**:`routeStore`/`projectStore`/`userStore` 均 `persist`(localStorage)。引导页生成前必须 `$reset`,否则复用 profile 时回灌旧 projectId / 旧路由表(§10 M3)。
|
||||
5. **后端零改动**:复用 `getUserRoute`/`getProjectRouteLimts`,不碰接口。
|
||||
6. **token 时序**:profile 预置脚本在 DocumentCreation 注入,早于 Vue 初始化与首个守卫,无竞态(优于 `loadStarted`,见 §10 可能-1)。
|
||||
7. **token 失效行为**:`http.js:70` 仅对**非** `/auth/user/info` 的 401 弹"重新登录"并跳 `/login`。embed 下 token 失效会弹标准 401 框→无外壳的登录页;属可接受边界,由客户端决定是否重新引导。
|
||||
8. **外壳态依赖**:`isProjectSpace`/`currentSpace` 仅外壳组件与"企业空间"专页消费(已 grep 确认)。普通业务子页裸挂安全;目标若为企业空间专页(`enterpriseManage/enterpriseSpace`、`setting/profile` 等),引导页需补 `appStore.getEnterpriseUserInfoFun()`。
|
||||
|
||||
---
|
||||
|
||||
## 6. 非目标(本轮 OUT)
|
||||
|
||||
- 不改造外壳组件本身;不为子页做独立 Vite 打包入口;不在 web 端做 token 刷新的嵌入式交互;不用"CSS 隐藏外壳"临时方案(外壳仍实例化、有副作用,已否决)。
|
||||
|
||||
---
|
||||
|
||||
## 7. 验收标准
|
||||
|
||||
1. **回归基线**:不带 `/embed` 且 `EMBED_MODE` 未点亮的所有访问行为与改造前一致;`generateRoutes`/`generateSpaceRoutes`/`transformComponentView`/`formatAsyncRoutes` 函数体未改(仅 `formatAsyncRoutes` 加 `export`)。
|
||||
2. **租户空间挂载**:`#/embed?space=2&target=%2ForganiMange%2FuserList` 渲染用户列表且**无外壳**,数据正常。
|
||||
3. **项目空间挂载**:`#/embed?space=3&projectId=<pid>&target=%2FprojectSpace%2FdataView` 正常渲染、projectId 正确。
|
||||
4. **token 不外泄**:URL/历史/日志无 token;localStorage token 由 profile 预置成功。
|
||||
5. **隔离**:普通浏览器新 tab 访问业务页,`EMBED_MODE` 不存在、守卫照常生效。
|
||||
|
||||
---
|
||||
|
||||
## 8. 回滚
|
||||
|
||||
新增物删除即回滚;guard 早返回分支删除即恢复。无数据/接口副作用。
|
||||
|
||||
---
|
||||
|
||||
## 9. 实现顺序
|
||||
|
||||
1. Web:EmbedLayout.vue → embed/index.vue → route.js 注册 + `generateEmbedRoutes`(闭包内、懒加载壳)→ guard 早返回。
|
||||
2. 浏览器手验 `space=2`(console 预置 `localStorage.token` + `sessionStorage.EMBED_MODE=1` 后访问 `#/embed?...`)。
|
||||
3. 验 `space=3`(有效 projectId)。
|
||||
4. 客户端独立 profile + 预置脚本接入,端到端联调。
|
||||
|
||||
---
|
||||
|
||||
## 10. 评审修正记录(opus 双评审 + commercial-admin 代码实测)
|
||||
|
||||
**已采纳为硬性修正(不改跑不起来):**
|
||||
- **S1 / 必错-2(闭包作用域)**:`generateEmbedRoutes` 定义在 `storeSetup()` 内(`route.js:103-183` 区间),否则 `setRoutes`/`router`/`projectStore` ReferenceError。→ 已落到 §3.4。
|
||||
- **必错-1(cloneDeep 拷坏组件)**:`EmbedLayout` 必须懒加载函数 `() => import()`。lodash `cloneDeep` 对作为对象属性的**函数按引用返回**(故原系统 `Layout=()=>import()` 正常),对**静态导入的组件 options 对象会深拷贝**并破坏 Vue 内部标识。→ 已落到 §3.4。
|
||||
- **S3(持久化污染 + 隔离)**:`routeStore/projectStore/userStore` 均 `persist`(localStorage)。EmbedLayout 路由会被持久化,复用 profile 时污染正常链路/反之。→ 独立 profile 升为**强制**(§4),生成前 `$reset`(§3.2)。
|
||||
- **M3(projectId 脏读)**:`dataView` setup 阶段快照式读 `projectId`(`views/projectSpace/dataView/index.vue:132`),叠加 persist 可能回灌旧值。→ `projectStore.$reset()` + 先赋值后生成(§3.2)。
|
||||
- **token 时序(可能-1)**:`loadStarted` 的 `runJavaScript` 异步排队,不保证早于首个守卫。→ 改用 profile 预置脚本 DocumentCreation 注入为首选(§4/§5.6)。
|
||||
- **/embed 插入位置(需确认-4)**:插在 `/:pathMatch(.*)*` 之前(§3.3)。
|
||||
- **keep-alive(M2)**:EmbedLayout 去掉 keep-alive(cacheList 恒空)(§3.1)。
|
||||
|
||||
**已核实成立、予以背书(原 spec 正确):**
|
||||
- 顶层 `component` 换 EmbedLayout 去外壳机制成立(`formatAsyncRoutes` 顶层节点 component 即外壳,`flatMultiLevelRoutes` 不动顶层 component)。
|
||||
- `projectStore.projectItem.id = projectId` 触发 computed,先赋值后生成"必要且充分"。
|
||||
- `searchTree`/`toArrayTree` 用法与既有 `generateRoutes`/`generateSpaceRoutes` 一致。
|
||||
- `useI18n`/`arcoLocales` 全局可用。
|
||||
- 回归基线(仅 `formatAsyncRoutes` 加 `export` + guard 顶部分支)可验证。
|
||||
|
||||
**结论**:方向正确、工作量可控;上述硬性修正纳入 v2 后,**可据本 spec 进入实现**。"必错-1/必错-2"是两个不改连跑都跑不起来的点,实现时务必遵守 §3.4 两条约束。
|
||||
|
||||
**已核实安全(原列为"现场确认",实为静态可查,已查清):**
|
||||
- 外壳 provide/inject 依赖:`grep` 确认 `src/layout` + `App.vue` **零 `provide()`**;外壳对事件总线**零 emit**。两个首批目标页 `organiMange/userList.vue`、`projectSpace/dataView/index.vue` 自身**既不 `inject()` 也不订阅任何 bus**。现存 `inject()` 仅在 `projectSpace/datasetInfo` 子树(colorLevel/contourLevel/GprHeader),是页面内部父子 provide,与外壳无关。`coustomEventBus`(`utils/event-bus.js`)定义后全仓无消费方。→ **裸挂这两页对外壳上下文依赖为 0,安全**。其他目标页若纳入,按同样三步静态核对(外壳 provide?页面 inject?总线发射方是否在外壳?)即可,无需等运行时。
|
||||
|
||||
**实现前必须用真实菜单接口数据核验(唯一未离线证实项):**
|
||||
- **顶层菜单 component 恒为 `'layout'`**(见 §11 E 类):`generateEmbedRoutes` 换壳逻辑 `asyncRoutes.forEach(r => r.component = EmbedLayout)` 假设每个顶层路由都是外壳包裹层。需抓一次真实 `getUserRoute`(space=2)与 `getProjectRouteLimts`(space=3)响应,确认所有顶层节点 `component === 'layout'`(而非直接页面或 `ParentView`)。若存在非 layout 顶层,换壳会渲染空白 → 改为"仅替换 component 原为 'layout' 的顶层节点"。
|
||||
|
||||
**仍需留意:**
|
||||
- embed 路由 name 与正常路由同名:独立 profile 下 embed SPA 不会生成 Layout 版路由,无冲突;若未来同一 SPA 既登录又 embed,需给 embed 路由 name 加前缀。
|
||||
|
||||
---
|
||||
|
||||
## 11. 覆盖范围:所有叶子菜单可导航页面(已代码实测)
|
||||
|
||||
**结论:本方案完整覆盖"所有叶子菜单可导航到的页面"。** 因为叶子菜单导航 = 只给路径,进入所需上下文仅 `path + space + projectId`,恰为 embed 契约所提供;且这些页路由与正常登录一致生成,仅顶层外壳由 `Layout` 换为 `EmbedLayout`。
|
||||
|
||||
### 为何成立(实测依据)
|
||||
- **叶子菜单页不带行参数**:靠 `?id=` 进入的详情页(`projectMange/projectConfiguration/details`、`templateManage/details`、`datasetInfo` 等)是从列表页**行点击 `router.push` 带参**进入的(证据:`projectMange/configuration.vue:91`、`projectSpace/templateManage/index.vue:105`),**不属于叶子菜单**,不在本范围内。
|
||||
- **项目空间叶子页自取数据**:`abnormalBody/List`、`dataMange`、`projectStructure` 等自调 `getGsTreeFun`/`queryProjectGsStruct`(证据:`grep` 命中 `src/views/projectSpace/*`),不依赖前序页面预加载的 store → 可独立裸挂。
|
||||
- **页内向自身详情下钻仍可用**:叶子页里行点击 push 到详情页时,该详情路由**也已在同空间一次性生成(EmbedLayout 版)**,故详情同样无外壳渲染。
|
||||
|
||||
### 边界(非阻断,已收敛)
|
||||
- **D 类(极少数读外壳态的叶子页)**:如"企业空间"读 `currentSpace`。引导页对 `space=2` 默认补 `appStore.getEnterpriseUserInfoFun()`(§3.2 已落),`isProjectSpace` 亦在引导页设置 → 覆盖。
|
||||
- **C 类(仅当嵌入页内"跨空间/回首页")**:单个叶子页本身不受影响;只有离开本页、跨租户↔项目空间或回工作台的跳转会脱离 embed 语境。属"离开该叶子页"的范畴,非"该叶子页能否嵌"。
|
||||
- **E 类(唯一结构性前提)**:换壳假设顶层菜单 component 恒为 `'layout'`。本系统所有业务页现均带外壳,强烈暗示成立;但需用真实菜单接口数据核验(见 §10「实现前必须核验」)。
|
||||
- **F 类(不适合/无意义)**:login/redirect/error;以及"导航中枢"类(工作台、项目列表,职责即切空间/进项目,单独嵌无意义)。这些通常也不是普通叶子功能菜单。
|
||||
Loading…
Reference in New Issue