任务书 P95 W4/W5/W6。填报入口收回中心后,前端不再从应用模块树里挑 /f/{code} 行算清单。
- My.vue「可填写」:数据源 store.modules → GET /api/form/my/fillable,只画 canFill=true;
空态文案改「表单发布后按面向人群自动出现在这里」,不再引导去 UserBase 配模块行
- AppContext.vue 整页改版:清单吃 fillable?appCode=,三档按 myTier 渲染
(填报恒显 + 单表单直落保留 / 数据管理 MANAGE∪ADMIN / 统计 ADMIN);
GetUserMod?appCode 仍拉一次,唯一用途=交给内嵌 Data.vue 判导出锚点(P76 机制原样),
空树整页告警与「树里没有 /f/ 行」分支退役——取树失败只等于不画导出按钮(fail-closed)
- fillLink 新增 fillRouteOf(与 buildFillLink 同一条 appCode 规则,点进去与分享出去同址);
modulePerms 删 fillCodesOfModules(唯一消费者已退役),canExport/anchorRows/hasPage 保留勿伤;
menuRoute 的 /f/ 解析与门户导航未动(分享链接、P87 探针照旧)
- mock:快应用树改四行终态(删填报行),新增 /api/form/my/fillable 桩覆盖 FILL/MANAGE/ADMIN
- vue-tsc --noEmit 与 vite build 双绿;p87 探针由当前源码重打包后 15/15 复跑绿
This commit is contained in:
+12
-1
@@ -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<AnswerDetail>("/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<string, string> = {};
|
||||
if (appCode) params.appCode = appCode;
|
||||
return alovaInstance.Get<FillableForm[]>("/api/form/my/fillable", { params }).send();
|
||||
}
|
||||
|
||||
@@ -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),桩不能替平台把没上线的能力假装成有。
|
||||
*
|
||||
* <b>P95 起这棵树里没有「填报」行了</b>:`/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,
|
||||
|
||||
@@ -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(教师那种"只能代填")→ 「可填写」<b>不</b>画卡,
|
||||
* 但快应用上下文的数据管理 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
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -126,6 +126,30 @@ export interface MyAnswerViewItem {
|
||||
submitTime: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 「可填写」列表的行(P95 W3/W4,`GET /api/form/my/fillable`)。
|
||||
*
|
||||
* <p>这不是答卷行,是**表单入口**行:后端按 P95 三层模型逐张表算出"这个人在这张表上算哪一层",
|
||||
* NONE 的行不下发。`canFill` 与 `myTier` 是两件事——`canFill=false` 且 `myTier="MANAGE"`
|
||||
* 就是"教师×面向学生"那种只能代填的表:「可填写」列表把它筛掉,但快应用上下文的
|
||||
* 数据管理 tab 仍要它(AppContext.vue 按 myTier 决定 tab 显隐)。</p>
|
||||
*/
|
||||
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<string, string> = {
|
||||
ALL: "全部",
|
||||
|
||||
@@ -21,6 +21,24 @@ export function buildFillLink(form: { code: string; appCode?: string | null }):
|
||||
return `${base}#/f/${encodeURIComponent(form.code)}${query}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 表单 → 站内跳转的路由对象(P95 W4/W5:「可填写」列表与快应用导航壳都点这一条)。
|
||||
*
|
||||
* <p>与 {@link buildFillLink} <b>共用同一条 appCode 规则</b>(中心表单不带 query、快应用带),
|
||||
* 只是不拼绝对地址——复制出去的链接和点进去的路径必须是同一个地址,
|
||||
* 否则"分享给同事"和"自己点"会解析到两张表。</p>
|
||||
*/
|
||||
export function fillRouteOf(form: { code: string; appCode?: string | null }): {
|
||||
path: string;
|
||||
query: Record<string, string>;
|
||||
} {
|
||||
const appCode = String(form.appCode ?? "").trim();
|
||||
return {
|
||||
path: `/f/${encodeURIComponent(form.code)}`,
|
||||
query: appCode && appCode !== DEFAULT_APP_CODE ? { appCode } : {}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 复制文本到剪贴板:navigator.clipboard 优先(安全上下文),失败回退旧系统同款
|
||||
* 临时 textarea + execCommand(http 内网环境/旧内核兜底)。返回是否成功。
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
/**
|
||||
* 数据面页面的"选哪张表单"共用件(数据列表 / 统计页两处同形,快应用上下文第三种候选来源)。
|
||||
*
|
||||
* 两条来路都是<b>合规</b>的,区别只在账号被授权了哪一行模块:
|
||||
* 两条来路都是<b>合规</b>的,区别只在清单从哪儿取:
|
||||
* - 中心上下文:`/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,不绕过任何守卫。
|
||||
|
||||
@@ -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 第三半句的前端侧口径):
|
||||
* 学生身份只见"填报"路由 = 导航树里只有填报行——这是模块授权的直接结果,
|
||||
|
||||
+141
-113
@@ -1,68 +1,71 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* 快应用上下文 /app/:appCode/(T8 落地,替换 T5 占位)。
|
||||
* 快应用上下文 /app/:appCode/(T8 落地,P95 W5 改版:导航壳按身份写死)。
|
||||
*
|
||||
* 形态(方案 §6.2 纯 A 形态 + 任务书 T8"快应用上下文"):
|
||||
* 形态(方案 §6.2 纯 A 形态 + P95 三层模型):
|
||||
* 快应用 = UserBase 真实应用行 + 无自有后端 + pc_url → 本路由。
|
||||
* - **模块树**:经 `/api/base/GetUserMod?appCode=<目标应用>` 取**那个应用**的树。H4 未上线时
|
||||
* 后端返回空树 + warn(T1 已验收的兜底,P3 裁决)——本页照此显示空态,**绝不回落本应用菜单**。
|
||||
* - **范围矩阵**:受限身份可见集**以后端为准**,本页不自行按角色过滤(任务书 T8 红线)——
|
||||
* 树里有什么就是什么,页面只做"单表单直落 vs 应用聚合轻导航"的形态分派。
|
||||
* - **单表单直落无导航**:树里只有一个可跳转填报行(`/f/{code}`)→ 直接渲染填报页,无轻导航。
|
||||
* - **应用聚合带轻导航**:树里有多个可跳转行 → 顶部一排轻导航(按 path 分派),内容区渲染对应视图。
|
||||
*
|
||||
* V26 三半句的前端侧(模块把关):
|
||||
* ① 无导出授权者不渲染导出按钮 → `canExport(tree, "app")`,其中 **tree 是本页拉到的那棵应用树**,
|
||||
* 经 `modules` prop 下传给内嵌的 Data.vue(P76:中心会话树里没有快应用的"导出"锚点行,
|
||||
* 拿它判锚点永远判不中——fail-closed 方向,有授权也不画按钮);
|
||||
* ② 导航不渲染 path 空节点 → 树构建走 `buildNavTree`(menuNav 已跳过锚点行,本页复验);
|
||||
* ③ 学生身份只见填报路由 → 树里只有 `/f/{code}` 行时,导航自然只有填报项(后端授权的直接结果,
|
||||
* 前端不再自己按角色过滤)。
|
||||
*
|
||||
* path → 视图分派(快应用模块行的 path 形态,方案 §6.2 表):
|
||||
* `/f/{code}` → 填报(渲染单表单,或跳中心填报页)
|
||||
* `/app/{appCode}/data` → 数据管理(复用 Data.vue,codes 来自树里的填报行)
|
||||
* `/app/{appCode}/stats`→ 统计(复用 Stats.vue)
|
||||
* 其余(含导入这类 path 空锚点)→ 不渲染为可跳转项(menuNav 已过滤 path 空)
|
||||
* - **清单来源**:`GET /api/form/my/fillable?appCode=<目标应用>`——中心按
|
||||
* `audience × 身份 × 管理员两表` 解析出的填报入口。**不再吃该应用的模块树**,
|
||||
* 发一张表单不需要超管加一条 `/f/{code}` 模块行(P95 §0 病根第 1 条就此拆掉)。
|
||||
* - **导航壳按身份渲染**(三档写死,不再由"树里有什么"决定):
|
||||
* · 填报 tab <b>恒显</b>:`canFill=true` 的行;0 行=空态;恰 1 行且地址没带子视图段=
|
||||
* <b>单表单直落</b>形态保留(内容区直接给这张表的填报入口,不叠导航);
|
||||
* · 数据管理 tab:存在 `myTier∈{MANAGE, ADMIN}` 的行时显,`:codes` 喂接口结果;
|
||||
* · 统计 tab:存在 `myTier=ADMIN` 的行时显,codes 同上。
|
||||
* - **"范围以后端为准"这条红线不动**:tab 只决定画不画;进去能看哪些行仍由后端
|
||||
* (`DataScopeService` + `FormAdminGuard.requireDataView`)把关,前端不按角色再筛一遍(任务书 T8 红线)。
|
||||
* - **模块树仍然拉一次**,唯一用途=把原始树经 `modules` prop 下传给内嵌 Data.vue 判"导出"锚点
|
||||
* (P76:中心会话树里没有快应用的锚点行,拿错树永远判不中)。空树/取树失败=fail-closed 不画导出按钮,
|
||||
* <b>不再整页报"这个应用进不来"</b>——原"空树"告警与"树里没有 /f/ 行"的分支随这条依赖收敛而退役。
|
||||
*/
|
||||
import { computed, onMounted, ref, watch } from "vue";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import { baseApi } from "@/api/module/base";
|
||||
import { buildNavTree, flattenNav, type NavNode } from "@/utils/menuNav";
|
||||
import { resolveMenuPage } from "@/utils/menuRoute";
|
||||
import { fillCodesOfModules } from "@/utils/modulePerms";
|
||||
import { listFillableForms } from "@/api/module/fill";
|
||||
import { fillRouteOf } from "@/utils/fillLink";
|
||||
import Data from "@/views/admin/Data.vue";
|
||||
import Stats from "@/views/admin/Stats.vue";
|
||||
import type { FillableForm } from "@/types/data";
|
||||
import type { IModule } from "@/types/Basics";
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
||||
const appCode = computed(() => String(route.params.appCode ?? ""));
|
||||
/** 子视图段(/app/:appCode/:view):空=由树决定单表单直落或落默认聚合视图 */
|
||||
/** 子视图段(/app/:appCode/:view):空=填报(单表单直落或多表单列表) */
|
||||
const viewParam = computed(() => String(route.params.view ?? ""));
|
||||
|
||||
const fillRows = ref<FillableForm[]>([]);
|
||||
/** 目标应用的原始模块树:本页只把它交给内嵌 Data.vue 判导出锚点(P76),不再拿它算导航 */
|
||||
const rawTree = ref<IModule[]>([]);
|
||||
const navTree = ref<NavNode[]>([]);
|
||||
const loading = ref(false);
|
||||
const errorText = ref("");
|
||||
const emptyTree = ref(false);
|
||||
|
||||
/** 树里的填报目标 code(数据/统计视图靠它 resolve 出 uuid——快应用不授权定义管理行,§3.4) */
|
||||
const fillCodes = computed(() => fillCodesOfModules(rawTree.value));
|
||||
/** 填报 tab 的行:后端 canFill 判定(层②"只能代填"的行 canFill=false,不进这张列表) */
|
||||
const fillCards = computed(() => fillRows.value.filter((r) => r.canFill));
|
||||
/** 数据管理候选=层②∪层③;统计候选=层③(与后端 FillAccessService 的三个谓词一一对应) */
|
||||
const manageRows = computed(() => fillRows.value.filter((r) => r.myTier === "MANAGE" || r.myTier === "ADMIN"));
|
||||
const adminRows = computed(() => fillRows.value.filter((r) => r.myTier === "ADMIN"));
|
||||
|
||||
/** 可跳转导航项(path 空锚点已被 menuNav 跳过——V26 第二半句的复验点) */
|
||||
const navItems = computed(() => flattenNav(navTree.value));
|
||||
const isSingleForm = computed(() => navItems.value.length === 1 && /^\/f\//.test(navItems.value[0]?.page || ""));
|
||||
function codesOf(rows: FillableForm[]): string[] {
|
||||
const codes: string[] = [];
|
||||
for (const row of rows) if (!codes.includes(row.code)) codes.push(row.code);
|
||||
return codes;
|
||||
}
|
||||
|
||||
const dataCodes = computed(() => codesOf(manageRows.value));
|
||||
const statsCodes = computed(() => codesOf(adminRows.value));
|
||||
const showDataTab = computed(() => dataCodes.value.length > 0);
|
||||
const showStatsTab = computed(() => statsCodes.value.length > 0);
|
||||
|
||||
/** 单表单直落:可填项恰好一个且地址没带子视图段(P95 保留 T8 的这条形态,只换数据源) */
|
||||
const isSingleForm = computed(() => !viewParam.value.trim() && fillCards.value.length === 1);
|
||||
|
||||
/** 当前内容视图:'fill' | 'data' | 'stats' | 'unknown' */
|
||||
const activeView = computed<"fill" | "data" | "stats" | "unknown">(() => {
|
||||
const v = viewParam.value.trim();
|
||||
if (!v) return isSingleForm.value ? "fill" : navItems.value.length ? "data" : "unknown";
|
||||
if (v === "data") return "data";
|
||||
if (v === "stats") return "stats";
|
||||
if (v === "fill") return "fill";
|
||||
// 模块行里配置的完整 path 段(如 /app/honor/data 的 "data" 已覆盖;未知段按 unknown 呈现)
|
||||
if (!v || v === "fill") return "fill";
|
||||
if (v === "data") return showDataTab.value ? "data" : "unknown";
|
||||
if (v === "stats") return showStatsTab.value ? "stats" : "unknown";
|
||||
return "unknown";
|
||||
});
|
||||
|
||||
@@ -73,58 +76,46 @@ const currentTitle = computed(() => {
|
||||
return "应用表单";
|
||||
});
|
||||
|
||||
async function loadTree() {
|
||||
async function loadAll() {
|
||||
if (!appCode.value) {
|
||||
errorText.value = "地址里缺少 appCode";
|
||||
return;
|
||||
}
|
||||
loading.value = true;
|
||||
errorText.value = "";
|
||||
emptyTree.value = false;
|
||||
try {
|
||||
// 目标应用的模块树:H4 未上线时后端返回空树(P3 口径),这里如实显示空态
|
||||
const tree: IModule[] = (await baseApi.getUserMod(appCode.value).send()) || [];
|
||||
rawTree.value = tree;
|
||||
// 导航树:path 空节点(权限锚点)跳过(menuNav 规则①,V26 第二半句)
|
||||
navTree.value = buildNavTree(tree, resolveMenuPage);
|
||||
emptyTree.value = navTree.value.length === 0;
|
||||
fillRows.value = (await listFillableForms(appCode.value)) || [];
|
||||
} catch (e: any) {
|
||||
rawTree.value = [];
|
||||
navTree.value = [];
|
||||
errorText.value = e?.message || "目标应用的模块树取不到";
|
||||
emptyTree.value = true;
|
||||
fillRows.value = [];
|
||||
errorText.value = e?.message || "可填清单取不到";
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** 轻导航点击:按模块行的 path 分派到本页的内容视图段 */
|
||||
function gotoNav(item: { page: string }) {
|
||||
const p = item.page;
|
||||
if (/^\/f\//.test(p)) {
|
||||
router.push(p);
|
||||
return;
|
||||
// 导出锚点那棵树:失败或空都按"判不出锚点=不给导出按钮"处理(fail-closed),
|
||||
// 刻意不并进 errorText——它取不到不影响填报/数据/统计三档壳的成立
|
||||
try {
|
||||
rawTree.value = (await baseApi.getUserMod(appCode.value).send()) || [];
|
||||
} catch {
|
||||
rawTree.value = [];
|
||||
}
|
||||
const m = /^\/app\/[^/]+\/(.+)$/.exec(p);
|
||||
const seg = m ? m[1] : p.replace(/^\//, "");
|
||||
router.push(`/app/${appCode.value}/${seg}`);
|
||||
}
|
||||
|
||||
/** 树里第一个可跳转项(单表单直落/默认落点用) */
|
||||
function firstNavPath(): string {
|
||||
return navItems.value[0]?.page || "";
|
||||
/** tab 点击:填报段回到应用入口(保留单表单直落判定),其余段按名分派 */
|
||||
function gotoView(seg: "" | "data" | "stats") {
|
||||
router.push(seg ? `/app/${appCode.value}/${seg}` : `/app/${appCode.value}/`);
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await loadTree();
|
||||
// 单表单直落:树里只有一个填报行且当前没带子视图段 → 直接把内容区渲染成该表单的填报
|
||||
// (不跳转,避免把用户从 /app/{appCode}/ 弹到别处;用户看着地址栏仍是应用入口)
|
||||
if (!viewParam.value && isSingleForm.value) {
|
||||
// 保持在本页渲染填报(activeView 已算成 fill),无需额外动作
|
||||
}
|
||||
});
|
||||
function goFill(row: FillableForm) {
|
||||
router.push(fillRouteOf(row));
|
||||
}
|
||||
|
||||
watch(appCode, () => loadTree());
|
||||
function fillPathText(row: FillableForm): string {
|
||||
const target = fillRouteOf(row);
|
||||
return `#${target.path}${target.query.appCode ? `?appCode=${target.query.appCode}` : ""}`;
|
||||
}
|
||||
|
||||
onMounted(loadAll);
|
||||
watch(appCode, () => loadAll());
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -133,64 +124,75 @@ watch(appCode, () => loadTree());
|
||||
<div class="app-head">
|
||||
<span class="app-tag">快应用</span>
|
||||
<code class="app-code">{{ appCode || "(空)" }}</code>
|
||||
<span class="toolbar-note">模块树按目标应用取(GetUserMod?appCode=),范围以后端为准</span>
|
||||
<Button icon="md-refresh" size="small" @click="loadTree">刷新</Button>
|
||||
<span class="toolbar-note">清单按目标应用取(GET /api/form/my/fillable?appCode=),导航壳按身份渲染,范围以后端为准</span>
|
||||
<Button icon="md-refresh" size="small" :loading="loading" @click="loadAll">刷新</Button>
|
||||
</div>
|
||||
|
||||
<Alert v-if="errorText" show-icon type="error" class="region-tip">{{ errorText }}</Alert>
|
||||
|
||||
<Alert v-else-if="emptyTree" show-icon type="warning" class="region-tip">
|
||||
「{{ appCode }}」这个应用当前没有可进入的模块行。
|
||||
<br />两种可能:① 你的身份没有被授权该应用的任何模块行(受限身份未见集由后端决定);
|
||||
② 平台侧尚未上线目标应用的模块树查询(UserBase 的 appCode 参数,H4)——届时本接口返回空树,
|
||||
<b>本页刻意不回落表单中心自己的菜单</b>(回落会把「要 A 应用的菜单」伪装成「A 应用就这些」)。
|
||||
</Alert>
|
||||
<!-- 导航壳:三档由身份决定(不是由模块树里有什么决定) -->
|
||||
<div class="light-nav">
|
||||
<span class="nav-live">
|
||||
<a :class="['nav-item', { active: activeView === 'fill' }]" href="javascript:;"
|
||||
@click="gotoView('')">填报({{ fillCards.length }})</a>
|
||||
<a v-if="showDataTab" :class="['nav-item', { active: activeView === 'data' }]" href="javascript:;"
|
||||
@click="gotoView('data')">数据管理</a>
|
||||
<a v-if="showStatsTab" :class="['nav-item', { active: activeView === 'stats' }]" href="javascript:;"
|
||||
@click="gotoView('stats')">统计</a>
|
||||
</span>
|
||||
<span class="toolbar-note">数据管理/统计两档按你在这些表单上的层级出现;导出按钮仍按该应用的模块树判</span>
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
<!-- 应用聚合:多于一个可跳转行时给轻导航(单表单直落时不出导航) -->
|
||||
<div v-if="!isSingleForm && navItems.length" class="light-nav">
|
||||
<span class="nav-live">
|
||||
<a v-for="item in navItems" :key="item.id" href="javascript:;"
|
||||
:class="['nav-item', { active: route.path === item.page }]" @click="gotoNav(item)">
|
||||
{{ item.name }}
|
||||
</a>
|
||||
</span>
|
||||
<span class="toolbar-note">path 空的权限锚点行(如"导出")不进导航</span>
|
||||
</div>
|
||||
<h5 class="section-title">{{ currentTitle }}</h5>
|
||||
|
||||
<h5 class="section-title">{{ currentTitle }}</h5>
|
||||
|
||||
<!-- 数据/统计视图复用中心管理端组件:定义管理权不在快应用,codes 只来自模块树的填报行;
|
||||
embedded 让它只渲染本体,避免与本页的白卡/内边距叠成两层;
|
||||
modules 把**本页刚拉到的那棵应用树**传进去——导出按钮的锚点判定吃它,不吃中心会话树(P76) -->
|
||||
<Data v-if="activeView === 'data'" context="app" :app-code="appCode" :codes="fillCodes"
|
||||
:modules="rawTree" embedded />
|
||||
<Stats v-else-if="activeView === 'stats'" context="app" :app-code="appCode" :codes="fillCodes" embedded />
|
||||
<template v-else-if="activeView === 'fill'">
|
||||
<!-- 数据/统计视图复用中心管理端组件:定义管理权不在快应用,codes 来自可填清单(接口驱动,P95);
|
||||
embedded 让它只渲染本体,避免与本页白卡/内边距叠成两层;
|
||||
modules 把**本页拉到的那棵应用树**传进去——导出按钮的锚点判定吃它,不吃中心会话树(P76) -->
|
||||
<Data v-if="activeView === 'data'" context="app" :app-code="appCode" :codes="dataCodes"
|
||||
:modules="rawTree" embedded />
|
||||
<Stats v-else-if="activeView === 'stats'" context="app" :app-code="appCode" :codes="statsCodes" embedded />
|
||||
<template v-else-if="activeView === 'fill'">
|
||||
<!-- 单表单直落:只有一个可填项时不叠列表,内容区直接给这张表的填报入口 -->
|
||||
<template v-if="isSingleForm">
|
||||
<Alert show-icon type="info">
|
||||
单表单直落:该应用只授权了一个填报行,内容区直接渲染填报,不再叠一层导航。
|
||||
<br />填报目标:<code>{{ firstNavPath() || "(树里没有 /f/{code} 行)" }}</code>
|
||||
单表单直落:该应用在你名下只有一个可填入口,内容区直接给它,不再叠一层导航。
|
||||
<br />填报目标:<code>{{ fillPathText(fillCards[0]) }}</code>
|
||||
</Alert>
|
||||
<div class="fill-mount">
|
||||
<div class="fill-hint">
|
||||
直接进入填报页:
|
||||
<a href="javascript:;" @click="firstNavPath() && router.push(firstNavPath())">
|
||||
{{ firstNavPath() || "—" }}
|
||||
</a>
|
||||
<a href="javascript:;" @click="goFill(fillCards[0])">{{ fillCards[0].name }}</a>
|
||||
(填报页本体是 T7 交付的 /f/:code,本页不复制一份)
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<Alert v-else show-icon type="warning">
|
||||
该应用模块行里的这一段路径没有对应视图(本页只认识 data / stats / fill 三种,
|
||||
以及 path 形如 /f/{code} 的填报行)。请核对平台侧的模块行配置。
|
||||
<div v-else-if="fillCards.length" class="fill-cards">
|
||||
<div v-for="row in fillCards" :key="row.code" class="fill-card">
|
||||
<div class="fill-card-main">
|
||||
<div class="fill-card-name">{{ row.name }}</div>
|
||||
<code class="fill-card-path">{{ fillPathText(row) }}</code>
|
||||
</div>
|
||||
<Button type="primary" @click="goFill(row)">进入填报</Button>
|
||||
</div>
|
||||
</div>
|
||||
<Alert v-else show-icon type="warning" class="region-tip">
|
||||
「{{ appCode }}」这个应用在你名下当前没有可填报的表单。
|
||||
<br />表单发布后会按它的<b>面向人群</b>自动出现在这里(面向学生的表出现在学生账号下,
|
||||
面向教师的表出现在教师账号下)——与基础平台的模块行授权无关;
|
||||
如果你是教师且这里为空,说明该应用没有面向学生的表单可代填。
|
||||
</Alert>
|
||||
</template>
|
||||
<Alert v-else show-icon type="warning">
|
||||
这一段路径对你不可见(本页只有 填报 / 数据管理 / 统计 三档,后两档按你在这些表单上的层级出现)。
|
||||
<br />能直接地址进来但看不到档,说明层级不够——数据面与统计的把关在后端,不在这里。
|
||||
</Alert>
|
||||
|
||||
<p class="toolbar-note foot-note">
|
||||
V26 前端侧三半句:①无导出授权不画导出按钮(快应用按 path 空的"导出"锚点行判)——
|
||||
导出按钮在数据视图内按该规则显隐;②导航不渲染 path 空节点;③学生身份只见填报路由(树里授权的直接结果)。
|
||||
接口侧的拒绝是 T4 已验的另一半,前端藏按钮不等于授权。
|
||||
V26 前端侧三半句(P95 改版):①无导出授权不画导出按钮——快应用按该应用树里 path 空的"导出"
|
||||
锚点行判(本页把那棵树原样交给内嵌 Data.vue,判不中就不画,fail-closed);
|
||||
②导航不再由模块树生成,按身份三档写死,path 空节点自然不进导航;
|
||||
③学生身份只见填报档=层①只给 canFill、不给数据/统计档的直接结果。
|
||||
接口侧的拒绝是 T4 已验的另一半,<b>前端藏按钮/藏档不等于授权</b>。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -264,6 +266,32 @@ watch(appCode, () => loadTree());
|
||||
font-size: 13px;
|
||||
color: #515a6e;
|
||||
}
|
||||
.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-path {
|
||||
color: #808695;
|
||||
font-size: 12px;
|
||||
margin-top: 4px;
|
||||
display: inline-block;
|
||||
}
|
||||
.foot-note {
|
||||
display: block;
|
||||
margin-top: 20px;
|
||||
|
||||
+53
-41
@@ -9,10 +9,11 @@
|
||||
* 两者是**两个端点、两个谓词**,页面按路由分派;顶部 Tab 只是 UI 导航(切路由),不是接口参数。
|
||||
* 管理员代填后:这条答卷在**填表人的"我提交的"**里出现,同时在**被代填人的"关于我的"**里出现(V18)。
|
||||
*
|
||||
* P91 起第三视图「可填写」(/my/fillable):这不是答卷视图,是**填报入口列表**——
|
||||
* 数据源=会话菜单树里 page 形如 /f/{code} 的行(App.vue 已拉过、store.modules 存原件),
|
||||
* 复用 buildNavTree+flattenNav 同一套解析(与左侧导航同口径,天然吃到 menuRoute 的修复)。
|
||||
* **不得在此按角色过滤**:树里有什么就是什么,授权语义在后端(任务书 T8 红线同源)。
|
||||
* P91 起第三视图「可填写」(/my/fillable),P95 W4 换数据源:这不再是答卷视图,而是**填报入口列表**。
|
||||
* 数据源从"会话菜单树里挑 path 形如 /f/{code} 的行"改成 `GET /api/form/my/fillable`——
|
||||
* 谁能填哪张表由中心按三层模型(audience × 身份 × 管理员两表)自解析,
|
||||
* UserBase 侧不再需要逐表单配 `/f/` 模块行(任务书 P95 §0.1/§0.2 红线 6:分享链接与直链解析不动)。
|
||||
* 前端照旧**不做本地角色过滤**:接口下发 canFill=true 的才画卡(canFill 是后端算的,不是我筛的)。
|
||||
*
|
||||
* 三条交互口径沿 T7 的"我的提交"段(不另造轮子):
|
||||
* 1. **编辑入口只在"我提交的"**:编辑=填表人的动作;"关于我的"整列只读
|
||||
@@ -30,14 +31,13 @@ import { useRoute, useRouter } from "vue-router";
|
||||
import { Message, Modal } from "view-ui-plus";
|
||||
import { actionColumn } from "@/utils/tableActions";
|
||||
import { useElementHeight } from "@/utils/hook/useElementHeight";
|
||||
import { deleteMyAnswer, getMyAnswerDetail, listAnswersAboutMe, listMySubmitted } from "@/api/module/fill";
|
||||
import { deleteMyAnswer, getMyAnswerDetail, listAnswersAboutMe, listFillableForms, listMySubmitted } from "@/api/module/fill";
|
||||
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 { fillRouteOf } from "@/utils/fillLink";
|
||||
import { useDefaultStore } from "@/store";
|
||||
import type { AnswerDetail, MyAnswerViewItem } from "@/types/data";
|
||||
import type { AnswerDetail, FillableForm, MyAnswerViewItem } from "@/types/data";
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
@@ -66,9 +66,9 @@ const VIEW_META = {
|
||||
},
|
||||
fillable: {
|
||||
title: "可填写",
|
||||
endpoint: "数据源:会话菜单树(GetUserMod)",
|
||||
predicate: "谓词:模块行 path 形如 /f/{code}(授权在后端)",
|
||||
note: "当前账号被授权的填报入口。列表由会话菜单树直接推导——树里没有的行这里不出现,也不做本地角色过滤。"
|
||||
endpoint: "GET /api/form/my/fillable",
|
||||
predicate: "谓词:后端按三层模型解析出的填报入口(canFill=true 才画卡)",
|
||||
note: "这里是当前账号能填报的表单。表单发布后按面向人群自动出现在这里——不需要有人在基础平台逐张表配模块行。"
|
||||
}
|
||||
} as const;
|
||||
|
||||
@@ -78,10 +78,19 @@ const regionRef = ref<HTMLElement | null>(null);
|
||||
const regionH = useElementHeight(regionRef);
|
||||
|
||||
const rows = ref<MyAnswerViewItem[]>([]);
|
||||
const fillableRows = ref<FillableForm[]>([]);
|
||||
const loading = ref(false);
|
||||
const loadedOnce = ref(false);
|
||||
const errorText = ref("");
|
||||
|
||||
/**
|
||||
* 「可填写」的卡片集=接口里 `canFill=true` 的行。
|
||||
*
|
||||
* <p>过滤条件只有这一条,且判定<b>来自后端</b>:`canFill=false` 的行(教师×面向学生那种"只能代填")
|
||||
* 由后端按三层模型算出,前端只是不把它画成"进入填报"的卡——前端不重新按角色判一遍(T8 红线:范围以后端为准)。</p>
|
||||
*/
|
||||
const fillableCards = computed<FillableForm[]>(() => fillableRows.value.filter((r) => r.canFill));
|
||||
|
||||
/** 会话用户 id:判"这条是不是我提交的"(编辑/删除入口显隐),身份来源仍是会话(不传后端) */
|
||||
const sessionUserId = computed(() => store.sessionUserId);
|
||||
|
||||
@@ -90,14 +99,19 @@ function isMine(row: MyAnswerViewItem): boolean {
|
||||
}
|
||||
|
||||
async function load() {
|
||||
if (view.value === "fillable") return; // 数据源=本地菜单树,无需请求
|
||||
loading.value = true;
|
||||
errorText.value = "";
|
||||
try {
|
||||
rows.value = view.value === "about" ? await listAnswersAboutMe() : await listMySubmitted();
|
||||
if (view.value === "fillable") {
|
||||
// P95 W4:入口清单来自后端解析(不再是会话菜单树);缺省 appCode=全部应用
|
||||
fillableRows.value = (await listFillableForms()) || [];
|
||||
} else {
|
||||
rows.value = view.value === "about" ? await listAnswersAboutMe() : await listMySubmitted();
|
||||
}
|
||||
loadedOnce.value = true;
|
||||
} catch (e: any) {
|
||||
rows.value = [];
|
||||
fillableRows.value = [];
|
||||
errorText.value = e?.message || "加载失败";
|
||||
Message.error(errorText.value);
|
||||
} finally {
|
||||
@@ -110,25 +124,17 @@ function switchView(target: "submit" | "about" | "fillable") {
|
||||
router.push({ path: `/${target === "submit" ? "my/submit" : "my/" + target}` });
|
||||
}
|
||||
|
||||
// ==================== P91 可填写视图:菜单树 → 填报入口卡片 ====================
|
||||
// ==================== 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;
|
||||
});
|
||||
});
|
||||
/** 点卡片=进 /f/{code}(中心表单不带 query,快应用带 ?appCode=,与复制出去的分享链接同一条地址) */
|
||||
function goFill(row: FillableForm) {
|
||||
router.push(fillRouteOf(row));
|
||||
}
|
||||
|
||||
function goFill(page: string) {
|
||||
router.push({ path: page });
|
||||
/** 卡片上那行灰字:把"点进去会到哪个地址"如实写出来(与 fillRouteOf 同一份规则,不另算一遍) */
|
||||
function fillPathText(row: FillableForm): string {
|
||||
const route = fillRouteOf(row);
|
||||
return `#${route.path}${route.query.appCode ? `?appCode=${route.query.appCode}` : ""}`;
|
||||
}
|
||||
|
||||
// ==================== 详情 / 编辑 / 删除 ====================
|
||||
@@ -242,7 +248,7 @@ onMounted(load);
|
||||
<Radio label="submit">我提交的</Radio>
|
||||
<Radio label="about">关于我的</Radio>
|
||||
</RadioGroup>
|
||||
<Button v-if="view !== 'fillable'" icon="md-refresh" @click="load">刷新</Button>
|
||||
<Button icon="md-refresh" @click="load">刷新</Button>
|
||||
<span class="toolbar-note">
|
||||
{{ meta.endpoint }} | {{ meta.predicate }}
|
||||
<template v-if="view !== 'fillable'">(两个端点两个谓词,不做参数分支)</template>
|
||||
@@ -255,27 +261,29 @@ onMounted(load);
|
||||
<br />本视图不渲染编辑/删除入口(填表人才能改自己提交的那条);也不渲染改状态(填表人在接口层即被拒)。
|
||||
</template>
|
||||
<template v-else-if="view === 'fillable'">
|
||||
<br />入口来自基础平台的「填报」模块行(path=/f/{表单code},授权给组后这里才会出现)。
|
||||
<br />入口由表单中心按<b>面向人群 × 你的身份</b>解析(P95 三层模型),与基础平台的模块行无关;
|
||||
教师面向学生表单只能<b>代填本校学生</b>,那种表不在这里出现(在快应用上下文的数据管理里)。
|
||||
</template>
|
||||
</Alert>
|
||||
|
||||
<!-- P91 可填写:卡片列表(菜单名 + 进入填报),数据源=会话菜单树 -->
|
||||
<!-- P95 可填写:卡片列表(名称 + 进入填报),数据源=GET /api/form/my/fillable -->
|
||||
<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 v-if="fillableCards.length" class="fill-cards">
|
||||
<div v-for="item in fillableCards" :key="`${item.appCode}/${item.code}`" 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>
|
||||
<code class="fill-card-path">{{ fillPathText(item) }}</code>
|
||||
</div>
|
||||
<Button type="primary" @click="goFill(item.page)">进入填报</Button>
|
||||
<Button type="primary" @click="goFill(item)">进入填报</Button>
|
||||
</div>
|
||||
</div>
|
||||
<Alert v-else show-icon type="warning">
|
||||
当前账号没有可填写的表单入口。请找管理员在基础平台为本应用配置「填报」模块行
|
||||
(path 填 /f/{表单code}),并授权给你所在的组织组。
|
||||
<Alert v-else-if="loadedOnce" show-icon type="warning">
|
||||
当前账号没有可填写的表单。表单发布后,会按它的<b>面向人群</b>自动出现在这里
|
||||
(面向学生的表出现在学生账号下,面向教师的表出现在教师账号下)——
|
||||
不需要在基础平台为本应用逐张配置「填报」模块行。
|
||||
</Alert>
|
||||
</template>
|
||||
|
||||
@@ -284,10 +292,14 @@ onMounted(load);
|
||||
no-data-text="这里还没有数据" class="link-table" />
|
||||
</div>
|
||||
|
||||
<p class="toolbar-note foot-note">
|
||||
<p v-if="view !== 'fillable'" class="toolbar-note foot-note">
|
||||
共 {{ rows.length }} 条(跨表单,不带值文档——要看值走详情,值文档与钉住版本的 schema 一起下发的,
|
||||
渲染与校验同源)。
|
||||
</p>
|
||||
<p v-else class="toolbar-note foot-note">
|
||||
共 {{ fillableCards.length }} 张可填(接口还包含"只能代填"的那些行,本列表只画 canFill=true 的;
|
||||
排序按截止时间,不限截止的在最后)。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Modal v-model="detailOpen" :title="`答卷详情(${detail?.formName || detail?.code || ''})`" :width="720"
|
||||
|
||||
@@ -14,8 +14,9 @@
|
||||
* 前端不据此再过滤一遍(任务书 T8 红线)。
|
||||
*
|
||||
* 快应用上下文(AppContext)复用本组件:传 `context="app"` + `app-code` + `codes`
|
||||
* (候选来自该应用模块树的 `/f/{code}` 行,经 resolve 拿 uuid——定义管理权不在快应用上下文里,§3.4)
|
||||
* + `modules`(**同一棵原始模块树**,按钮级权限按它判,P76)。
|
||||
* (候选来自 `GET /api/form/my/fillable?appCode=` 按层级筛出的 code,经 resolve 拿 uuid——
|
||||
* 定义管理权不在快应用上下文里,§3.4;P95 W5 起不再从该应用模块树挑 `/f/` 行)
|
||||
* + `modules`(**同一棵原始模块树**,只给按钮级权限判定用,P76)。
|
||||
*/
|
||||
import { computed, h, nextTick, onBeforeUnmount, onMounted, ref, watch } from "vue";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
@@ -51,7 +52,7 @@ const props = withDefaults(
|
||||
context?: "center" | "app";
|
||||
/** app 上下文的归属应用(center 留空=form-center) */
|
||||
appCode?: string;
|
||||
/** app 上下文的候选 code(来自该应用模块树的 /f/{code} 行) */
|
||||
/** app 上下文的候选 code(来自 /api/form/my/fillable 的层级筛取,P95 W5) */
|
||||
codes?: string[];
|
||||
/**
|
||||
* 按钮级权限判定的**那棵树**(P76):app 上下文必须由宿主传入目标应用的模块树
|
||||
|
||||
Reference in New Issue
Block a user