t14:前端模块行退役——左导航三支路自解析、AppContext 重构为通用壳两视图

- App.vue:引导只剩 GetUserInfo,之后并行取 fillable/managed/capabilities 三支构造导航
  (取不到按空集/false 走,fail-closed,不造兜底菜单树);摘 GetUserMod 调用与 setModules
- store:modules/IModule 状态与 setModules 删除,改存三支路数据 + managesForm() 判"这张表我管不管得动"
- AppContext.vue:重写为通用壳两视图(§5.1)——管理员视图=数据列表+复制填报路径
  (添加/导入 T18 接进这一档),填报者视图=仅填报 tab;判定只看 managed 是否非空,
  壳零自有权限逻辑;GetUserMod?appCode= 拉目标应用树那一路与 :modules prop 链整条拆除
- Data.vue:导出按钮改"该表单 ALL 档(managed 成员)",中心与快应用两种上下文从此同一条判据;
  AnswerDetail.vue 的管理动作区同判据;utils/modulePerms.ts(canExport/hasPage)删除
- utils/menuNav.ts:buildNavTree 与"从树渲染菜单"三条规则删除,只留导航数据结构与
  高亮/面包屑/默认跳转;menuRoute 的 /f/{code} 形态识别按任务书保留(分享直链/历史链接)
