P76(T8 验收遗留):快应用上下文的导出按钮此前恒按中心会话树判"导出"锚点 (Data.vue exportable 读 store.modules),锚点判定不生效(fail-closed 方向, 有授权也不画按钮)。本轮把 AppContext 拉到的**目标应用树**经 modules prop 下传, Data.vue 的权限判据改为 permsTree(app=应用树 / center=会话树),canExport 纯函数不动。 Stats.vue 未接:本页无锚点判定也无 store.modules 引用,加 prop 是死代码(差异记 T9 报告)。 P77(T8 验收遗留):应用上下文内的"详情"不再跳回中心路由 /admin/data/detail。 形态二选一取**子路由 /app/:appCode/data/detail**(同一个 AnswerDetail.vue): 地址保留 uuid 可深链/刷新、详情页只有一份实现、back() 本就按 app 回应用数据视图, 写 parentPath 反而造假(menuRoute 的 ROUTE_PATHS 只收静态 path,应用段导航在页内轻导航)。 goDetail 按 props.context 分派;back() 改读 route.params.appCode,撤掉 query.app 双轨。 观察项①(T8 验收记档):确认跨触底页勾选**会被清空**——view-ui-plus 的 data 监听器 用 makeObjBaseData 重建 objData,_isChecked 只认行上的 _checked(点选不回写行), 整组替换后勾选视觉与 getSelection 归零而"已选 N 条"仍累计。新增 withSelectionState() 在 reload/loadMore 两条装载路径按 selected 回填 _checked,selected 为唯一事实源。 演练桩(本地件,dev:mock 生效):baseapi 增 fc-drill 快应用模块树(方案 §6.2 五行含 导入/导出两个 path 空锚点),dataapi 增 /api/form/resolve 桩(快应用数据视图唯一的 合规取数路径是 code→resolve→uuid,契约 §1)。其余外指 appCode 仍旧返回空树(P3 口径)。 门禁:npm run build ✓ / npm run type-check 0 错 ✓; t1~t9 全量复跑:t5 13/0/1、t6 22/0/2、t7 12/0/0、t8 21/0/1、t9 12/0/1 (skip 均为 headless Edge --dump-dom 吐 0 字节的 P73 环境事实,不假通过)。 本提交含 T6~T8 三轮堆在工作区的既有改动(沿 T8 验收 §5 推送分层)。
This commit is contained in:
@@ -0,0 +1,321 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* 表单级配置面板(方案 §5.1:配置与状态定义**不进版本快照**,改它们不升版)。
|
||||
*
|
||||
* 三处口径按后端原样,不自己发明:
|
||||
* - **限次只有 fillLimit 一个开关**:`limitScope` 不是表单可配项,它是提交时按 subject_id
|
||||
* 推导的语义(subject 非空按被代填人限、否则按提交人限——契约 §5)。所以这里给的是解释文案,
|
||||
* 不是一个假的单选框(本轮问题单 P53)。
|
||||
* - 截止时间只到日期时后端补 23:59:59(方案 §5.4),空=不限。
|
||||
* - 状态定义:code 建后不改(历史答卷按 code 存状态,改 code=改写历史),能改的只有 label
|
||||
* 与「废弃」标记;废弃是软删(V13 历史仍显示其名),且至少留一个可用状态。
|
||||
*/
|
||||
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 { MAX_NAME_LEN, CODE_PATTERN } from "@/types/formSchema";
|
||||
|
||||
const props = defineProps<{ form: FormView | null }>();
|
||||
const emit = defineEmits<{ (e: "saved", form: FormView): void }>();
|
||||
|
||||
const visible = ref(false);
|
||||
const saving = ref(false);
|
||||
const model = reactive<{
|
||||
audience: number;
|
||||
fillLimit: number;
|
||||
deadline: string;
|
||||
answerNoEnabled: number;
|
||||
groupTag: string;
|
||||
}>({
|
||||
audience: AUDIENCE_OPTIONS[0].value,
|
||||
fillLimit: 0,
|
||||
deadline: "",
|
||||
answerNoEnabled: 0,
|
||||
groupTag: ""
|
||||
});
|
||||
const statusDefs = ref<StatusDefView[]>([]);
|
||||
const newStatus = reactive({ code: "", label: "" });
|
||||
|
||||
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.fillLimit = f.fillLimit ?? 0;
|
||||
model.deadline = f.deadline ? String(f.deadline).slice(0, 10) : "";
|
||||
model.answerNoEnabled = f.answerNoEnabled ?? 0;
|
||||
model.groupTag = f.groupTag || "";
|
||||
statusDefs.value = (f.statusDefs || []).map((d) => ({ ...d }));
|
||||
newStatus.code = "";
|
||||
newStatus.label = "";
|
||||
}
|
||||
);
|
||||
|
||||
const isPlatform = computed(() => props.form?.schId === 0);
|
||||
const statusText = computed(() => {
|
||||
const f = props.form;
|
||||
if (!f) return "";
|
||||
if (f.status === FORM_STATUS.DRAFT) return "草稿(从未发布)";
|
||||
if (f.status === FORM_STATUS.PUBLISHED) return `已发布 v${f.currentVersion}`;
|
||||
return `已停用(当前发布 v${f.currentVersion})`;
|
||||
});
|
||||
|
||||
function open() {
|
||||
visible.value = true;
|
||||
}
|
||||
|
||||
function addStatus() {
|
||||
const code = newStatus.code.trim();
|
||||
const label = newStatus.label.trim();
|
||||
if (!code || !CODE_PATTERN.test(code)) {
|
||||
Message.error("状态 code 仅限字母/数字/下划线(≤64 位)");
|
||||
return;
|
||||
}
|
||||
if (!label) {
|
||||
Message.error("状态名称不能为空");
|
||||
return;
|
||||
}
|
||||
if (statusDefs.value.some((d) => d.code === code)) {
|
||||
// 含已废弃的 code:复用它会改写历史答卷的状态名(后端同判)
|
||||
Message.error(`状态 code 已存在(含已废弃的):${code}`);
|
||||
return;
|
||||
}
|
||||
statusDefs.value.push({ code, label, is_deleted: false });
|
||||
newStatus.code = "";
|
||||
newStatus.label = "";
|
||||
}
|
||||
|
||||
function moveStatus(index: number, dir: -1 | 1) {
|
||||
const target = index + dir;
|
||||
if (target < 0 || target >= statusDefs.value.length) return;
|
||||
const list = statusDefs.value;
|
||||
[list[index], list[target]] = [list[target], list[index]];
|
||||
}
|
||||
|
||||
function liveCount(defs: StatusDefView[]): number {
|
||||
return defs.filter((d) => !d.is_deleted).length;
|
||||
}
|
||||
|
||||
async function submit() {
|
||||
const f = props.form;
|
||||
if (!f) return;
|
||||
if (statusDefs.value.length && liveCount(statusDefs.value) === 0) {
|
||||
Message.error("至少要保留一个可用状态(全部废弃后新数据无状态可给)");
|
||||
return;
|
||||
}
|
||||
if (model.answerNoEnabled === 1 && f.code.length > 49) {
|
||||
Message.error(`开启报名号要求 code ≤49 位(当前 ${f.code.length})`);
|
||||
return;
|
||||
}
|
||||
saving.value = true;
|
||||
try {
|
||||
await formApi
|
||||
.config({
|
||||
uuid: f.formUuid,
|
||||
audience: model.audience,
|
||||
fillLimit: model.fillLimit,
|
||||
deadline: model.deadline,
|
||||
answerNoEnabled: model.answerNoEnabled,
|
||||
groupTag: model.groupTag
|
||||
})
|
||||
.send();
|
||||
// 状态定义为空列表时不能传空数组(后端拒),传 null 才是「回默认待处理/已处理」
|
||||
const defs = statusDefs.value;
|
||||
const latest = await formApi
|
||||
.statusDefs({ uuid: f.formUuid, statusDefs: defs.length ? defs : null })
|
||||
.send();
|
||||
emit("saved", latest);
|
||||
Message.success("表单配置已保存(不进版本快照,无需升版)");
|
||||
visible.value = false;
|
||||
} catch (e: any) {
|
||||
Message.error(e?.message || "保存失败");
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function toggleLifecycle() {
|
||||
const f = props.form;
|
||||
if (!f) return;
|
||||
try {
|
||||
const res =
|
||||
f.status === FORM_STATUS.PUBLISHED
|
||||
? await formApi.disable(f.formUuid).send()
|
||||
: await formApi.enable(f.formUuid).send();
|
||||
emit("saved", res);
|
||||
Message.success(
|
||||
f.status === FORM_STATUS.PUBLISHED
|
||||
? "已停用:填报入口失效,数据仍可读可导"
|
||||
: "已恢复当前发布版本"
|
||||
);
|
||||
} catch (e: any) {
|
||||
Message.error(e?.message || "操作失败");
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({ open });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal v-model="visible" :title="`表单配置|${form?.name || ''}`" :width="640" :mask-closable="false">
|
||||
<div v-if="form" class="cfg">
|
||||
<Alert v-if="isPlatform" type="warning" show-icon class="mb10">
|
||||
平台级表单(sch_id=0):跨校资产,只有超管上下文可维护。
|
||||
</Alert>
|
||||
|
||||
<div class="cfg-row">
|
||||
<span class="cfg-label">生命周期</span>
|
||||
<span class="cfg-static">
|
||||
{{ statusText }}
|
||||
<Tag v-if="form.draftVersion" color="warning" size="small">
|
||||
待发布草稿 v{{ form.draftVersion }}
|
||||
</Tag>
|
||||
</span>
|
||||
<span class="cfg-actions">
|
||||
<Button v-if="form.status === FORM_STATUS.PUBLISHED || form.status === FORM_STATUS.DISABLED"
|
||||
size="small" @click="toggleLifecycle">
|
||||
{{ form.status === FORM_STATUS.PUBLISHED ? "停用" : "恢复" }}
|
||||
</Button>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<div class="cfg-row">
|
||||
<span class="cfg-label">限次</span>
|
||||
<RadioGroup v-model="model.fillLimit">
|
||||
<Radio :label="0">不限次数</Radio>
|
||||
<Radio :label="1">每人限填一次</Radio>
|
||||
</RadioGroup>
|
||||
<span class="cfg-note">
|
||||
「每人」按提交主体判:管理员代填指定了被代填人时按那个人限,否则按提交人限;
|
||||
软删答卷会释放名额(契约 §5)
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="cfg-row">
|
||||
<span class="cfg-label">截止时间</span>
|
||||
<DatePicker v-model="model.deadline" type="date" format="yyyy-MM-dd" placeholder="不限"
|
||||
clearable style="width: 180px" />
|
||||
<span class="cfg-note">只选日期时后端补 23:59:59;过期后填报页只读、提交接口拒绝</span>
|
||||
</div>
|
||||
|
||||
<div class="cfg-row">
|
||||
<span class="cfg-label">报名号</span>
|
||||
<i-switch v-model="model.answerNoEnabled" :true-value="1" :false-value="0" />
|
||||
<span class="cfg-note">
|
||||
开启后提交生成 <code>{code}-{YYYYMMDD}-{当日序号}</code>(要求 code ≤49 位)
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="cfg-row">
|
||||
<span class="cfg-label">分组标签</span>
|
||||
<Input v-model="model.groupTag" :maxlength="64" placeholder="可空,用于列表筛选归类" style="width: 220px" />
|
||||
</div>
|
||||
|
||||
<Divider />
|
||||
|
||||
<div class="cfg-status">
|
||||
<div class="cfg-row">
|
||||
<span class="cfg-label">状态定义</span>
|
||||
<span class="cfg-note">
|
||||
状态是标记不是流转(无流转规则、无会签);默认「待处理/已处理」,
|
||||
<Button size="small" type="text" @click="statusDefs = []">回默认</Button>
|
||||
</span>
|
||||
</div>
|
||||
<div v-for="(d, i) in statusDefs" :key="d.code" class="status-row"
|
||||
:class="{ dropped: d.is_deleted }">
|
||||
<code class="status-code">{{ d.code }}</code>
|
||||
<Input v-model="d.label" size="small" :maxlength="128" style="width: 160px" />
|
||||
<span class="status-order">
|
||||
<Button size="small" type="text" icon="ios-arrow-up" :disabled="i === 0"
|
||||
@click="moveStatus(i, -1)" />
|
||||
<Button size="small" type="text" icon="ios-arrow-down"
|
||||
:disabled="i === statusDefs.length - 1" @click="moveStatus(i, 1)" />
|
||||
</span>
|
||||
<Checkbox v-model="d.is_deleted">废弃</Checkbox>
|
||||
</div>
|
||||
<div class="status-row">
|
||||
<Input v-model="newStatus.code" size="small" placeholder="code" style="width: 120px"
|
||||
:maxlength="64" />
|
||||
<Input v-model="newStatus.label" size="small" placeholder="名称" style="width: 160px"
|
||||
:maxlength="128" />
|
||||
<Button size="small" icon="md-add" @click="addStatus">新增状态</Button>
|
||||
</div>
|
||||
<div class="cfg-note">
|
||||
已废弃的状态历史答卷仍显示其名(V13),只是新数据不再给;code 一经使用不改,
|
||||
改 code 等于改写历史答卷的状态名。名称长度 ≤{{ MAX_NAME_LEN }} 与后端同口径。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<Button @click="visible = false">取消</Button>
|
||||
<Button type="primary" :loading="saving" @click="submit">保存配置</Button>
|
||||
</template>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.cfg-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 14px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.cfg-label {
|
||||
width: 78px;
|
||||
color: #515a6e;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.cfg-static {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.cfg-actions {
|
||||
margin-left: auto;
|
||||
}
|
||||
.cfg-note {
|
||||
color: #808695;
|
||||
font-size: 12px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
.cfg-status {
|
||||
margin-top: 4px;
|
||||
}
|
||||
.status-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.status-row.dropped .status-code,
|
||||
.status-row.dropped input {
|
||||
color: #c5c8ce;
|
||||
}
|
||||
.status-code {
|
||||
font-family: Menlo, Consolas, monospace;
|
||||
font-size: 12px;
|
||||
background: #f3f4f6;
|
||||
border-radius: 3px;
|
||||
padding: 2px 6px;
|
||||
min-width: 90px;
|
||||
text-align: center;
|
||||
}
|
||||
.status-order {
|
||||
display: flex;
|
||||
}
|
||||
.mb10 {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user