t13:前端换形——audience 数组多选 + 冻结/匿名窗口开关,fillable 行去 canFill/myTier

- types/form.ts:AUDIENCE 改字符串码闭集(含 ANONYMOUS)+ audienceText();
  FormView/FormConfigRequest 加 audience 数组、frozen、anonCookieMinutes
- types/data.ts:FillableForm 去 canFill/myTier(C2,入集即可填);
  DATA_SCOPE_LABELS 由四枚举改两档(ALL/SELF,§19 作废-1)
- FormConfigModal:面向人群改复选组(多选共集)+ 冻结开关(文案指路"喂外部业务应用的表单请开启")
  + 匿名防重窗口;含 ANONYMOUS 时限次置灰并在提交前挡(与后端同判据)
- Forms.vue 面向列、My.vue「可填写」卡集、fill.ts/routes.ts 注记同步换形
- AppContext.vue:填报 tab 直接吃入集行;数据/统计两档的驱动位改由 T14 的 managed 端点接,
  本批不留"按 myTier 猜"的兼容分支(壳零自有权限逻辑)
- mock:formapi/dataapi 的 audience 数组形态 + 匿名×限次跨字段约束
This commit is contained in:
chenxing
2026-10-20 12:54:26 +08:00
parent 25dc3eb2e5
commit 4a7db3d22c
11 changed files with 174 additions and 88 deletions
+11 -14
View File
@@ -41,20 +41,17 @@ const rawTree = ref<IModule[]>([]);
const loading = ref(false);
const errorText = ref("");
/** 填报 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"));
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));
/** 填报 tab 的行:契约 v2 C2 之后入集即等价于可填(行里没有 canFill,前端不再筛) */
const fillCards = computed(() => fillRows.value);
/**
* 数据管理/统计两档的驱动位在 T13 这一批处于<b>换轨空窗</b>:
* 原驱动是 P95 的 `myTier`(三层模型的最高层),随 §19 作废-3 一起删掉了;
* 新驱动是 T14 的 `GET /api/form/my/managed`(创建人 ∪ form_admin 的"我管理的表单")。
* 本批同一次执行内就会接上,空窗期这两档不出现在壳上——<b>刻意不留"按 myTier 猜"的兼容分支</b>,
* 免得留下第二套判定口径(红线:壳零自有权限逻辑)。
*/
const dataCodes = computed<string[]>(() => []);
const statsCodes = computed<string[]>(() => []);
const showDataTab = computed(() => dataCodes.value.length > 0);
const showStatsTab = computed(() => statsCodes.value.length > 0);
+10 -9
View File
@@ -9,11 +9,11 @@
* 两者是**两个端点、两个谓词**,页面按路由分派;顶部 Tab 只是 UI 导航(切路由),不是接口参数。
* 管理员代填后:这条答卷在**填表人的"我提交的"**里出现,同时在**被代填人的"关于我的"**里出现(V18)。
*
* P91 起第三视图「可填写」(/my/fillable),P95 W4 换数据源:这不再是答卷视图,而是**填报入口列表**。
* 数据源从"会话菜单树里挑 path 形如 /f/{code} 的行"改成 `GET /api/form/my/fillable`——
* 谁能填哪张表由中心按三层模型(audience × 身份 × 管理员两表)自解析,
* P91 起第三视图「可填写」(/my/fillable),P95 W4 换数据源、契约 v2 C2 定行形状:这不再是答卷视图,
* 而是**填报入口列表**。数据源从"会话菜单树里挑 path 形如 /f/{code} 的行"改成 `GET /api/form/my/fillable`——
* 谁能填哪张表由中心按 <b>audience 门</b>(会话身份类型 ∈ 表单面向人群)自解析,
* UserBase 侧不再需要逐表单配 `/f/` 模块行(任务书 P95 §0.1/§0.2 红线 6:分享链接与直链解析不动)。
* 前端照旧**不做本地角色过滤**:接口下发 canFill=true 的才画卡(canFill 是后端算的,不是我筛的)。
* 前端照旧**不做本地角色过滤**:接口下发什么就画什么(入集即等价于可填,行里不再有 canFill)。
*
* 三条交互口径沿 T7 的"我的提交"段(不另造轮子):
* 1. **编辑入口只在"我提交的"**:编辑=填表人的动作;"关于我的"整列只读
@@ -67,7 +67,7 @@ const VIEW_META = {
fillable: {
title: "可填写",
endpoint: "GET /api/form/my/fillable",
predicate: "谓词:后端按三层模型解析出的填报入口(canFill=true 才画卡)",
predicate: "谓词:会话身份类型 ∈ form.audience(audience 门,与提交时同一公式)",
note: "这里是当前账号能填报的表单。表单发布后按面向人群自动出现在这里——不需要有人在基础平台逐张表配模块行。"
}
} as const;
@@ -84,12 +84,13 @@ const loadedOnce = ref(false);
const errorText = ref("");
/**
* 「可填写」的卡片集=接口里 `canFill=true` 的行。
* 「可填写」的卡片集=接口下发的全部行。
*
* <p>过滤条件只有这一条,且判定<b>来自后端</b>:`canFill=false` 的行(教师×面向学生那种"只能代填")
* 由后端按三层模型算出,前端只是不把它画成"进入填报"的卡——前端不重新按角色判一遍(T8 红线:范围以后端为准)。</p>
* <p>契约 v2 C2 之后行里没有 `canFill`:<b>入集即等价于可填</b>(后端按 audience 门筛过一次,
* 不再有"进了列表但不能自填、只给代填"的中间态)。前端依旧不做任何本地身份过滤
* (T8 红线:范围与资格以后端为准,这里只画后端给的东西)。</p>
*/
const fillableCards = computed<FillableForm[]>(() => fillableRows.value.filter((r) => r.canFill));
const fillableCards = computed<FillableForm[]>(() => fillableRows.value);
/** 会话用户 id:判"这条是不是我提交的"(编辑/删除入口显隐),身份来源仍是会话(不传后端) */
const sessionUserId = computed(() => store.sessionUserId);
+4 -3
View File
@@ -21,7 +21,7 @@ import { useElementHeight } from "@/utils/hook/useElementHeight";
import FormConfigModal from "@/components/form/FormConfigModal.vue";
import FormAdminModal from "@/components/admin/FormAdminModal.vue";
import type { FormView } from "@/types/form";
import { AUDIENCE, FORM_STATUS } from "@/types/form";
import { FORM_STATUS, audienceText } from "@/types/form";
import { CODE_PATTERN, MAX_NAME_LEN } from "@/types/formSchema";
const router = useRouter();
@@ -86,8 +86,9 @@ const columns = computed(() => [
{
title: "面向",
key: "audience",
width: 90,
render: (h2: any, p: any) => h2("span", p.row.audience === AUDIENCE.TEACHER ? "面向教师" : "面向学生")
width: 120,
// 人群是多值(契约 v2 C1),列宽给得比原来宽一点:"学生/教师/匿名" 三种组合都要能读完
render: (h2: any, p: any) => h2("span", audienceText(p.row.audience))
},
{
title: "截止",