317 lines
9.0 KiB
Vue
317 lines
9.0 KiB
Vue
<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>
|