feat(p91): 我的填写新增「可填写」视图——会话菜单树推导的填报入口列表
build / build (push) Successful in 1m48s

- routes.ts 增 /my/fillable(子页面路由不占模块行不进导航,手册 §2.2 注记)。
- My.vue 三视图化:Tab 增「可填写」(居首);数据源=store.modules(App.vue
  已拉的 GetUserMod 原始树)经 buildNavTree+flattenNav 同一套解析(与左侧
  导航同口径,含 menuRoute 的 /f/{code} 修复),按 page 形如 /f/ 挑填报行并
  去重;**不按角色过滤**——树里有什么就是什么,授权语义在后端(T8 红线同源)。
- 卡片列表:菜单名+寻址 path+「进入填报」router.push(page);空态文案引导
  找管理员配 /f/{code} 模块行并授权。
- 该视图纯前端零请求(答卷两视图行为不变)。

验证:vue-tsc --noEmit 干净;vite build 通过。
This commit is contained in:
chenxing
2026-10-15 23:42:51 +08:00
parent 79c98e4280
commit 66bb272004
2 changed files with 113 additions and 8 deletions
+8
View File
@@ -129,6 +129,14 @@ const routes: RouteRecordRaw[] = [
component: () => import("@/views/My.vue"),
meta: { title: "关于我的", parentPath: "/my" },
},
{
// P91 可填表单列表:数据源=会话菜单树里的 /f/ 行(授权语义在后端,前端不自行过滤)。
// 子页面路由不占模块行、不进导航(手册 §2.2 注记),入口在 My 容器顶部 Tab。
path: "/my/fillable",
name: "fc_my_fillable",
component: () => import("@/views/My.vue"),
meta: { title: "可填写", parentPath: "/my" },
},
// 兜底:外部深链/残留配置落到未登记路由时不白屏,回入口由壳决定首个可用页
{ path: "/:pathMatch(.*)*", redirect: "/" },
];
+105 -8
View File
@@ -9,6 +9,11 @@
* 两者是**两个端点、两个谓词**,页面按路由分派;顶部 Tab 只是 UI 导航(切路由),不是接口参数。
* 管理员代填后:这条答卷在**填表人的"我提交的"**里出现,同时在**被代填人的"关于我的"**里出现(V18)。
*
* P91 起第三视图「可填写」(/my/fillable):这不是答卷视图,是**填报入口列表**——
* 数据源=会话菜单树里 page 形如 /f/{code} 的行(App.vue 已拉过、store.modules 存原件),
* 复用 buildNavTree+flattenNav 同一套解析(与左侧导航同口径,天然吃到 menuRoute 的修复)。
* **不得在此按角色过滤**:树里有什么就是什么,授权语义在后端(任务书 T8 红线同源)。
*
* 三条交互口径沿 T7 的"我的提交"段(不另造轮子):
* 1. **编辑入口只在"我提交的"**:编辑=填表人的动作;"关于我的"整列只读
* (被代填人能看不能改——改是填表人或管理员的动作,后端 myDetail 同口径)。
@@ -29,6 +34,8 @@ import { deleteMyAnswer, getMyAnswerDetail, listAnswersAboutMe, listMySubmitted
import AnswerEditModal from "@/components/answer/AnswerEditModal.vue";
import AnswerValueView from "@/components/answer/AnswerValueView.vue";
import { statusDisplay } from "@/utils/dataValue";
import { buildNavTree, flattenNav, type FlatNav } from "@/utils/menuNav";
import { resolveMenuPage } from "@/utils/menuRoute";
import { useDefaultStore } from "@/store";
import type { AnswerDetail, MyAnswerViewItem } from "@/types/data";
@@ -36,8 +43,13 @@ const route = useRoute();
const router = useRouter();
const store = useDefaultStore();
/** 当前视图:只由路由路径决定(两个独立入口,不看 query) */
const view = computed<"submit" | "about">(() => (String(route.path).endsWith("/about") ? "about" : "submit"));
/** 当前视图:只由路由路径决定(三个独立入口,不看 query) */
const view = computed<"submit" | "about" | "fillable">(() => {
const p = String(route.path);
if (p.endsWith("/about")) return "about";
if (p.endsWith("/fillable")) return "fillable";
return "submit";
});
const VIEW_META = {
submit: {
@@ -51,6 +63,12 @@ const VIEW_META = {
endpoint: "GET /api/form/my/about-list",
predicate: "谓词:subject_id = 会话用户",
note: "这些数据是关于我的(老师/管理员代填后指定的主体是我)。整列只读——我不是提交人,改/删是提交人或管理员的动作。"
},
fillable: {
title: "可填写",
endpoint: "数据源:会话菜单树(GetUserMod)",
predicate: "谓词:模块行 path 形如 /f/{code}(授权在后端)",
note: "当前账号被授权的填报入口。列表由会话菜单树直接推导——树里没有的行这里不出现,也不做本地角色过滤。"
}
} as const;
@@ -72,6 +90,7 @@ function isMine(row: MyAnswerViewItem): boolean {
}
async function load() {
if (view.value === "fillable") return; // 数据源=本地菜单树,无需请求
loading.value = true;
errorText.value = "";
try {
@@ -86,9 +105,30 @@ async function load() {
}
}
function switchView(target: "submit" | "about") {
function switchView(target: "submit" | "about" | "fillable") {
if (target === view.value) return;
router.push({ path: target === "about" ? "/my/about" : "/my/submit" });
router.push({ path: `/${target === "submit" ? "my/submit" : "my/" + target}` });
}
// ==================== P91 可填写视图:菜单树 → 填报入口卡片 ====================
/**
* 与左侧导航同一套解析:buildNavTree 吃 menuRoute(P87 起 /f/{code} 行能解析),
* flattenNav 展平后按 page 形态挑填报行;按 page 去重(同名/多行指向同一表单只出一张卡)。
*/
const fillableItems = computed<FlatNav[]>(() => {
if (view.value !== "fillable") return [];
const nav = flattenNav(buildNavTree(store.modules, resolveMenuPage));
const seen = new Set<string>();
return nav.filter((n) => {
if (!/^\/f\//.test(n.page) || seen.has(n.page)) return false;
seen.add(n.page);
return true;
});
});
function goFill(page: string) {
router.push({ path: page });
}
// ==================== 详情 / 编辑 / 删除 ====================
@@ -196,13 +236,16 @@ onMounted(load);
<div class="page-wrapper yh-fill">
<div class="main-container">
<div class="view-head">
<RadioGroup :model-value="view" type="button" @on-change="(v: string | number) => switchView(String(v) as 'submit' | 'about')">
<RadioGroup :model-value="view" type="button"
@on-change="(v: string | number) => switchView(String(v) as 'submit' | 'about' | 'fillable')">
<Radio label="fillable">可填写</Radio>
<Radio label="submit">我提交的</Radio>
<Radio label="about">关于我的</Radio>
</RadioGroup>
<Button icon="md-refresh" @click="load">刷新</Button>
<Button v-if="view !== 'fillable'" icon="md-refresh" @click="load">刷新</Button>
<span class="toolbar-note">
{{ meta.endpoint }} | {{ meta.predicate }}(两个端点两个谓词,不做参数分支)
{{ meta.endpoint }} | {{ meta.predicate }}
<template v-if="view !== 'fillable'">(两个端点两个谓词,不做参数分支)</template>
</span>
</div>
@@ -211,9 +254,32 @@ onMounted(load);
<template v-if="view === 'about'">
<br />本视图不渲染编辑/删除入口(填表人才能改自己提交的那条);也不渲染改状态(填表人在接口层即被拒)。
</template>
<template v-else-if="view === 'fillable'">
<br />入口来自基础平台的「填报」模块行(path=/f/{表单code},授权给组后这里才会出现)。
</template>
</Alert>
<div class="table-region" ref="regionRef">
<!-- P91 可填写:卡片列表(菜单名 + 进入填报),数据源=会话菜单树 -->
<template v-if="view === 'fillable'">
<div v-if="fillableItems.length" class="fill-cards">
<div v-for="item in fillableItems" :key="item.id" class="fill-card">
<div class="fill-card-main">
<div class="fill-card-name">
<i class="iconfont icon-edit"></i>
{{ item.name }}
</div>
<code class="fill-card-path">{{ item.page }}</code>
</div>
<Button type="primary" @click="goFill(item.page)">进入填报</Button>
</div>
</div>
<Alert v-else show-icon type="warning">
当前账号没有可填写的表单入口。请找管理员在基础平台为本应用配置「填报」模块行
(path 填 /f/{表单code}),并授权给你所在的组织组。
</Alert>
</template>
<div v-else class="table-region" ref="regionRef">
<Table :columns="tableColumns" :data="rows" :height="regionH || undefined" :loading="loading"
no-data-text="这里还没有数据" class="link-table" />
</div>
@@ -278,4 +344,35 @@ onMounted(load);
margin-top: 12px;
line-height: 1.6;
}
/* P91 可填写卡片列表 */
.fill-cards {
display: flex;
flex-direction: column;
gap: 12px;
}
.fill-card {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
padding: 14px 18px;
border: 1px solid #e8eaec;
border-radius: 4px;
background: #fff;
}
.fill-card-name {
font-size: 14px;
color: #17233d;
font-weight: 600;
}
.fill-card-name .iconfont {
margin-right: 8px;
color: var(--brand, #2d8cf0);
}
.fill-card-path {
color: #808695;
font-size: 12px;
margin-top: 4px;
display: inline-block;
}
</style>