fix(exam): replace unfounded 4-char title rule with explicit validation messages

- 考试名称仅要求非空,移除无依据的至少 4 字限制;保留及格分 1-100、
  至少一题、总分 100、题干/选项/答案完整、至少一名员工的发布校验
- 发布坞展示具体校验失败原因,不再静默禁用按钮
- CHANGELOG/README 同步 2026-07-25 组织姓名定向补齐与生产复核记录
This commit is contained in:
2026-07-25 18:15:32 +08:00
parent 30ef6a8f6b
commit bb8b2dde7e
4 changed files with 49 additions and 9 deletions
+24 -8
View File
@@ -122,6 +122,7 @@
</view>
<view class="publish-dock">
<text v-if="publishValidationMessage" class="publish-validation-message" role="alert">{{ publishValidationMessage }}</text>
<button class="dock-secondary" :disabled="saving || !valid" @click="save(false)">{{ saving ? '保存中' : '保存草稿' }}</button>
<button class="dock-primary" :disabled="saving || !valid || !selectedMemberIds.length" @click="save(true)">{{ saving ? '发布中' : `发布给 ${selectedMemberIds.length} 人` }}</button>
</view>
@@ -161,14 +162,20 @@ const dateAfter = (days: number) => { const date = new Date(); date.setHours(12,
const today = dateAfter(0);
const requestId = () => `exam-publish:${examId.value || 'new'}:${Date.now()}:${Math.random().toString(36).slice(2, 8)}`;
const totalScore = computed(() => questions.value.reduce((sum, item) => sum + Number(item.score || 0), 0));
const valid = computed(() => title.value.trim().length >= 4
&& passingScore.value > 0 && passingScore.value <= 100
&& totalScore.value === 100
&& questions.value.length > 0
&& questions.value.every((item, index) => item.stem.trim()
&& Number(item.score) > 0
&& singleAnswerModels.value[index]?.trim()
&& (item.format === 'short' || item.options.filter((option) => option.trim()).length >= 2)));
const validationMessage = computed(() => {
if (!title.value.trim()) return '请填写考试名称';
if (passingScore.value < 1 || passingScore.value > 100) return '及格分必须在 1–100 分之间';
if (!questions.value.length) return '请至少添加一道考试题';
if (totalScore.value !== 100) return '试卷题目分值合计必须为 100 分';
const incompleteIndex = questions.value.findIndex((item, index) => !item.stem.trim()
|| Number(item.score) <= 0
|| !singleAnswerModels.value[index]?.trim()
|| (item.format !== 'short' && item.options.filter((option) => option.trim()).length < 2));
return incompleteIndex < 0 ? '' : `请补全第 ${incompleteIndex + 1} 题的题干、分值、选项和答案`;
});
const valid = computed(() => !validationMessage.value);
const publishValidationMessage = computed(() => validationMessage.value
|| (!selectedMemberIds.value.length ? '发布前请至少选择 1 名员工' : ''));
const goBack = () => uni.navigateBack({ delta: 1, fail: () => uni.redirectTo({ url: '/pages/supervisor/exams/index' }) });
const selectPosition = (event: { detail: { value: string } }) => { positionIndex.value = Number(event.detail.value || 0); };
const selectDueDate = (event: { detail: { value: string } }) => { dueDate.value = event.detail.value; };
@@ -710,6 +717,15 @@ onLoad((options) => { examId.value = Number(options?.id || 0); void load(); });
background: rgba(255, 255, 255, 0.97);
}
.publish-validation-message {
grid-column: 1 / -1;
color: #a86100;
font-size: 12px;
font-weight: 700;
line-height: 1.4;
text-align: center;
}
.dock-primary,
.dock-secondary {
display: flex;
@@ -55,6 +55,9 @@ test('员工考试与主管组卷路由、题目播报、真实人员多选和
assert.match(edit, /selectedMemberIds\.value\.length < 100/);
assert.match(edit, /members\.value\.slice\(0, 100\)/);
assert.match(edit, /requestId/);
assert.doesNotMatch(edit, /title\.value\.trim\(\)\.length >= 4/);
assert.match(edit, /请填写考试名称/);
assert.match(edit, /publish-validation-message/);
assert.match(service, /\/api\/aihr\/mobile\/exams\/supervisor/);
});