From af52ee5f58fc5de1c70dd6d419958cffb81f3c91 Mon Sep 17 00:00:00 2001 From: jhy <184825448@qq.com> Date: Sat, 17 Oct 2026 10:35:18 +0800 Subject: [PATCH] feat: add local history and error pages --- frontend/src/App.vue | 3 ++- frontend/src/router.ts | 5 ++++- frontend/src/services/localHistory.ts | 6 ++++++ frontend/src/views/ForbiddenView.vue | 1 + frontend/src/views/HistoryView.vue | 10 ++++++++++ frontend/src/views/NotFoundView.vue | 1 + 6 files changed, 24 insertions(+), 2 deletions(-) create mode 100644 frontend/src/services/localHistory.ts create mode 100644 frontend/src/views/ForbiddenView.vue create mode 100644 frontend/src/views/HistoryView.vue create mode 100644 frontend/src/views/NotFoundView.vue diff --git a/frontend/src/App.vue b/frontend/src/App.vue index ace22a5..fd44697 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -4,6 +4,7 @@ import { Activity, BookOpen, Bot, CheckCircle2, ChevronRight, Database, FileText import { createKnowledge, createSource, getHealth, getKnowledge, getSources, type SourceItem } from './api' import { retrieveHybrid } from './services/hybrid' import { getCurrentUser } from './services/auth' +import { saveLocalHistory } from './services/localHistory' import { useRouter } from 'vue-router' import type { SessionUser } from './types/auth' type Page='chat'|'knowledge'|'sources'|'evaluation' @@ -20,7 +21,7 @@ async function saveSource(){error.value='';notice.value='';if(!sourceForm.value. let sourceSearchTimer:number|undefined watch(sourceSearch,()=>{window.clearTimeout(sourceSearchTimer);sourceSearchTimer=window.setTimeout(refreshSources,260)}) onMounted(async()=>{try{currentUser.value=await getCurrentUser()}catch{router.replace('/login');return}try{await getHealth();apiStatus.value='已连接'}catch{apiStatus.value='未连接'}if(currentUser.value.role==='admin')await Promise.all([refresh(),refreshSources()])}) -async function send(){const q=question.value.trim();if(!q||busy.value)return;messages.value.push({role:'user',content:q});question.value='';busy.value=true;try{const r=await retrieveHybrid({query:q,top_k:5,candidate_k:50});const content=r.status==='no_evidence'?'未检索到可用的已核验知识依据。请调整问题或补充审核后的资料。':`已通过 RRF 混合检索命中 ${r.total_hits} 条依据(关键词 ${r.lexical_total_hits},向量 ${r.vector_total_hits},${r.elapsed_ms.toFixed(1)} ms)。当前仅展示检索证据,尚未接入大语言模型生成回答。`;messages.value.push({role:'assistant',content,sources:r.results.map(hit=>({title:`${hit.title}:${hit.snippet}`,source_url:hit.source_url}))})}catch(e){messages.value.push({role:'assistant',content:e instanceof Error?e.message:'暂时无法连接混合检索服务。'})}finally{busy.value=false}} +async function send(){const q=question.value.trim();if(!q||busy.value)return;messages.value.push({role:'user',content:q});question.value='';busy.value=true;try{const r=await retrieveHybrid({query:q,top_k:5,candidate_k:50});const content=r.status==='no_evidence'?'未检索到可用的已核验知识依据。请调整问题或补充审核后的资料。':`已通过 RRF 混合检索命中 ${r.total_hits} 条依据(关键词 ${r.lexical_total_hits},向量 ${r.vector_total_hits},${r.elapsed_ms.toFixed(1)} ms)。当前仅展示检索证据,尚未接入大语言模型生成回答。`;messages.value.push({role:'assistant',content,sources:r.results.map(hit=>({title:`${hit.title}:${hit.snippet}`,source_url:hit.source_url}))});saveLocalHistory(q,content)}catch(e){const content=e instanceof Error?e.message:'暂时无法连接混合检索服务。';messages.value.push({role:'assistant',content});saveLocalHistory(q,content)}finally{busy.value=false}} function validUrl(value:string){try{if(value)new URL(value);return true}catch{return false}} async function save(){ error.value='';notice.value='' diff --git a/frontend/src/router.ts b/frontend/src/router.ts index 98c321c..58710ef 100644 --- a/frontend/src/router.ts +++ b/frontend/src/router.ts @@ -8,6 +8,8 @@ const router=createRouter({history:createWebHistory(),routes:[ {path:'/',component:App}, {path:'/login',component:()=>import('./views/LoginView.vue'),meta:{public:true}}, {path:'/register',component:()=>import('./views/RegisterView.vue'),meta:{public:true}}, + {path:'/403',component:()=>import('./views/ForbiddenView.vue'),meta:{public:true}}, + {path:'/history',component:()=>import('./views/HistoryView.vue')}, {path:'/profile',component:()=>import('./views/ProfileView.vue')}, {path:'/settings',component:()=>import('./views/AccountSettingsView.vue')}, {path:'/indexes',component:()=>import('./views/IndexManagementView.vue'),meta:{role:'admin'}}, @@ -17,6 +19,7 @@ const router=createRouter({history:createWebHistory(),routes:[ {path:'/sources',component:()=>import('./views/SourceManagementView.vue'),meta:{role:'admin'}}, {path:'/retrieval-exports',component:()=>import('./views/RetrievalExportView.vue'),meta:{role:'admin'}}, {path:'/admin',component:()=>import('./views/AdminView.vue'),meta:{role:'admin'}}, + {path:'/:pathMatch(.*)*',component:()=>import('./views/NotFoundView.vue'),meta:{public:true}}, ]}) -router.beforeEach(async to=>{if(to.meta.public)return true;try{role=(await getCurrentUser()).role}catch{return {path:'/login',query:{redirect:to.fullPath}}}if(to.meta.role&&role!==to.meta.role)return {path:'/profile',query:{forbidden:'1'}};return true}) +router.beforeEach(async to=>{if(to.meta.public)return true;try{role=(await getCurrentUser()).role}catch{return {path:'/login',query:{redirect:to.fullPath}}}if(to.meta.role&&role!==to.meta.role)return {path:'/403'};return true}) export default router diff --git a/frontend/src/services/localHistory.ts b/frontend/src/services/localHistory.ts new file mode 100644 index 0000000..d660b61 --- /dev/null +++ b/frontend/src/services/localHistory.ts @@ -0,0 +1,6 @@ +export type LocalHistoryItem={id:string;question:string;summary:string;created_at:string} +const key='nalati-local-history-v1' +export function readLocalHistory():LocalHistoryItem[]{try{const value=JSON.parse(sessionStorage.getItem(key)||'[]');return Array.isArray(value)?value:[]}catch{return []}} +export function saveLocalHistory(question:string,summary:string){const item:LocalHistoryItem={id:crypto.randomUUID(),question,summary,created_at:new Date().toISOString()};sessionStorage.setItem(key,JSON.stringify([item,...readLocalHistory()].slice(0,100)))} +export function removeLocalHistory(id:string){sessionStorage.setItem(key,JSON.stringify(readLocalHistory().filter(item=>item.id!==id)))} +export function clearLocalHistory(){sessionStorage.removeItem(key)} diff --git a/frontend/src/views/ForbiddenView.vue b/frontend/src/views/ForbiddenView.vue new file mode 100644 index 0000000..e7f0b35 --- /dev/null +++ b/frontend/src/views/ForbiddenView.vue @@ -0,0 +1 @@ +403 无权限当前账户没有访问此页面的权限。管理员能力仍必须由后端授权。返回首页 diff --git a/frontend/src/views/HistoryView.vue b/frontend/src/views/HistoryView.vue new file mode 100644 index 0000000..46df8f1 --- /dev/null +++ b/frontend/src/views/HistoryView.vue @@ -0,0 +1,10 @@ + +返回智能问答历史对话当前记录仅保存在本浏览器,尚未接入服务器持久化;清除浏览器数据后可能丢失。清除全部本地记录暂无本地记录。请先在智能问答页发起提问。问题时间操作{{item.question}}{{new Date(item.created_at).toLocaleString()}}查看删除{{selected.question}}{{selected.summary}} diff --git a/frontend/src/views/NotFoundView.vue b/frontend/src/views/NotFoundView.vue new file mode 100644 index 0000000..aefae3a --- /dev/null +++ b/frontend/src/views/NotFoundView.vue @@ -0,0 +1 @@ +404 页面不存在请检查地址,或返回智能问答首页。返回首页
当前账户没有访问此页面的权限。管理员能力仍必须由后端授权。
当前记录仅保存在本浏览器,尚未接入服务器持久化;清除浏览器数据后可能丢失。
暂无本地记录。请先在智能问答页发起提问。
{{selected.summary}}
请检查地址,或返回智能问答首页。