feat: 添加分页功能和缓存管理,优化问答记录展示
This commit is contained in:
@@ -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"
|
||||
>
|
||||
<!-- 复制成功图标:未复制时向上移出,复制后滑入 -->
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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
|
||||
};
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user