feat(shell): T5 前端·壳与管理端框架——fork notice/front 基线并剥领域
build / build (push) Successful in 3m10s

- A 类主题 loader 原文接入 + env-config 引导 + vite /static 与 /api(8086) 代理(P6/P11)
- GetUserMod 左导航:utils/menuNav.ts 单点实现 path 空锚点跳过、分组收敛、icon 兜底、面包屑基线(对齐 Notice 13afb22 refined 版)
- alova 封装 + api/module/base.ts(getUserMod 可选 appCode,P2 定名;H4 前空树不回落,P3)
- 路由骨架一 SPA 四形态:/admin/{forms,data,stats,dicts}、/f/:code、/app/:appCode/*、/my;self!=top 剥壳(方案 §3.3)
- 规范件落位:tableActions.ts(UserBase 同构源,正文 MD5 不变)、TablePageAlovaNew + useElementHeight、page-wrapper yh-fill 骨架
- view-ui-plus 精确锁 1.3.24;本地零 layout.css/icons.css;avatar 两件由 YschTheme sync 下发
- 剥除:notice 领域 views/api/mock/menu 兜底、alova-vant(mobiapi)、ztree+jquery 与重依赖(按需回补,见 service/docs/T5-report.md 问题单 P52)
This commit is contained in:
chenxing
2026-10-14 20:35:41 +08:00
parent cebc32fbef
commit fdd9aaaa3b
81 changed files with 12410 additions and 1473 deletions
+130 -10
View File
@@ -1,15 +1,135 @@
<script setup lang="ts">
import { onBeforeUnmount, onMounted, reactive, ref } from "vue";
import layout from "./components/layout.vue";
import { useRoute, useRouter } from "vue-router";
import config from "@/assets/config/host.config";
import { baseApi } from "@/api/module/base";
import { resolveMenuPage } from "@/utils/menuRoute";
import { buildNavTree, firstNavPage, isSingleNav, type NavNode } from "@/utils/menuNav";
import { Message } from "view-ui-plus";
import type { IModule } from "@/types/Basics";
const router = useRouter();
const route = useRoute();
const isReady = ref(false);
/**
* 页面即组件(方案 §3.3):被 iframe 嵌入时自动剥壳——不渲染顶栏/左导航/面包屑,
* 也不发两个鉴权请求(嵌入上下文里本应用 /api/base/* 取不到会话会弹错误提示并画出带壳页面)。
* 显式 mode 参数(?mode=embed|page)的闭集判定在 T7 填报页落地,壳这层只兜 self != top 这一条。
*/
const isEmbedded = window.self !== window.top;
/**
* 嵌入态必须旁路全局 body 的 min-width(assets/styles/base/_base.scss 的 1200px):
* isEmbedded 只旁路了组件壳,管不到全局 CSS,窄 iframe 里仍按 ≥1200px 排版(整体右偏 + 右裁)。
* 内联样式优先级高于样式表;非嵌入态移除内联属性回落原值。
*/
function syncEmbedLayout(embedded: boolean) {
if (embedded) document.body.style.minWidth = "0";
else document.body.style.removeProperty("min-width");
}
const layoutData = reactive({
appName: "表单中心",
baseHost: config.baseHost,
ssoHost: config.ssoHost,
userInfo: null as any,
/** 已按「path 空=权限锚点不进导航」规则过滤的可渲染菜单树(见 utils/menuNav.ts) */
navList: [] as NavNode[],
isAlone: false,
});
onMounted(async () => {
// 首次导航异步完成后再判默认跳转:mount 时 currentRoute 可能仍是初始 "/"
await router.isReady();
syncEmbedLayout(isEmbedded);
isReady.value = true;
if (isEmbedded) return;
try {
// 1. 当前会话用户
const user: any = await baseApi.getUserInfo().send();
const { id, name, profilePicture } = user || {};
// 2. 当前用户已授权的模块树(左导航数据源;path 空的权限锚点行也在内,由导航层跳过)
const modList: IModule[] = (await baseApi.getUserMod().send()) || [];
layoutData.userInfo = { id, name: name || "用户", avatar: profilePicture || "" };
layoutData.navList = buildNavTree(modList, resolveMenuPage);
layoutData.isAlone = isSingleNav(layoutData.navList);
if (import.meta.env.DEV) {
// 冒烟取证:原始树 vs 可渲染导航(path 空的权限锚点应当只出现在前者)
console.table(
(modList || []).map((m: any) => ({
name: m?.name,
id: m?.id,
modId: m?.modId,
page: m?.page,
icon: m?.icon,
children: (m?.children || []).length
}))
);
console.table(
layoutData.navList.map((n) => ({ 渲染项: n.name, page: n.page, 分组头: n.group }))
);
}
// 入口页:跳第一个可跳转菜单项(无授权用户即「我的填写」)
if (router.currentRoute.value.path === "/") {
const first = firstNavPage(layoutData.navList);
if (first) router.replace(first);
}
} catch (error: any) {
console.error("加载用户信息失败", error);
if (error?.message !== "redirect") {
Message.error("载入用户信息失败,请检查后端服务是否正常!");
}
// 降级只降「导航为空」,不造兜底菜单树——静默回落会把「平台未配模块/未授权」伪装成正常
layoutData.userInfo = { name: "用户", avatar: "" };
layoutData.navList = [];
layoutData.isAlone = true;
}
});
onBeforeUnmount(() => syncEmbedLayout(false));
</script>
<template>
<main class="shell-placeholder">
<h1>表单中心</h1>
<p>工程壳已就绪(T1)。管理端框架、设计器与填报页在 T5~T8 落地。</p>
</main>
<!-- 嵌入态:裸渲染,页面自己管理样式与滚动(方案 §3.3 剥壳) -->
<router-view v-if="isEmbedded" :key="route.fullPath" />
<template v-else>
<layout v-if="isReady" :layoutData="layoutData" />
<div v-if="isReady" class="base-wrap-main" :class="{ 'alone-page': layoutData.isAlone }">
<div class="main-wrapper">
<!-- key=fullPath:仅 query 变化的深链(如详情页换 id)强制重渲染,避免显示上一条数据 -->
<router-view :key="route.fullPath" />
</div>
</div>
</template>
</template>
<script setup lang="ts"></script>
<style scoped>
.shell-placeholder {
font-family: system-ui, -apple-system, "Microsoft YaHei", sans-serif;
padding: 24px;
<style lang="scss">
html,
body {
margin: 0;
padding: 0;
height: 100%;
overflow: hidden;
}
#app {
height: 100%;
}
/* 页面级滚动容器:main-wrapper 定高 + 纵向滚动。内容超一屏时若只给 min-height,
会被 body 的 overflow:hidden 直接裁掉且无滚动条;列表页高度由 .table-region 实测(规范 §4.3) */
.base-wrap-main .main-wrapper {
height: 100%;
overflow-y: auto;
}
/* 表格操作列(operatingHandle/actionIcon 渲染的 Tooltip 包裹层)横排且随单元格居中 */
.ivu-table-cell > .ivu-tooltip {
display: inline-block;
vertical-align: middle;
margin: 0 2px;
}
</style>