feat: 完成整体内容开发

This commit is contained in:
2026-05-22 16:27:35 +08:00
parent 5e05bf4f63
commit d3cb786edb
72 changed files with 2410 additions and 29 deletions
+108
View File
@@ -0,0 +1,108 @@
<!-- app/components/home/AnswerForm.vue - 答题提交表单状态全部来自 Pinia store -->
<script lang="ts" setup>
import { Search } from "lucide-vue-next";
import { storeToRefs } from "pinia";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle
} from "@/components/ui/card";
import { Label } from "@/components/ui/label";
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 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);
}
});
/** 表单提交只触发 store action,组件不直接接触请求服务 */
const onSubmit = () => {
void answerStore.searchAnswer();
};
</script>
<template>
<Card>
<CardHeader>
<CardTitle class="text-xl">题目查询</CardTitle>
<CardDescription
>服务端流式请求模型前端返回普通 JSON 结果</CardDescription
>
</CardHeader>
<CardContent>
<form class="grid gap-4" @submit.prevent="onSubmit">
<div class="grid gap-2">
<Label for="answer-question">题目</Label>
<Textarea
id="answer-question"
v-model="question"
class="min-h-32 resize-y"
placeholder="输入题目内容"
/>
</div>
<div
class="grid gap-4 md:grid-cols-[220px_minmax(0,1fr)] md:items-start"
>
<div class="grid gap-2">
<Label for="answer-type">题型</Label>
<Select v-model="selectedQuestionType">
<SelectTrigger id="answer-type" 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>
<div class="grid gap-2">
<Label for="answer-options">选项</Label>
<Textarea
id="answer-options"
v-model="options"
class="min-h-24 resize-y"
placeholder="A. 选项一&#10;B. 选项二"
/>
</div>
</div>
<div class="flex items-center justify-end">
<Button type="submit" :disabled="searchLoading">
<Search class="size-4" />
<span>{{ searchLoading ? "查询中" : "获取答案" }}</span>
</Button>
</div>
</form>
</CardContent>
</Card>
</template>
+80
View File
@@ -0,0 +1,80 @@
<!-- app/components/home/AnswerResult.vue - 答题结果展示只展示安全业务文案 -->
<script lang="ts" setup>
import { AlertCircle, CheckCircle2, Loader2 } from "lucide-vue-next";
import { storeToRefs } from "pinia";
import {
Alert,
AlertDescription,
AlertTitle
} from "@/components/ui/alert";
import { Badge } from "@/components/ui/badge";
import {
Card,
CardAction,
CardContent,
CardHeader,
CardTitle
} from "@/components/ui/card";
import { Skeleton } from "@/components/ui/skeleton";
import { useAnswerStore } from "@/stores/answer";
const answerStore = useAnswerStore();
const { answerResult, searchErrorMessage, searchLoading } = storeToRefs(answerStore);
</script>
<template>
<Card>
<CardHeader>
<CardTitle>查询结果</CardTitle>
<CardAction>
<Badge variant="secondary">OCS JSON</Badge>
</CardAction>
</CardHeader>
<CardContent>
<div v-if="searchLoading" class="grid min-h-32 gap-3">
<div class="flex items-center gap-2 text-sm text-muted-foreground">
<Loader2 class="size-4 animate-spin" />
<span>正在获取答案</span>
</div>
<Skeleton class="h-5 w-2/3" />
<Skeleton class="h-20 w-full" />
</div>
<Alert v-else-if="searchErrorMessage" variant="destructive" class="min-h-32">
<AlertCircle class="size-4" />
<AlertTitle>查询失败</AlertTitle>
<AlertDescription>{{ searchErrorMessage }}</AlertDescription>
</Alert>
<div v-else-if="answerResult" class="grid gap-4">
<div class="flex items-center gap-2 text-sm font-medium text-foreground">
<CheckCircle2 class="size-4" />
<span>已返回答案</span>
</div>
<div class="grid gap-2 rounded-2xl border bg-muted/40 p-4">
<span class="text-xs font-medium text-muted-foreground">题目</span>
<p class="whitespace-pre-wrap text-sm leading-6 text-foreground">
{{ answerResult.question }}
</p>
</div>
<div class="grid gap-2 rounded-2xl border bg-muted/40 p-4">
<span class="text-xs font-medium text-muted-foreground">答案</span>
<p class="whitespace-pre-wrap text-base font-semibold leading-7 text-foreground">
{{ answerResult.answer }}
</p>
</div>
</div>
<div
v-else
class="flex min-h-32 items-center justify-center rounded-2xl border border-dashed text-sm text-muted-foreground"
>
等待题目提交
</div>
</CardContent>
</Card>
</template>
+75
View File
@@ -0,0 +1,75 @@
<!-- app/components/home/OcsConfigCard.vue - OCS AnswererWrapper 配置片段 -->
<script lang="ts" setup>
import { Check, Copy } from "lucide-vue-next";
import { Button } from "@/components/ui/button";
import {
Card,
CardAction,
CardContent,
CardHeader,
CardTitle
} from "@/components/ui/card";
const origin = ref("http://localhost:3000");
const copied = ref(false);
/** 浏览器端读取当前站点地址,避免文档里写死 localhost 或生产域名 */
onMounted(() => {
origin.value = window.location.origin;
});
/** 与旧 Python 项目的 ocs_config_example.json 保持同样字段 */
const configText = computed(() => {
return JSON.stringify(
[
{
name: "AI智能题库",
homepage: origin.value,
url: `${origin.value}/api/search`,
method: "get",
contentType: "json",
data: {
title: "${title}",
type: "${type}",
options: "${options}"
},
handler:
"return (res)=> res.code === 1 ? [res.question, res.answer] : [res.msg, undefined]"
}
],
null,
2
);
});
/** 复制配置片段,失败时不打断主流程 */
const copyConfig = async () => {
await navigator.clipboard.writeText(configText.value).catch(() => undefined);
copied.value = true;
window.setTimeout(() => {
copied.value = false;
}, 1200);
};
</script>
<template>
<Card>
<CardHeader>
<CardTitle>OCS 配置</CardTitle>
<CardAction>
<Button type="button" variant="outline" size="sm" @click="copyConfig">
<Check v-if="copied" class="size-3.5" />
<Copy v-else class="size-3.5" />
<span>{{ copied ? "已复制" : "复制" }}</span>
</Button>
</CardAction>
</CardHeader>
<CardContent>
<pre
class="max-h-[30rem] overflow-auto rounded-2xl bg-muted p-4 text-xs leading-5 text-muted-foreground"
><code>{{ configText }}</code></pre>
</CardContent>
</Card>
</template>