feat: 接口统一优化
This commit is contained in:
@@ -17,9 +17,9 @@ import { useAuthStore } from "@/stores/auth";
|
||||
const colorMode = useColorMode();
|
||||
const isDark = computed(() => colorMode.value === "dark");
|
||||
|
||||
function toggleColorMode() {
|
||||
const toggleColorMode = () => {
|
||||
colorMode.value = isDark.value ? "light" : "dark";
|
||||
}
|
||||
};
|
||||
|
||||
const authStore = useAuthStore();
|
||||
|
||||
|
||||
+16
-112
@@ -1,112 +1,16 @@
|
||||
// app/interfaces/answer.ts - 前端和本地 API 之间使用的响应类型
|
||||
|
||||
/** OCS 支持的题型;空字符串表示不指定题型,交给模型根据题目判断 */
|
||||
export type QuestionType =
|
||||
| ""
|
||||
| "single"
|
||||
| "multiple"
|
||||
| "judgement"
|
||||
| "completion";
|
||||
|
||||
/** 答题接口请求体,字段名保持旧 Python 服务和 OCS AnswererWrapper 兼容 */
|
||||
export interface ISearchRequest {
|
||||
/** 题目正文,必填 */
|
||||
title: string;
|
||||
/** 题型,可能为空字符串 */
|
||||
type: QuestionType | string;
|
||||
/** 选项文本,多行或 JSON 字符串均按旧逻辑透传给服务端解析 */
|
||||
options: string;
|
||||
}
|
||||
|
||||
/** 答题成功时返回给 OCS 和前端的结构 */
|
||||
export interface ISearchSuccessResponse {
|
||||
/** OCS 约定:1 表示找到答案 */
|
||||
code: 1;
|
||||
/** 原题目正文 */
|
||||
question: string;
|
||||
/** 最终答案;多选题继续使用 `#` 分隔 */
|
||||
answer: string;
|
||||
}
|
||||
|
||||
/** 答题失败时返回给 OCS 和前端的结构 */
|
||||
export interface ISearchErrorResponse {
|
||||
/** OCS 约定:0 表示失败 */
|
||||
code: 0;
|
||||
/** 本地安全错误文案,不包含上游细节 */
|
||||
msg: string;
|
||||
}
|
||||
|
||||
/** 答题接口联合响应 */
|
||||
export type ISearchResponse = ISearchSuccessResponse | ISearchErrorResponse;
|
||||
|
||||
/** 健康检查响应,只暴露非敏感运行信息 */
|
||||
export interface IHealthResponse {
|
||||
/** 服务状态,正常时为 ok */
|
||||
status: "ok";
|
||||
/** 本地状态文案 */
|
||||
message: string;
|
||||
/** 服务版本 */
|
||||
version: string;
|
||||
/** 当前模型名,不包含 API Key 或 baseURL */
|
||||
model: string;
|
||||
}
|
||||
|
||||
/** 服务统计响应 */
|
||||
export interface IStatsResponse {
|
||||
/** 服务版本 */
|
||||
version: string;
|
||||
/** 进程运行秒数 */
|
||||
uptime: number;
|
||||
/** 当前模型名 */
|
||||
model: string;
|
||||
/** 当前用户的问答记录总数 */
|
||||
qa_records_count: number;
|
||||
}
|
||||
|
||||
/** 最近问答记录,字段来自 server/utils/runtimeState.ts */
|
||||
export interface IQaRecord {
|
||||
/** 本地可读时间 */
|
||||
time: string;
|
||||
/** ISO 时间,方便排序和调试 */
|
||||
timestamp: string;
|
||||
/** 题目正文 */
|
||||
question: string;
|
||||
/** 题型 */
|
||||
type: string;
|
||||
/** 选项文本 */
|
||||
options: string;
|
||||
/** 最终返回给 OCS 的答案 */
|
||||
answer: string;
|
||||
}
|
||||
|
||||
/** 最近问答记录分页请求 */
|
||||
export interface IRecordsRequest {
|
||||
/** 当前页码,从 1 开始 */
|
||||
page: number;
|
||||
/** 每页数量 */
|
||||
size: number;
|
||||
}
|
||||
|
||||
/** 最近问答记录接口响应 */
|
||||
export interface IRecordsResponse {
|
||||
/** 管理接口是否成功 */
|
||||
success: boolean;
|
||||
/** 失败时的本地安全文案 */
|
||||
message?: string;
|
||||
/** 当前页问答记录,最新在前 */
|
||||
records: IQaRecord[];
|
||||
/** 当前页码,从 1 开始 */
|
||||
page: number;
|
||||
/** 每页数量 */
|
||||
size: number;
|
||||
/** 当前进程内问答记录总数 */
|
||||
total: number;
|
||||
}
|
||||
|
||||
/** 清空缓存接口响应 */
|
||||
export interface IClearCacheResponse {
|
||||
/** 是否清理成功;缓存关闭时为 false */
|
||||
success: boolean;
|
||||
/** 本地状态文案 */
|
||||
message: string;
|
||||
}
|
||||
// app/interfaces/answer.ts - 从 shared/types/answer.ts 重新导出,前端直接使用 @/interfaces 路径无需改动
|
||||
export type {
|
||||
IApiResponse,
|
||||
IClearCacheResponse,
|
||||
IHealthResponse,
|
||||
IQaRecord,
|
||||
IRecordsData,
|
||||
IRecordsRequest,
|
||||
IRecordsResponse,
|
||||
ISearchErrorResponse,
|
||||
ISearchRequest,
|
||||
ISearchResponse,
|
||||
ISearchSuccessResponse,
|
||||
IStatsResponse,
|
||||
QuestionType
|
||||
} from "~~/shared/types/answer";
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// app/services/answer_service.ts - OCS AI 答题服务相关 API
|
||||
import type {
|
||||
IApiResponse,
|
||||
IClearCacheResponse,
|
||||
IHealthResponse,
|
||||
IRecordsRequest,
|
||||
@@ -27,14 +28,14 @@ export class AnswerService {
|
||||
|
||||
/** 读取健康状态;无需 token */
|
||||
public static getHealth() {
|
||||
return BaseClientService.get<IHealthResponse>(
|
||||
return BaseClientService.get<IApiResponse<IHealthResponse>>(
|
||||
`${AnswerService.basePath}/health`
|
||||
);
|
||||
}
|
||||
|
||||
/** 读取运行统计(需登录) */
|
||||
public static getStats() {
|
||||
return BaseClientService.get<IStatsResponse>(
|
||||
return BaseClientService.get<IApiResponse<IStatsResponse>>(
|
||||
`${AnswerService.basePath}/stats`
|
||||
);
|
||||
}
|
||||
|
||||
+26
-39
@@ -9,6 +9,7 @@ import type {
|
||||
QuestionType
|
||||
} from "@/interfaces";
|
||||
import { AnswerService } from "@/services";
|
||||
import { formatUptime, getClientErrorMessage } from "~/utils";
|
||||
|
||||
/** Dashboard 最近记录表格每页数量,组件直接复用,避免多个地方写死 */
|
||||
export const DASHBOARD_RECORD_PAGE_SIZE = 10;
|
||||
@@ -37,55 +38,44 @@ export const QUESTION_TYPE_LABELS: Record<string, string> = {
|
||||
completion: "填空题"
|
||||
};
|
||||
|
||||
/** 从 `$fetch` 错误里提取安全文案;服务端已经保证 message/msg 不含敏感上游细节 */
|
||||
const getClientErrorMessage = (error: unknown, fallback: string) => {
|
||||
if (typeof error !== "object" || error === null) return fallback;
|
||||
|
||||
const data = "data" in error ? (error as { data?: unknown }).data : undefined;
|
||||
if (typeof data === "object" && data !== null) {
|
||||
const message = (data as { message?: unknown; msg?: unknown }).message;
|
||||
const msg = (data as { message?: unknown; msg?: unknown }).msg;
|
||||
|
||||
if (typeof message === "string" && message) return message;
|
||||
if (typeof msg === "string" && msg) return msg;
|
||||
}
|
||||
|
||||
return fallback;
|
||||
};
|
||||
|
||||
/** 把运行秒数格式化为 dashboard 更容易扫读的时长 */
|
||||
const formatUptime = (seconds: number) => {
|
||||
const safeSeconds = Math.max(0, Math.floor(seconds));
|
||||
const days = Math.floor(safeSeconds / 86_400);
|
||||
const hours = Math.floor((safeSeconds % 86_400) / 3_600);
|
||||
const minutes = Math.floor((safeSeconds % 3_600) / 60);
|
||||
|
||||
if (days > 0) return `${days} 天 ${hours} 小时`;
|
||||
if (hours > 0) return `${hours} 小时 ${minutes} 分钟`;
|
||||
return `${Math.max(1, minutes)} 分钟`;
|
||||
};
|
||||
|
||||
/** OCS AI 答题服务前端状态管理 */
|
||||
export const useAnswerStore = defineStore("answer", () => {
|
||||
// ---- 答题表单状态 ----
|
||||
/** 当前问题内容 */
|
||||
const question = ref("");
|
||||
/** 当前问题类型 */
|
||||
const questionType = ref<QuestionType>("");
|
||||
/** 当前问题选项 */
|
||||
const options = ref("");
|
||||
/** 答题请求加载状态 */
|
||||
const searchLoading = ref(false);
|
||||
/** 答题结果 */
|
||||
const answerResult = ref<ISearchSuccessResponse>();
|
||||
/** 答题错误文案 */
|
||||
const searchErrorMessage = ref("");
|
||||
|
||||
// ---- Dashboard 状态 ----
|
||||
/** 健康检查信息 */
|
||||
const health = ref<IHealthResponse>();
|
||||
/** 运行统计信息 */
|
||||
const stats = ref<IStatsResponse>();
|
||||
/** 最近问答记录 */
|
||||
const records = ref<IQaRecord[]>([]);
|
||||
/** Dashboard 错误文案 */
|
||||
const dashboardErrorMessage = ref("");
|
||||
/** 统计信息加载状态 */
|
||||
const statsLoading = ref(false);
|
||||
/** 最近问答记录加载状态 */
|
||||
const recordsLoading = ref(false);
|
||||
/** 缓存清理状态 */
|
||||
const cacheClearing = ref(false);
|
||||
/** 最近问答记录当前页码 */
|
||||
const recordsPage = ref(1);
|
||||
/** 最近问答记录每页数量 */
|
||||
const recordsPageSize = ref(DASHBOARD_RECORD_PAGE_SIZE);
|
||||
/** 最近问答记录总数 */
|
||||
const recordCount = ref(0);
|
||||
/** 当前选中的问答记录 */
|
||||
const selectedRecord = ref<IQaRecord>();
|
||||
|
||||
/** Dashboard 表格当前页数据,页码切换会重新请求服务端分页接口 */
|
||||
@@ -154,7 +144,7 @@ export const useAnswerStore = defineStore("answer", () => {
|
||||
const getHealth = async () => {
|
||||
try {
|
||||
const res = await AnswerService.getHealth();
|
||||
health.value = res.data;
|
||||
health.value = res.data.data;
|
||||
} catch (error) {
|
||||
dashboardErrorMessage.value = getClientErrorMessage(
|
||||
error,
|
||||
@@ -168,7 +158,7 @@ export const useAnswerStore = defineStore("answer", () => {
|
||||
statsLoading.value = true;
|
||||
try {
|
||||
const res = await AnswerService.getStats();
|
||||
stats.value = res.data;
|
||||
stats.value = res.data.data;
|
||||
} catch (error) {
|
||||
dashboardErrorMessage.value = getClientErrorMessage(
|
||||
error,
|
||||
@@ -187,10 +177,10 @@ export const useAnswerStore = defineStore("answer", () => {
|
||||
page,
|
||||
size: recordsPageSize.value
|
||||
});
|
||||
records.value = res.data.records;
|
||||
recordsPage.value = res.data.page;
|
||||
recordsPageSize.value = res.data.size;
|
||||
recordCount.value = res.data.total;
|
||||
records.value = res.data.data.records;
|
||||
recordsPage.value = res.data.data.page;
|
||||
recordsPageSize.value = res.data.data.size;
|
||||
recordCount.value = res.data.data.total;
|
||||
} catch (error) {
|
||||
dashboardErrorMessage.value = getClientErrorMessage(
|
||||
error,
|
||||
@@ -207,16 +197,13 @@ export const useAnswerStore = defineStore("answer", () => {
|
||||
await Promise.allSettled([getHealth(), getStats(), getRecords()]);
|
||||
};
|
||||
|
||||
/** 清空缓存和问答记录后刷新统计与表格 */
|
||||
/** 清空缓存后刷新统计与表格;$fetch 非 2xx 时会抛出,错误由 catch 处理 */
|
||||
const clearCache = async () => {
|
||||
cacheClearing.value = true;
|
||||
dashboardErrorMessage.value = "";
|
||||
|
||||
try {
|
||||
const res = await AnswerService.clearCache();
|
||||
if (!res.data.success) {
|
||||
dashboardErrorMessage.value = res.data.message;
|
||||
}
|
||||
await AnswerService.clearCache();
|
||||
await Promise.allSettled([getStats(), getRecords(1)]);
|
||||
} catch (error) {
|
||||
dashboardErrorMessage.value = getClientErrorMessage(
|
||||
|
||||
+7
-6
@@ -132,12 +132,13 @@ export const useAuthStore = defineStore("auth", () => {
|
||||
loading.value = true;
|
||||
error.value = "";
|
||||
try {
|
||||
const res = await $fetch<{ success: boolean; msg: string; apiToken?: string }>(
|
||||
"/api/user/refresh-token",
|
||||
{ method: "POST" }
|
||||
);
|
||||
if (res.success && res.apiToken && user.value) {
|
||||
user.value = { ...user.value, apiToken: res.apiToken };
|
||||
const res = await $fetch<{
|
||||
code: number;
|
||||
msg: string;
|
||||
data: { apiToken: string } | null;
|
||||
}>("/api/user/refresh-token", { method: "POST" });
|
||||
if (res.code === 0 && res.data?.apiToken && user.value) {
|
||||
user.value = { ...user.value, apiToken: res.data.apiToken };
|
||||
return true;
|
||||
}
|
||||
error.value = res.msg || "刷新失败";
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
// app/utils/fetch.ts - $fetch 请求工具
|
||||
|
||||
/** 从 `$fetch` 错误里提取安全文案;服务端已经保证 message/msg 不含敏感上游细节 */
|
||||
export const getClientErrorMessage = (error: unknown, fallback: string) => {
|
||||
if (typeof error !== "object" || error === null) return fallback;
|
||||
|
||||
const data = "data" in error ? (error as { data?: unknown }).data : undefined;
|
||||
if (typeof data === "object" && data !== null) {
|
||||
const message = (data as { message?: unknown; msg?: unknown }).message;
|
||||
const msg = (data as { message?: unknown; msg?: unknown }).msg;
|
||||
|
||||
if (typeof message === "string" && message) return message;
|
||||
if (typeof msg === "string" && msg) return msg;
|
||||
}
|
||||
|
||||
return fallback;
|
||||
};
|
||||
+17
-6
@@ -6,19 +6,18 @@
|
||||
* 输入:`A.\n错\nB.\n对`
|
||||
* 输出:`A. 错\nB. 对`
|
||||
*/
|
||||
export function formatOptions(options: string): string {
|
||||
export const formatOptions = (options: string): string =>
|
||||
// 匹配类似 "A." 开头的选项标记,将其后紧跟的换行内容合并到同一行
|
||||
return options.replace(/^([A-Za-z]\.)\s*\n\s*/gm, "$1 ");
|
||||
}
|
||||
options.replace(/^([A-Za-z]\.)\s*\n\s*/gm, "$1 ");
|
||||
|
||||
/**
|
||||
* 将选项字符串拆分为逐行结构,标记哪些行是正确答案。
|
||||
* answer 按 # 分隔(多选),与选项正文做等值匹配(大小写不敏感)。
|
||||
*/
|
||||
export function getOptionLines(
|
||||
export const getOptionLines = (
|
||||
options: string,
|
||||
answer: string
|
||||
): { text: string; isCorrect: boolean }[] {
|
||||
): { text: string; isCorrect: boolean }[] => {
|
||||
const segments = answer
|
||||
.split("#")
|
||||
.map((s) => s.trim().toUpperCase())
|
||||
@@ -33,4 +32,16 @@ export function getOptionLines(
|
||||
const isCorrect = content !== "" && segments.includes(content);
|
||||
return { text: line, isCorrect };
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/** 把运行秒数格式化为 dashboard 更容易扫读的时长 */
|
||||
export const formatUptime = (seconds: number) => {
|
||||
const safeSeconds = Math.max(0, Math.floor(seconds));
|
||||
const days = Math.floor(safeSeconds / 86_400);
|
||||
const hours = Math.floor((safeSeconds % 86_400) / 3_600);
|
||||
const minutes = Math.floor((safeSeconds % 3_600) / 60);
|
||||
|
||||
if (days > 0) return `${days} 天 ${hours} 小时`;
|
||||
if (hours > 0) return `${hours} 小时 ${minutes} 分钟`;
|
||||
return `${Math.max(1, minutes)} 分钟`;
|
||||
};
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from "./clipboard";
|
||||
export * from "./fetch";
|
||||
export * from "./format";
|
||||
export * from "./sortableTable";
|
||||
|
||||
Reference in New Issue
Block a user