From 90041ad42437af582916364a596f855a016361fd Mon Sep 17 00:00:00 2001 From: jhy <184825448@qq.com> Date: Fri, 16 Oct 2026 19:45:51 +0800 Subject: [PATCH] feat: show navigation by user role --- frontend/src/App.vue | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 07b815d..0962304 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -3,24 +3,28 @@ import { computed, onMounted, ref, watch } from 'vue' import { Activity, BookOpen, Bot, CheckCircle2, ChevronRight, Database, FileText, Globe2, Link2, MessageSquareText, Plus, Search, Send, ShieldCheck, Sparkles, AlertCircle } from 'lucide-vue-next' import { createKnowledge, createSource, getHealth, getKnowledge, getSources, type SourceItem } from './api' import { retrieveHybrid } from './services/hybrid' +import { getCurrentUser } from './services/auth' +import { useRouter } from 'vue-router' +import type { SessionUser } from './types/auth' type Page='chat'|'knowledge'|'sources'|'evaluation' -const page=ref('chat'), apiStatus=ref('检测中'), question=ref(''), busy=ref(false), search=ref(''), showForm=ref(false), error=ref(''), notice=ref('') +const router=useRouter(),currentUser=ref(),page=ref('chat'), apiStatus=ref('检测中'), question=ref(''), busy=ref(false), search=ref(''), showForm=ref(false), error=ref(''), notice=ref('') const messages=ref([{role:'assistant',content:'你好!我是边疆红驿文旅问答原型。当前后端仅为基础接口,尚未接入知识检索和大语言模型。'}] as {role:string;content:string;sources?:{title:string;source_url?:string|null}[]}[]) const items=ref([]), form=ref({title:'',category:'旅游资源',content:'',source_url:''}) const sources=ref([]), sourceSearch=ref(''), showSourceForm=ref(false), sourceForm=ref({name:'',source_url:'',notes:''}) -const nav=[{id:'chat',label:'智能问答',icon:MessageSquareText},{id:'knowledge',label:'知识库管理',icon:Database},{id:'sources',label:'来源与更新',icon:Globe2},{id:'evaluation',label:'实验评估',icon:Activity}] as const +const nav=[{id:'chat',label:'智能问答',icon:MessageSquareText},{id:'knowledge',label:'知识库管理',icon:Database,adminOnly:true},{id:'sources',label:'来源与更新',icon:Globe2,adminOnly:true},{id:'evaluation',label:'实验评估',icon:Activity,adminOnly:true}] as const +const visibleNav=computed(()=>nav.filter(item=>!('adminOnly' in item)||!item.adminOnly||currentUser.value?.role==='admin')) const filtered=computed(()=>items.value.filter(x=>`${x.title} ${x.category} ${x.content}`.toLowerCase().includes(search.value.toLowerCase()))) async function refresh(){try{items.value=await getKnowledge();error.value=''}catch{error.value='无法读取后端知识库,请确认 API 和 PostgreSQL 已启动。'}} async function refreshSources(){try{sources.value=(await getSources(sourceSearch.value)).items;error.value=''}catch{error.value='无法读取来源列表,请确认 API 和 PostgreSQL 已启动。'}} async function saveSource(){error.value='';notice.value='';if(!sourceForm.value.name.trim()){error.value='请填写来源名称。';return}if(!validUrl(sourceForm.value.source_url)){error.value='来源网址必须是有效的 http(s) 地址。';return}try{await createSource({...sourceForm.value,name:sourceForm.value.name.trim(),...(sourceForm.value.source_url?{source_url:sourceForm.value.source_url}:{})});sourceForm.value={name:'',source_url:'',notes:''};showSourceForm.value=false;notice.value='来源已登记,可继续创建关联文档和知识条目。';await refreshSources()}catch(e){error.value=e instanceof Error?e.message:'保存失败'}} let sourceSearchTimer:number|undefined watch(sourceSearch,()=>{window.clearTimeout(sourceSearchTimer);sourceSearchTimer=window.setTimeout(refreshSources,260)}) -onMounted(async()=>{try{await getHealth();apiStatus.value='已连接'}catch{apiStatus.value='未连接'}await Promise.all([refresh(),refreshSources()])}) +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}} function validUrl(value:string){try{if(value)new URL(value);return true}catch{return false}} async function save(){error.value='';notice.value='';if(!form.value.title.trim()||!form.value.content.trim()){error.value='请填写知识标题和正文。';return}if(!validUrl(form.value.source_url)){error.value='来源网址必须是有效的 http(s) 地址。';return}try{await createKnowledge({...form.value,title:form.value.title.trim(),content:form.value.content.trim(),...(form.value.source_url?{source_url:form.value.source_url}:{})});form.value={title:'',category:'旅游资源',content:'',source_url:''};showForm.value=false;notice.value='已保存为待审核条目,尚未进入检索索引。';await refresh()}catch(e){error.value=e instanceof Error?e.message:'保存失败'}} -