gwy-exam/client/src/views/mock/MockFormView.vue

317 lines
9.0 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script setup lang="ts">
import { computed, onMounted, reactive, ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { mockApi, type MockExamAnalysis } from '../../api'
import { useRequest } from '../../composables/useRequest'
import { useAppStore } from '../../stores/app'
import AppCard from '../../components/base/AppCard.vue'
import AppButton from '../../components/base/AppButton.vue'
import AppIcon from '../../components/base/AppIcon.vue'
import AppPageHeader from '../../components/base/AppPageHeader.vue'
import AppLoading from '../../components/feedback/AppLoading.vue'
import AppError from '../../components/feedback/AppError.vue'
const route = useRoute()
const router = useRouter()
const app = useAppStore()
const MODULES = ['言语理解', '数量关系', '判断推理', '资料分析', '常识判断'] as const
type ModuleKey = (typeof MODULES)[number]
const editId = computed(() => (route.params.id as string | undefined) ?? '')
const isEdit = computed(() => Boolean(editId.value))
const title = computed(() => (isEdit.value ? '修改模考成绩' : '录入模考成绩'))
const analysis = useRequest<MockExamAnalysis>(() => mockApi.analysis(), isEdit.value)
const form = reactive<{
name: string
date: string
modules: Record<ModuleKey, number>
note: string
}>({
name: '',
date: '',
modules: {
言语理解: 0,
数量关系: 0,
判断推理: 0,
资料分析: 0,
常识判断: 0
},
note: ''
})
const saving = ref(false)
const submitted = ref(false)
onMounted(() => {
if (!isEdit.value) {
// 默认今天;不修改已填内容
if (!form.date) form.date = new Date().toISOString().slice(0, 10)
}
})
const sourceError = computed(() => analysis.error.value ?? '')
const total = computed(() => MODULES.reduce((sum, m) => sum + (Number(form.modules[m]) || 0), 0))
const totalOverflow = computed(() => total.value > 100)
const nameError = computed(() => submitted.value && !form.name.trim() ? '请填写模考名称' : '')
const dateError = computed(() => submitted.value && !form.date ? '请选择模考日期' : '')
function patchModule(m: ModuleKey, value: string) {
const n = Number(value)
form.modules[m] = Number.isFinite(n) ? Math.min(100, Math.max(0, n)) : 0
}
async function submit() {
submitted.value = true
if (nameError.value || dateError.value || totalOverflow.value) return
if (saving.value) return
saving.value = true
const body = {
name: form.name.trim(),
date: form.date,
total: total.value,
言语理解: form.modules['言语理解'],
数量关系: form.modules['数量关系'],
判断推理: form.modules['判断推理'],
资料分析: form.modules['资料分析'],
常识判断: form.modules['常识判断'],
note: form.note.trim() || undefined
}
try {
if (isEdit.value) {
await mockApi.update(editId.value, body)
app.toast('模考成绩已更新', 'success')
} else {
await mockApi.record(body)
app.toast('模考成绩已录入', 'success')
}
router.push('/mock')
} catch (err) {
app.toast(err instanceof Error ? err.message : '保存失败', 'error')
} finally {
saving.value = false
}
}
function back() {
router.push('/mock')
}
// 编辑模式:从分析数据中找到目标记录并回填
const loaded = computed(() => {
if (!isEdit.value || !analysis.data.value) return null
return analysis.data.value.records.find((r) => r.id === editId.value) ?? null
})
const formReady = computed(() => {
if (!isEdit.value) return true
return !analysis.loading.value
})
// 编辑模式:分析数据到达后回填表单
watch(
() => analysis.data.value,
() => {
const record = loaded.value
if (!isEdit.value || !record) return
form.name = record.name
form.date = record.date
for (const m of MODULES) form.modules[m] = record.modules[m]
form.note = record.note ?? ''
},
{ immediate: true }
)
</script>
<template>
<div class="mock-form">
<AppPageHeader :title="title" subtitle="仅支持行测五个模块分数,总分自动汇总">
<AppButton variant="ghost" @click="back">
<AppIcon name="chevron-left" :size="16" />返回模考分析
</AppButton>
</AppPageHeader>
<AppLoading v-if="isEdit && analysis.loading.value" fullscreen text="正在加载模考记录…" />
<AppError
v-else-if="sourceError"
fullscreen
title="加载失败"
:message="sourceError"
retry
@retry="analysis.refresh"
/>
<AppError
v-else-if="isEdit && !loaded"
fullscreen
title="未找到模考记录"
message="该记录可能已被删除"
/>
<template v-else-if="formReady">
<AppCard title="成绩信息" :padded="true" class="mock-form__card">
<div class="form-field">
<label class="form-field__label">模考名称</label>
<input
v-model="form.name"
class="form-control"
type="text"
placeholder="如:行测全真模考(四)"
/>
<p v-if="nameError" class="form-field__error">{{ nameError }}</p>
</div>
<div class="form-field">
<label class="form-field__label">模考日期</label>
<input v-model="form.date" class="form-control form-control--date" type="date" />
<p v-if="dateError" class="form-field__error">{{ dateError }}</p>
</div>
<div class="form-field">
<label class="form-field__label">五个模块分数每题满分合计 100 </label>
<div class="mock-form__modules">
<label v-for="m in MODULES" :key="m" class="mock-form__score">
<span class="mock-form__score-name">{{ m }}</span>
<input
:value="form.modules[m]"
class="form-control mock-form__score-input"
type="number"
min="0"
max="100"
inputmode="numeric"
@input="patchModule(m, ($event.target as HTMLInputElement).value)"
/>
</label>
</div>
<p v-if="totalOverflow" class="form-field__error">
模块分数合计 {{ total }} 不能超过 100
</p>
</div>
<div class="mock-form__total">
<span class="mock-form__total-label">总分自动汇总</span>
<strong class="mock-form__total-value">{{ total }}<small> </small></strong>
</div>
<div class="form-field">
<label class="form-field__label">备注可选</label>
<textarea
v-model="form.note"
class="form-control mock-form__note"
placeholder="如:资料分析时间紧张,常识失分较多"
/>
</div>
<AppButton variant="primary" size="lg" block :disabled="saving" @click="submit">
{{ saving ? '保存中' : isEdit ? '保存修改' : '保存成绩' }}
</AppButton>
</AppCard>
</template>
</div>
</template>
<style scoped>
.mock-form {
display: flex;
flex-direction: column;
gap: var(--space-5);
}
.mock-form__card {
max-width: 640px;
}
.mock-form__modules {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: var(--space-3);
}
.mock-form__score {
display: flex;
align-items: center;
gap: var(--space-3);
padding: var(--space-3);
border: 1px solid var(--border-subtle);
border-radius: var(--radius-md);
background: var(--bg-soft);
}
.mock-form__score-name {
flex: 1;
min-width: 0;
font-size: var(--fs-subtitle);
font-weight: var(--fw-card-title);
color: var(--text-primary);
}
.form-control.mock-form__score-input {
flex: 0 0 auto;
width: 88px;
text-align: right;
}
.mock-form__total {
display: flex;
align-items: baseline;
justify-content: space-between;
padding: var(--space-3) var(--space-4);
border-radius: var(--radius-md);
background: var(--primary-soft, var(--bg-soft));
}
.mock-form__total-label {
font-size: var(--fs-subtitle);
color: var(--text-secondary);
}
.mock-form__total-value {
font-family: var(--font-num);
font-size: 22px;
color: var(--primary);
}
.mock-form__total-value small {
font-size: var(--fs-label);
font-weight: normal;
color: var(--text-muted);
}
.mock-form__note {
min-height: 84px;
resize: vertical;
}
/* 表单通用字段样式(与题库录入保持一致) */
.form-field {
display: flex;
flex-direction: column;
gap: var(--space-2);
margin-bottom: var(--space-4);
}
.form-field__label {
font-size: var(--fs-label);
font-weight: var(--fw-label-bold);
color: var(--text-secondary);
}
.form-field__error {
margin: 0;
font-size: var(--fs-label);
color: var(--danger);
}
.form-control {
width: 100%;
box-sizing: border-box;
padding: var(--space-2) var(--space-3);
border: 1px solid var(--border-subtle);
border-radius: var(--radius-sm);
background: var(--bg-card);
color: var(--text-primary);
font-size: var(--fs-subtitle);
}
.form-control:focus {
outline: none;
border-color: var(--primary);
}
@media (max-width: 767px) {
.mock-form__modules {
grid-template-columns: 1fr;
}
}
</style>