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:
@@ -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" />
|
||||
|
||||
Reference in New Issue
Block a user