diff --git a/src/api/module/fill.ts b/src/api/module/fill.ts index aeb7329..5cc4a06 100644 --- a/src/api/module/fill.ts +++ b/src/api/module/fill.ts @@ -6,7 +6,7 @@ */ import { alovaInstance } from "@/utils/alova/alova-iview"; import type { ResolvedForm, SubmitRequest, SubmitView, MyAnswerItem } from "@/types/answer"; -import type { AnswerDetail, MyAnswerViewItem } from "@/types/data"; +import type { AnswerDetail, FillableForm, MyAnswerViewItem } from "@/types/data"; /** 解析当前发布版本(契约 §1:入口寻址) */ export function resolveForm(code: string, appCode?: string) { @@ -55,3 +55,14 @@ export function listAnswersAboutMe() { export function getMyAnswerDetail(answerId: string) { return alovaInstance.Get("/api/form/my/detail", { params: { answerId } }).send(); } + +// ==================== 可填写列表(P95 W3/W4,/my/fillable) ==================== +// 数据源=后端按 P95 三层模型解析的**填报入口**清单,不再是会话菜单树里挑 /f/ 行(P91 的形态)。 +// appCode 可选:缺省=全部应用(中心 + 快应用),快应用上下文传自身 appCode 收窄。 + +/** 我可填的表单(入口层非 NONE 的都下发;本人能不能填看 canFill) */ +export function listFillableForms(appCode?: string) { + const params: Record = {}; + if (appCode) params.appCode = appCode; + return alovaInstance.Get("/api/form/my/fillable", { params }).send(); +} diff --git a/src/assets/mock/baseapi.ts b/src/assets/mock/baseapi.ts index a95a7d3..6984a74 100644 --- a/src/assets/mock/baseapi.ts +++ b/src/assets/mock/baseapi.ts @@ -82,25 +82,18 @@ const OWN_MODULES: IModule[] = [ ]; /** - * 第一个快应用的模块树(方案 §6.2 表的典型配法,T9 演练桩)。 + * 第一个快应用的模块树(T9 演练桩,P95 W6 改版为终态形态)。 * - * 只在 `appCode=fc-drill` 时下发,用来在本机把 **P76(导出锚点按应用树判)/ P77(应用内详情形态)/ - * V19(快应用独立模块树渲染左导航)** 跑到可实拍的程度——真会话取证要等 H2/H4(执行手册 §2 人工项)。 + * 只在 `appCode=fc-drill` 时下发,用来在本机把 **P76(导出锚点按应用树判)/ P77(应用内详情形态)** + * 跑到可实拍的程度——真会话取证要等 H2/H4(执行手册 §2 人工项)。 * 其余外指 appCode 仍旧返回空树:那是 H4 未上线时的既定口径(P3),桩不能替平台把没上线的能力假装成有。 + * + * P95 起这棵树里没有「填报」行了:`/f/{code}` 模块行不再承担填报授权 + * (入口由中心按三层模型解析,见 `/api/form/my/fillable` 桩),本页的树只剩两种职责—— + * 数据查看/统计两个页面行,和 path 空的导入/导出按钮锚点(P76 判的就是这两行)。 + * 与 `service/sql/h2_userbase_app_and_modules.sql` 的快应用段同一份终态。 */ const DRILL_APP_MODULES: IModule[] = [ - { - id: 9201, - modId: 9201, - name: "填报", - parentId: 0, - page: "/f/drill-honour", - level: 1, - icon: "edit", - orderNumber: 1, - usable: true, - children: [] - }, { id: 9202, modId: 9202, diff --git a/src/assets/mock/dataapi.ts b/src/assets/mock/dataapi.ts index 56fccb5..7fbf923 100644 --- a/src/assets/mock/dataapi.ts +++ b/src/assets/mock/dataapi.ts @@ -18,7 +18,7 @@ * 游标(keyset):`cursor`=上一页末行 answerId(uuid),服务端按它定位后取下一页—— * 这里用 uuid 在数组里的下标模拟,形状与后端一致(前端不感知差异)。 */ -import type { AnswerDetail, AnswerRow, BatchItemResult, FilterField, ListView, MyAnswerViewItem, StatsView } from "@/types/data"; +import type { AnswerDetail, AnswerRow, BatchItemResult, FilterField, FillableForm, ListView, MyAnswerViewItem, StatsView } from "@/types/data"; const ok = (data: any) => ({ code: 0, msg: "", data }); const bizErr = (code: number, msg: string) => ({ code, msg, data: null }); @@ -317,6 +317,39 @@ export default [ return ok(results); } }, + // ==================== 可填写入口清单(P95 W3/W4,/my/fillable) ==================== + /** + * 三行种子刻意覆盖三层模型的可观测差异(冒烟脚本 p95-fill-scope-smoke.sh 就断言这三件事): + * ① FILL:学生面向、canFill=true → 「可填写」画卡; + * ② MANAGE:学生面向、canFill=false(教师那种"只能代填")→ 「可填写」不画卡, + * 但快应用上下文的数据管理 tab 要靠它出现; + * ③ ADMIN:教师面向、canFill=true + 最高层 → 统计 tab 只由它决定。 + * appCode 传值时按该应用收窄(缺省=全部应用),与后端 FillableFormService 同一口径。 + */ + { url: "/api/form/my/fillable", method: "get", response: ({ query }: any) => { + const rows: FillableForm[] = [ + { code: "honor_apply", name: "三好学生申报", appCode: APP, audience: 1, + deadline: "2026-12-31 23:59:59", fillLimit: 1, groupTag: null, canFill: true, myTier: "FILL" }, + { code: "home_visit", name: "家访登记(教师代填)", appCode: APP, audience: 1, + deadline: null, fillLimit: 0, groupTag: "五年级", canFill: false, myTier: "MANAGE" }, + { code: "teacher_survey", name: "教师问卷", appCode: APP, audience: 2, + deadline: "2026-11-30 23:59:59", fillLimit: 0, groupTag: null, canFill: true, myTier: "ADMIN" }, + { code: "drill-honour", name: "荣誉申报(演练应用)", appCode: "fc-drill", audience: 1, + deadline: "2026-10-31 23:59:59", fillLimit: 0, groupTag: null, canFill: true, myTier: "FILL" }, + { code: "drill_roster", name: "花名册(演练应用,教师代填)", appCode: "fc-drill", audience: 1, + deadline: null, fillLimit: 0, groupTag: null, canFill: false, myTier: "MANAGE" } + ]; + // 排序沿后端口径:deadline 升序、不限截止在后、同档按 code + const app = String(query?.appCode || "").trim(); + const hit = app ? rows.filter((r) => r.appCode === app) : rows; + const sorted = [...hit].sort((a, b) => { + const da = a.deadline || "9999"; + const db = b.deadline || "9999"; + return da === db ? a.code.localeCompare(b.code) : da.localeCompare(db); + }); + return ok(sorted); + } }, + // ==================== 我的填写两视图(V18) ==================== { url: "/api/form/my/submit-list", method: "get", response: () => { const items: MyAnswerViewItem[] = seed diff --git a/src/router/routes.ts b/src/router/routes.ts index 40672db..7a874c6 100644 --- a/src/router/routes.ts +++ b/src/router/routes.ts @@ -130,8 +130,8 @@ const routes: RouteRecordRaw[] = [ meta: { title: "关于我的", parentPath: "/my" }, }, { - // P91 可填表单列表:数据源=会话菜单树里的 /f/ 行(授权语义在后端,前端不自行过滤)。 - // 子页面路由不占模块行、不进导航(手册 §2.2 注记),入口在 My 容器顶部 Tab。 + // P91 起第三视图;P95 W4 换数据源=GET /api/form/my/fillable(中心按三层模型解析的填报入口, + // 不再从会话菜单树挑 /f/ 行)。子页面路由不占模块行、不进导航(手册 §2.2 注记),入口在 My 容器顶部 Tab。 path: "/my/fillable", name: "fc_my_fillable", component: () => import("@/views/My.vue"), diff --git a/src/types/data.ts b/src/types/data.ts index 9dad4cc..936b343 100644 --- a/src/types/data.ts +++ b/src/types/data.ts @@ -126,6 +126,30 @@ export interface MyAnswerViewItem { submitTime: string; } +/** + * 「可填写」列表的行(P95 W3/W4,`GET /api/form/my/fillable`)。 + * + *

这不是答卷行,是**表单入口**行:后端按 P95 三层模型逐张表算出"这个人在这张表上算哪一层", + * NONE 的行不下发。`canFill` 与 `myTier` 是两件事——`canFill=false` 且 `myTier="MANAGE"` + * 就是"教师×面向学生"那种只能代填的表:「可填写」列表把它筛掉,但快应用上下文的 + * 数据管理 tab 仍要它(AppContext.vue 按 myTier 决定 tab 显隐)。

+ */ +export interface FillableForm { + code: string; + name: string; + appCode: string; + /** 面向人群:1 学生 / 2 教师(与定义域 FormView 同一个数字口径) */ + audience: number; + /** yyyy-MM-dd HH:mm:ss;null=不限截止 */ + deadline: string | null; + /** 限填次数:0 不限 / 1 限一次 */ + fillLimit: number; + groupTag: string | null; + canFill: boolean; + /** 并集最高层:ADMIN > MANAGE > FILL */ + myTier: "ADMIN" | "MANAGE" | "FILL"; +} + /** 数据范围闭集四枚举的展示名(契约 §8;与后端 FormEnums.DataScope 的 label 同源) */ export const DATA_SCOPE_LABELS: Record = { ALL: "全部", diff --git a/src/utils/fillLink.ts b/src/utils/fillLink.ts index 89c8851..b3b9f58 100644 --- a/src/utils/fillLink.ts +++ b/src/utils/fillLink.ts @@ -21,6 +21,24 @@ export function buildFillLink(form: { code: string; appCode?: string | null }): return `${base}#/f/${encodeURIComponent(form.code)}${query}`; } +/** + * 表单 → 站内跳转的路由对象(P95 W4/W5:「可填写」列表与快应用导航壳都点这一条)。 + * + *

与 {@link buildFillLink} 共用同一条 appCode 规则(中心表单不带 query、快应用带), + * 只是不拼绝对地址——复制出去的链接和点进去的路径必须是同一个地址, + * 否则"分享给同事"和"自己点"会解析到两张表。

+ */ +export function fillRouteOf(form: { code: string; appCode?: string | null }): { + path: string; + query: Record; +} { + const appCode = String(form.appCode ?? "").trim(); + return { + path: `/f/${encodeURIComponent(form.code)}`, + query: appCode && appCode !== DEFAULT_APP_CODE ? { appCode } : {} + }; +} + /** * 复制文本到剪贴板:navigator.clipboard 优先(安全上下文),失败回退旧系统同款 * 临时 textarea + execCommand(http 内网环境/旧内核兜底)。返回是否成功。 diff --git a/src/utils/hook/useFormCandidates.ts b/src/utils/hook/useFormCandidates.ts index e3257c8..881f181 100644 --- a/src/utils/hook/useFormCandidates.ts +++ b/src/utils/hook/useFormCandidates.ts @@ -1,10 +1,11 @@ /** * 数据面页面的"选哪张表单"共用件(数据列表 / 统计页两处同形,快应用上下文第三种候选来源)。 * - * 两条来路都是合规的,区别只在账号被授权了哪一行模块: + * 两条来路都是合规的,区别只在清单从哪儿取: * - 中心上下文:`/api/form/admin/list`(要"表单设计"模块行)→ 一次拿全本校表单清单; - * - 快应用上下文:只认该应用模块树里的 `/f/{code}` 行,逐个 `resolve` 换 uuid - * (定义编辑权不在快应用,方案 §3.4;而 resolve 是读接口的唯一入口,契约 §1)。 + * - 快应用上下文:宿主(AppContext)把 `GET /api/form/my/fillable?appCode=` 里按层级筛出的 code + * 传进来(P95 W5;原"从该应用模块树挑 `/f/{code}` 行"的形态已退役), + * 逐个 `resolve` 换 uuid(定义编辑权不在快应用,方案 §3.4;而 resolve 是读接口的唯一入口,契约 §1)。 * * 清单取不到(只授权数据管理、没授权表单设计的账号)时**不假装是空列表**: * 报错文案留给页面显示,并留一条"按 code 直达"的口子——它同样只经 resolve,不绕过任何守卫。 diff --git a/src/utils/modulePerms.ts b/src/utils/modulePerms.ts index 83ec778..e0e8da6 100644 --- a/src/utils/modulePerms.ts +++ b/src/utils/modulePerms.ts @@ -64,20 +64,6 @@ export function canExport(tree: IModule[] | null | undefined, context: DataExpor return hasPage(tree, "/admin/data"); } -/** - * 快应用树里的填报目标(模块行 path 形如 `/f/{code}`,方案 §6.2 表第一行)。 - * 数据/统计页要的是表单 uuid,而 uuid 只能由 code 经 resolve 得到(契约 §1"resolve 是读接口的唯一入口")—— - * 快应用上下文不授权定义管理行,拿不到表单列表,这条是唯一的合规取数路径。 - */ -export function fillCodesOfModules(tree: IModule[] | null | undefined): string[] { - const codes: string[] = []; - for (const p of authorizedPaths(tree)) { - const m = /^\/f\/([^/?#]+)/.exec(p); - if (m && !codes.includes(m[1])) codes.push(m[1]); - } - return codes; -} - /** * 受限身份可见集的来源(V26 第三半句的前端侧口径): * 学生身份只见"填报"路由 = 导航树里只有填报行——这是模块授权的直接结果, diff --git a/src/views/AppContext.vue b/src/views/AppContext.vue index 3d05bbf..06a800b 100644 --- a/src/views/AppContext.vue +++ b/src/views/AppContext.vue @@ -1,68 +1,71 @@