- mock:baseapi 去 GetUserMod 与两棵模块树;dataapi 补 /my/managed 与 /my/capabilities 桩
This commit is contained in:
chenxing
2026-10-20 13:21:23 +08:00
parent 4a7db3d22c
commit 6401a69c18
12 changed files with 324 additions and 472 deletions
+9 -3
View File
@@ -22,7 +22,7 @@ import { resolveForm } from "@/api/module/fill";
import AnswerValueView from "@/components/answer/AnswerValueView.vue";
import AnswerEditModal from "@/components/answer/AnswerEditModal.vue";
import { useDefaultStore } from "@/store";
import { hasPage } from "@/utils/modulePerms";
import { managesForm } from "@/store";
import { statusDisplay } from "@/utils/dataValue";
import type { AnswerDetail } from "@/types/data";
import type { StatusDefView } from "@/types/form";
@@ -36,8 +36,14 @@ const detail = ref<AnswerDetail | null>(null);
const loading = ref(false);
const error = ref("");
/** 数据管理权(授权模块行判定;填表人身份进来时这里就是 false——V29 前端半句) */
const canManage = computed(() => hasPage(store.modules, "/admin/data"));
/**
* 数据管理权(契约 v2 C4②:该表单 ALL 档;`modulePerms.hasPage` 随模块行退役删除)。
* 判据=这张表在 `my/managed` 清单里;填表人身份进来时就是 false(V29 前端半句不变),
* 接口侧的拒绝仍由后端守卫兜底——藏按钮不等于授权。
*/
const canManage = computed(() =>
managesForm(store.managed, detail.value?.appCode, detail.value?.code)
);
const liveStatusDefs = ref<StatusDefView[]>([]);
const statusModal = ref<{ visible: boolean; target: string }>({ visible: false, target: "" });
const editOpen = ref(false);
+12 -20
View File
@@ -13,10 +13,9 @@
* 4. **数据范围以后端为准**:响应里的 `scopes` 只用来把"你现在看到的是哪个范围"讲清楚,
* 前端不据此再过滤一遍(任务书 T8 红线)。
*
* 快应用上下文(AppContext)复用本组件:传 `context="app"` + `app-code` + `codes`
* (候选来自 `GET /api/form/my/fillable?appCode=` 按层级筛出的 code,经 resolve 拿 uuid——
* 定义管理权不在快应用上下文里,§3.4;P95 W5 起不再从该应用模块树挑 `/f/` 行)
* + `modules`(**同一棵原始模块树**,只给按钮级权限判定用,P76)。
* 快应用上下文(通用壳)复用本组件:传 `context="app"` + `app-code` + `codes`
* (候选由壳按"我管理的表单 ∩ 该应用"算出,经 resolve 拿 uuid——定义管理权不在通用壳里,§3.4)。
* 导出按钮的显隐不再需要宿主传任何权限载体:本组件自己按 `my/managed` 清单判 ALL 档(契约 v2 §2.4)。
*/
import { computed, h, nextTick, onBeforeUnmount, onMounted, ref, watch } from "vue";
import { useRoute, useRouter } from "vue-router";
@@ -25,8 +24,7 @@ import { actionColumn } from "@/utils/tableActions";
import { useElementHeight } from "@/utils/hook/useElementHeight";
import { useFormCandidates } from "@/utils/hook/useFormCandidates";
import { dataApi, downloadBlob, exportBlob } from "@/api/module/data";
import { useDefaultStore } from "@/store";
import { canExport } from "@/utils/modulePerms";
import { managesForm, useDefaultStore } from "@/store";
import AnswerEditModal from "@/components/answer/AnswerEditModal.vue";
import {
cellText,
@@ -42,7 +40,6 @@ import {
import { scopeLabel, type AnswerDetail, type AnswerRow, type FilterField, type FilterItem, type ListView } from "@/types/data";
import type { StatusDefView } from "@/types/form";
import type { DesignField } from "@/types/formSchema";
import type { IModule } from "@/types/Basics";
/** 行视图 + 勾选态(`_checked` 是 view-ui-plus Table 的行内选中字段,见 withSelectionState) */
type RowVM = AnswerRow & { _checked?: boolean };
@@ -54,20 +51,13 @@ const props = withDefaults(
appCode?: string;
/** app 上下文的候选 code(来自 /api/form/my/fillable 的层级筛取,P95 W5) */
codes?: string[];
/**
* 按钮级权限判定的**那棵树**(P76):app 上下文必须由宿主传入目标应用的模块树
* (GetUserMod?appCode= 的结果)——中心会话树里没有快应用的"导出"锚点行,
* 拿它判锚点等于永远判不中(fail-closed 方向:有授权也不画按钮)。
* center 上下文不传,仍按 `store.modules`(会话自己的树)判页面行。
*/
modules?: IModule[] | null;
/**
* 被宿主页面内嵌(快应用上下文 /app/:appCode/)时置 true:只渲染内容本体,
* 不再套一层 `.page-wrapper > .main-container`——否则会出现双层白卡与双份内边距。
*/
embedded?: boolean;
}>(),
{ context: "center", appCode: "", codes: () => [], modules: null, embedded: false }
{ context: "center", appCode: "", codes: () => [], embedded: false }
);
const route = useRoute();
@@ -446,12 +436,14 @@ async function submitBatchStatus() {
// ==================== 导出 ====================
/**
* 权限判定用的树(P76):app 上下文=宿主编进来的**目标应用树**(快应用的"导出"锚点行在这里),
* center 上下文=会话自己的树(按 `/admin/data` 页面行判,手册 §2.2 中心表没有导出行)。
* 两种上下文各自吃自己的树,`canExport` 的纯函数判据不变。
* 导出按钮显隐(契约 v2 §2.4:`modulePerms.canExport` 随模块行退役删除):
* 判据换成"<b>该表单 ALL 档持有</b>"的清单形态——当前选中的这张表在 `my/managed` 里,就画导出。
* 中心与快应用两种上下文从此**同一条判据**(不再有"两棵树"的差别,B13/P78 遗迹清零);
* 接口侧的 403 兜底仍在后端守卫,这里只决定画不画。
*/
const permsTree = computed(() => (props.context === "app" ? props.modules : store.modules));
const exportable = computed(() => canExport(permsTree.value, props.context === "app" ? "app" : "center"));
const exportable = computed(() =>
managesForm(store.managed, current.value?.appCode || props.appCode, current.value?.code)
);
const exporting = ref(false);
function askExport() {