feat: 添加分页功能和缓存管理,优化问答记录展示

This commit is contained in:
2026-05-23 00:44:47 +08:00
parent c48d57df14
commit cc7125e681
16 changed files with 172 additions and 243 deletions
+1 -1
View File
@@ -58,7 +58,7 @@ defineExpose({ isCopied });
:variant="variant"
:size="size"
:disabled="disabled"
:class="cn('relative overflow-hidden pl-8', className)"
:class="cn('relative overflow-hidden pl-7', className)"
@click.prevent.stop="handleCopy"
>
<!-- 复制成功图标未复制时向上移出复制后滑入 -->
+35 -3
View File
@@ -14,8 +14,19 @@ import {
CardHeader,
CardTitle
} from "@/components/ui/card";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue
} from "@/components/ui/select";
import type { IQaRecord } from "@/interfaces";
import { QUESTION_TYPE_LABELS, useAnswerStore } from "@/stores/answer";
import {
DASHBOARD_PAGE_SIZE_OPTIONS,
QUESTION_TYPE_LABELS,
useAnswerStore
} from "@/stores/answer";
const answerStore = useAnswerStore();
const {
@@ -78,8 +89,29 @@ const displayOptions = (record: IQaRecord) => {
<template>
<Card>
<CardHeader>
<CardTitle>最近问答记录</CardTitle>
<CardDescription>当前进程最多保留最近 100 </CardDescription>
<div class="flex items-center justify-between gap-4">
<div>
<CardTitle>最近问答记录</CardTitle>
<CardDescription>分页展示按创建时间倒序</CardDescription>
</div>
<Select
:model-value="String(recordsPageSize)"
@update:model-value="(v) => answerStore.setRecordsPageSize(Number(v))"
>
<SelectTrigger class="w-28">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem
v-for="size in DASHBOARD_PAGE_SIZE_OPTIONS"
:key="size"
:value="String(size)"
>
{{ size }} 条/页
</SelectItem>
</SelectContent>
</Select>
</div>
</CardHeader>
<CardContent>
+5 -12
View File
@@ -3,10 +3,7 @@
import { RefreshCw, Trash2 } from "lucide-vue-next";
import { storeToRefs } from "pinia";
import {
Alert,
AlertDescription
} from "@/components/ui/alert";
import { Alert, AlertDescription } from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
import {
Card,
@@ -39,13 +36,7 @@ const overviewItems = computed(() => [
{
label: "模型",
value: stats.value?.model || health.value?.model || "-",
detail: stats.value?.cache_enabled ? "缓存已启用" : "缓存未启用",
loading: statsLoading.value && !stats.value
},
{
label: "缓存数量",
value: stats.value?.cache_size?.toString() ?? "0",
detail: "当前进程",
detail: "AI 模型",
loading: statsLoading.value && !stats.value
},
{
@@ -59,7 +50,9 @@ const overviewItems = computed(() => [
<template>
<section class="grid gap-4">
<div class="flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
<div
class="flex flex-col gap-3 md:flex-row md:items-center md:justify-between"
>
<div>
<h1 class="text-xl font-semibold text-foreground">Dashboard</h1>
<p class="mt-1 text-sm text-muted-foreground">
+27 -3
View File
@@ -1,5 +1,8 @@
<!-- app/components/home/OcsConfigCard.vue - OCS AnswererWrapper 配置片段 -->
<script lang="ts" setup>
import { RefreshCw } from "lucide-vue-next";
import { Button } from "@/components/ui/button";
import {
Card,
CardAction,
@@ -12,6 +15,14 @@ import { useAuthStore } from "@/stores/auth";
const authStore = useAuthStore();
const origin = ref("http://localhost:3000");
const refreshing = ref(false);
const handleRefreshToken = async () => {
refreshing.value = true;
await authStore.refreshApiToken();
refreshing.value = false;
};
/** 浏览器端读取当前站点地址,避免文档里写死 localhost 或生产域名 */
onMounted(() => {
origin.value = window.location.origin;
@@ -22,7 +33,8 @@ const configText = computed(() => {
const data: Record<string, string> = {
title: "${title}",
type: "${type}",
options: "${options}"
options: "${options}",
token: ""
};
if (authStore.user?.apiToken) {
@@ -53,13 +65,25 @@ const configText = computed(() => {
<CardHeader>
<CardTitle>OCS 配置</CardTitle>
<CardAction>
<CopyCom :text="configText" :timeout="1200" />
<div class="flex items-center gap-2">
<Button
v-if="authStore.user?.apiToken && authStore.isOnline"
variant="outline"
size="sm"
:disabled="refreshing"
@click="handleRefreshToken"
>
<RefreshCw :class="['size-4', refreshing ? 'animate-spin' : '']" />
刷新 Token
</Button>
<CopyCom :text="configText" :timeout="1200" :disabled="refreshing" />
</div>
</CardAction>
</CardHeader>
<CardContent>
<pre
class="max-h-120 overflow-auto rounded-2xl bg-muted p-4 text-xs leading-5 text-muted-foreground"
class="h-89.5 overflow-x-auto overflow-y-hidden rounded-2xl bg-muted p-4 text-xs leading-5 text-muted-foreground"
><code>{{ configText }}</code>
</pre>
</CardContent>
+2 -8
View File
@@ -47,13 +47,11 @@ export interface IHealthResponse {
message: string;
/** 服务版本 */
version: string;
/** 当前是否启用内存缓存 */
cache_enabled: boolean;
/** 当前模型名,不包含 API Key 或 baseURL */
model: string;
}
/** 服务统计响应,来自当前 Nuxt 进程的内存状态 */
/** 服务统计响应 */
export interface IStatsResponse {
/** 服务版本 */
version: string;
@@ -61,11 +59,7 @@ export interface IStatsResponse {
uptime: number;
/** 当前模型名 */
model: string;
/** 当前是否启用内存缓存 */
cache_enabled: boolean;
/** 当前有效缓存数量 */
cache_size: number;
/** 当前进程内问答记录数量 */
/** 当前用户的问答记录总数 */
qa_records_count: number;
}
-52
View File
@@ -1,25 +1,13 @@
<!-- app/pages/dashboard.vue - 运行状态与最近问答记录页面 -->
<script lang="ts" setup>
import { RefreshCw } from "lucide-vue-next";
import QaRecordDetail from "@/components/dashboard/QaRecordDetail.vue";
import QaRecordsTable from "@/components/dashboard/QaRecordsTable.vue";
import StatsOverview from "@/components/dashboard/StatsOverview.vue";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle
} from "@/components/ui/card";
import { useAnswerStore } from "@/stores/answer";
import { useAuthStore } from "@/stores/auth";
definePageMeta({ middleware: "auth" });
const answerStore = useAnswerStore();
const authStore = useAuthStore();
useHead({
title: "Dashboard - OCS AI 答题服务"
@@ -36,50 +24,10 @@ if (import.meta.client) {
void answerStore.loadDashboard();
});
}
const refreshing = ref(false);
const handleRefreshToken = async () => {
refreshing.value = true;
await authStore.refreshApiToken();
refreshing.value = false;
};
</script>
<template>
<div class="grid gap-6">
<!-- OCS 油猴脚本 API token 配置卡片 -->
<Card v-if="authStore.user?.apiToken">
<CardHeader>
<CardTitle>OCS 脚本 Token</CardTitle>
<CardDescription>
OCS 油猴脚本的服务器配置中填入此
token脚本将以你的账号身份调用答题接口
</CardDescription>
</CardHeader>
<CardContent>
<div class="flex items-center gap-2">
<code
class="flex-1 overflow-x-auto bg-muted px-3 py-2 font-mono text-sm rounded-full"
>
{{ authStore.user.apiToken }}
</code>
<CopyCom :text="authStore.user.apiToken" :disabled="refreshing" />
<Button
variant="outline"
size="icon"
:disabled="refreshing"
title="刷新 Token"
class="flex items-center gap-1 w-fit px-2.5"
@click="handleRefreshToken"
>
<RefreshCw :class="['size-4', refreshing ? 'animate-spin' : '']" />
<span>刷新 Token</span>
</Button>
</div>
</CardContent>
</Card>
<StatsOverview />
<QaRecordsTable />
<QaRecordDetail />
+12 -1
View File
@@ -13,6 +13,9 @@ import { AnswerService } from "@/services";
/** Dashboard 最近记录表格每页数量,组件直接复用,避免多个地方写死 */
export const DASHBOARD_RECORD_PAGE_SIZE = 10;
/** 分页大小可选列表,与后端白名单保持一致 */
export const DASHBOARD_PAGE_SIZE_OPTIONS = [10, 50, 100] as const;
/** 题型下拉选项,value 保持 OCS / 旧 Python 服务使用的英文值 */
export const QUESTION_TYPE_OPTIONS: Array<{
label: string;
@@ -104,6 +107,13 @@ export const useAnswerStore = defineStore("answer", () => {
await getRecords(page);
};
/** 切换分页大小,重置到第 1 页后重新请求 */
const setRecordsPageSize = async (size: number) => {
if (recordsLoading.value || size === recordsPageSize.value) return;
recordsPageSize.value = size;
await getRecords(1);
};
/** 调用答题 API,返回 OCS 兼容结构但只在 store 中维护 UI 状态 */
const searchAnswer = async () => {
const title = question.value.trim();
@@ -245,6 +255,7 @@ export const useAnswerStore = defineStore("answer", () => {
loadDashboard,
clearCache,
selectRecord,
setRecordsPage
setRecordsPage,
setRecordsPageSize
};
});