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
+50 -13
View File
@@ -13,8 +13,8 @@
import { computed, reactive, ref, watch } from "vue";
import { Message, Modal } from "view-ui-plus";
import { formApi } from "@/api/module/form";
import type { FormView, StatusDefView } from "@/types/form";
import { AUDIENCE_OPTIONS, FORM_STATUS } from "@/types/form";
import type { AudienceCode, FormView, StatusDefView } from "@/types/form";
import { AUDIENCE, AUDIENCE_OPTIONS, FORM_STATUS } from "@/types/form";
import { MAX_NAME_LEN, CODE_PATTERN } from "@/types/formSchema";
const props = defineProps<{ form: FormView | null }>();
@@ -23,30 +23,39 @@ const emit = defineEmits<{ (e: "saved", form: FormView): void }>();
const visible = ref(false);
const saving = ref(false);
const model = reactive<{
audience: number;
audience: string[];
fillLimit: number;
deadline: string;
answerNoEnabled: number;
frozen: number;
anonCookieMinutes: number;
groupTag: string;
}>({
audience: AUDIENCE_OPTIONS[0].value,
audience: [AUDIENCE.STUDENT],
fillLimit: 0,
deadline: "",
answerNoEnabled: 0,
frozen: 0,
anonCookieMinutes: 10,
groupTag: ""
});
const statusDefs = ref<StatusDefView[]>([]);
const newStatus = reactive({ code: "", label: "" });
/** 含匿名时后端强制 fill_limit=0(§3.2/作废-11:匿名无 limit_key)——前端同判据提前挡,不给点了才报错 */
const anonymousChosen = computed(() => model.audience.includes(AUDIENCE.ANONYMOUS));
watch(
() => [visible.value, props.form?.formUuid] as const,
() => {
const f = props.form;
if (!visible.value || !f) return;
model.audience = f.audience ?? AUDIENCE_OPTIONS[0].value;
model.audience = f.audience?.length ? [...f.audience] : [AUDIENCE.STUDENT];
model.fillLimit = f.fillLimit ?? 0;
model.deadline = f.deadline ? String(f.deadline).slice(0, 10) : "";
model.answerNoEnabled = f.answerNoEnabled ?? 0;
model.frozen = f.frozen ?? 0;
model.anonCookieMinutes = f.anonCookieMinutes ?? 10;
model.groupTag = f.groupTag || "";
statusDefs.value = (f.statusDefs || []).map((d) => ({ ...d }));
newStatus.code = "";
@@ -110,15 +119,25 @@ async function submit() {
Message.error(`开启报名号要求 code ≤49 位(当前 ${f.code.length})`);
return;
}
if (!model.audience.length) {
Message.error("面向人群至少要选一项(空集合等于谁都不能填)");
return;
}
if (anonymousChosen.value && model.fillLimit === 1) {
Message.error("含「匿名可填」的表单不支持限次(匿名没有可比对的身份),请把限次改回不限");
return;
}
saving.value = true;
try {
await formApi
.config({
uuid: f.formUuid,
audience: model.audience,
audience: model.audience as AudienceCode[],
fillLimit: model.fillLimit,
deadline: model.deadline,
answerNoEnabled: model.answerNoEnabled,
frozen: model.frozen,
anonCookieMinutes: model.anonCookieMinutes,
groupTag: model.groupTag
})
.send();
@@ -184,21 +203,24 @@ defineExpose({ open });
<div class="cfg-row">
<span class="cfg-label">面向人群</span>
<RadioGroup v-model="model.audience">
<Radio v-for="a in AUDIENCE_OPTIONS" :key="a.value" :label="a.value">{{ a.label }}</Radio>
</RadioGroup>
<span class="cfg-note">决定数据范围矩阵落在哪一列(契约 §8)</span>
<CheckboxGroup v-model="model.audience">
<Checkbox v-for="a in AUDIENCE_OPTIONS" :key="a.value" :label="a.value">{{ a.label }}</Checkbox>
</CheckboxGroup>
<span class="cfg-note">
可多选(契约 v2 C1)。填报资格=登录身份类型落在集合内;「匿名可填」只对无会话的
<code>/anon</code> 通道生效,登录用户不会因此多出入口
</span>
</div>
<div class="cfg-row">
<span class="cfg-label">限次</span>
<RadioGroup v-model="model.fillLimit">
<Radio :label="0">不限次数</Radio>
<Radio :label="1">每人限填一次</Radio>
<Radio :label="1" :disabled="anonymousChosen">每人限填一次</Radio>
</RadioGroup>
<span class="cfg-note">
「每人」按提交主体判:管理员代填指定了被代填人时按那个人限,否则按提交人限;
软删答卷会释放名额(契约 §5)
「每人」按提交主体判:管理员代录指定了被记录人时按那个人限,否则按提交人限;软删答卷会释放名额。
<template v-if="anonymousChosen">含匿名人群时不可限次(匿名无 limit_key,防重只有 cookie)。</template>
</span>
</div>
@@ -217,6 +239,21 @@ defineExpose({ open });
</span>
</div>
<div class="cfg-row">
<span class="cfg-label">提交后禁编辑</span>
<i-switch v-model="model.frozen" :true-value="1" :false-value="0" />
<span class="cfg-note">
开启后提交人与管理员的编辑通道同时关闭,修正只能「撤回(软删)+ 重新提交」——
<b>喂外部业务应用的表单请开启</b>(原件是档案,业务对象归应用)
</span>
</div>
<div class="cfg-row">
<span class="cfg-label">匿名防重窗口</span>
<InputNumber v-model="model.anonCookieMinutes" :min="1" :max="1440" style="width: 140px" />
<span class="cfg-note">分钟。仅对面向人群含「匿名可填」的表单有意义(默认 10 分钟)</span>
</div>
<div class="cfg-row">
<span class="cfg-label">分组标签</span>
<Input v-model="model.groupTag" :maxlength="64" placeholder="可空,用于列表筛选归类" style="width: 220px" />