diff --git a/src/router/routes.ts b/src/router/routes.ts index efccf90..40672db 100644 --- a/src/router/routes.ts +++ b/src/router/routes.ts @@ -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: "/" }, ]; diff --git a/src/views/My.vue b/src/views/My.vue index 86ac391..35f4c68 100644 --- a/src/views/My.vue +++ b/src/views/My.vue @@ -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(() => { + if (view.value !== "fillable") return []; + const nav = flattenNav(buildNavTree(store.modules, resolveMenuPage)); + const seen = new Set(); + 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);
- + + 可填写 我提交的 关于我的 - + - {{ meta.endpoint }} | {{ meta.predicate }}(两个端点两个谓词,不做参数分支) + {{ meta.endpoint }} | {{ meta.predicate }} +
@@ -211,9 +254,32 @@ onMounted(load); + -
+ + + +
@@ -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; +}