Files
OCS_service/app/components/home/AnswerForm.vue
T
2026-05-22 22:38:01 +08:00

182 lines
5.4 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.
<!-- app/components/home/AnswerForm.vue - 答题提交表单状态全部来自 Pinia store -->
<script lang="ts" setup>
import { toTypedSchema } from "@vee-validate/zod";
import { Search } from "lucide-vue-next";
import { storeToRefs } from "pinia";
import { useForm } from "vee-validate";
import * as z from "zod";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle
} from "@/components/ui/card";
import {
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage
} from "@/components/ui/form";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue
} from "@/components/ui/select";
import { Textarea } from "@/components/ui/textarea";
import type { QuestionType } from "@/interfaces";
import { QUESTION_TYPE_OPTIONS, useAnswerStore } from "@/stores/answer";
const answerStore = useAnswerStore();
const { question, questionType, options, searchLoading } =
storeToRefs(answerStore);
const authStore = useAuthStore();
/** session 尚未初始化完成(客户端 plugin 还在请求中) */
const sessionChecking = computed(() => !authStore.initialized);
const AUTO_TYPE_VALUE = "__auto__";
/** shadcn Select 不使用空字符串作为 item value,这里只在组件边界做一次映射 */
const selectedQuestionType = computed({
get: () => questionType.value || AUTO_TYPE_VALUE,
set: (value: string) => {
questionType.value =
value === AUTO_TYPE_VALUE ? "" : (value as QuestionType);
}
});
const formSchema = toTypedSchema(
z.object({
question: z
.string({ required_error: "题目不能为空" })
.min(1, "题目不能为空"),
options: z.string().default("")
})
);
const { handleSubmit, setFieldError } = useForm({
validationSchema: formSchema,
initialValues: { question: question.value, options: options.value }
});
let clearErrorTimer: ReturnType<typeof setTimeout> | null = null;
/** 表单提交先更新 store 再触发 action,组件不直接接触请求服务 */
const onSubmit = handleSubmit(
(values) => {
if (clearErrorTimer) {
clearTimeout(clearErrorTimer);
clearErrorTimer = null;
}
question.value = values.question;
options.value = values.options ?? "";
void answerStore.searchAnswer();
},
() => {
// 校验失败 5s 后自动清除提示
if (clearErrorTimer) clearTimeout(clearErrorTimer);
clearErrorTimer = setTimeout(() => {
setFieldError("question", undefined);
clearErrorTimer = null;
}, 5000);
}
);
onUnmounted(() => {
// 清除定时器
if (clearErrorTimer) clearTimeout(clearErrorTimer);
});
</script>
<template>
<Card>
<CardHeader>
<CardTitle class="text-xl">题目查询</CardTitle>
<CardDescription
>服务端流式请求模型前端返回普通 JSON 结果</CardDescription
>
</CardHeader>
<CardContent>
<form class="grid gap-4" @submit="onSubmit">
<FormField
v-slot="{ componentField }"
name="question"
:validate-on-blur="false"
:validate-on-change="false"
:validate-on-model-update="false"
>
<FormItem>
<FormLabel>题目</FormLabel>
<FormControl>
<Textarea
class="min-h-32 resize-y"
placeholder="输入题目内容"
v-bind="componentField"
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
<div
class="grid gap-4 md:grid-cols-[220px_minmax(0,1fr)] md:items-start"
>
<div class="grid gap-2">
<label class="text-sm font-medium leading-none">题型</label>
<Select v-model="selectedQuestionType">
<SelectTrigger class="w-full">
<SelectValue placeholder="自动判断" />
</SelectTrigger>
<SelectContent>
<SelectItem
v-for="item in QUESTION_TYPE_OPTIONS"
:key="item.value || AUTO_TYPE_VALUE"
:value="item.value || AUTO_TYPE_VALUE"
>
{{ item.label }}
</SelectItem>
</SelectContent>
</Select>
</div>
<FormField v-slot="{ componentField }" name="options">
<FormItem>
<FormLabel>选项</FormLabel>
<FormControl>
<Textarea
class="min-h-24 resize-y"
placeholder="A. 选项一&#10;B. 选项二"
v-bind="componentField"
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
</div>
<div class="flex items-center justify-end">
<!-- 已登录或 session 尚未确认乐观显示正常按钮SSR/客户端 DOM 一致避免水合不一致-->
<!-- 确认未登录后才切换为"去登录"此切换只发生在客户端 plugin 完成后 -->
<Button
v-if="authStore.isOnline || sessionChecking"
type="submit"
:disabled="searchLoading"
>
<Search class="size-4" />
<span>{{ searchLoading ? "查询中" : "获取答案" }}</span>
</Button>
<Button v-else as-child variant="outline">
<NuxtLink to="/login">去登录</NuxtLink>
</Button>
</div>
</form>
</CardContent>
</Card>
</template>