feat: 完成整体内容开发
This commit is contained in:
@@ -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. 选项一 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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user