From 66bb27200408021573ed6bd0caed132ff3900625 Mon Sep 17 00:00:00 2001 From: chenxing Date: Thu, 15 Oct 2026 23:42:51 +0800 Subject: [PATCH] =?UTF-8?q?feat(p91):=20=E6=88=91=E7=9A=84=E5=A1=AB?= =?UTF-8?q?=E5=86=99=E6=96=B0=E5=A2=9E=E3=80=8C=E5=8F=AF=E5=A1=AB=E5=86=99?= =?UTF-8?q?=E3=80=8D=E8=A7=86=E5=9B=BE=E2=80=94=E2=80=94=E4=BC=9A=E8=AF=9D?= =?UTF-8?q?=E8=8F=9C=E5=8D=95=E6=A0=91=E6=8E=A8=E5=AF=BC=E7=9A=84=E5=A1=AB?= =?UTF-8?q?=E6=8A=A5=E5=85=A5=E5=8F=A3=E5=88=97=E8=A1=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 通过。 --- src/router/routes.ts | 8 +++ src/views/My.vue | 113 ++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 113 insertions(+), 8 deletions(-) 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; +}