feat: add document management workspace
This commit is contained in:
@@ -12,6 +12,9 @@ export type SourceItem={id:number;version:number;name:string;source_url:string|n
|
||||
export function getSources(q=''){return apiRequest<{items:SourceItem[];total:number}>(`/api/sources?q=${encodeURIComponent(q)}`)}
|
||||
export function createSource(payload:{name:string;source_url?:string;notes:string}){return apiRequest<SourceItem>('/api/sources',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(payload)},true)}
|
||||
export function reviewKnowledge(id:number,payload:{expected_version:number;verification_status:VerificationStatus;reviewer:string;verification_notes:string}){return apiRequest(`/api/knowledge/${id}/review`,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(payload)},true)}
|
||||
export type DocumentItem={id:number;version:number;source_id:number;title:string;category:string;content:string;collected_at:string;notes:string;created_at:string;updated_at:string;is_deleted:boolean}
|
||||
export function getDocuments(q='',page=1,pageSize=20){return apiRequest<{items:DocumentItem[];total:number;page:number;page_size:number}>(`/api/documents?q=${encodeURIComponent(q)}&page=${page}&page_size=${pageSize}`)}
|
||||
export function createDocument(payload:{source_id:number;title:string;category:string;content:string;collected_at:string;notes?:string}){return apiRequest<DocumentItem>('/api/documents',{method:'POST',body:JSON.stringify(payload)},true)}
|
||||
|
||||
export type RetrievalHit={rank:number;knowledge_id:number;knowledge_version:number;document_id:number|null;document_version:number|null;source_id:number|null;source_version:number|null;source_name:string|null;source_url:string;title:string;category:string;snippet:string;snippet_start:number;snippet_end:number;score:number;matched_terms:string[]}
|
||||
export type RetrievalResult={run_id:string;query:string;mode:'keyword';status:'ok'|'no_evidence';top_k:number;category:string|null;query_terms:string[];corpus_size:number;corpus_fingerprint:string;total_hits:number;results:RetrievalHit[];elapsed_ms:number;created_at:string;config:{tokenizer:string;scorer:string;k1:number;b:number;title_weight:number;stopwords_version:string}}
|
||||
|
||||
@@ -12,6 +12,7 @@ const router=createRouter({history:createWebHistory(),routes:[
|
||||
{path:'/settings',component:()=>import('./views/AccountSettingsView.vue')},
|
||||
{path:'/indexes',component:()=>import('./views/IndexManagementView.vue'),meta:{role:'admin'}},
|
||||
{path:'/reviews',component:()=>import('./views/KnowledgeReviewView.vue'),meta:{role:'admin'}},
|
||||
{path:'/documents',component:()=>import('./views/DocumentManagementView.vue'),meta:{role:'admin'}},
|
||||
{path:'/admin',component:()=>import('./views/AdminView.vue'),meta:{role:'admin'}},
|
||||
]})
|
||||
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})
|
||||
|
||||
@@ -29,7 +29,7 @@ onMounted(() => load(0))
|
||||
<main>
|
||||
<h1>管理员用户管理</h1>
|
||||
<p>用户数据由受服务端管理员权限保护的接口实时提供。</p>
|
||||
<p><RouterLink to="/reviews">进入知识审核工作台</RouterLink> <RouterLink to="/indexes">进入索引管理</RouterLink></p>
|
||||
<p><RouterLink to="/reviews">进入知识审核工作台</RouterLink> <RouterLink to="/documents">进入文档管理</RouterLink> <RouterLink to="/indexes">进入索引管理</RouterLink></p>
|
||||
<p v-if="error" role="alert">{{ error }}</p>
|
||||
<p v-else-if="loading">正在读取用户列表…</p>
|
||||
<template v-else>
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { createDocument, getDocuments, getSources, type DocumentItem, type SourceItem } from '../api'
|
||||
const documents=ref<DocumentItem[]>([]), sources=ref<SourceItem[]>([]), query=ref(''), page=ref(1), total=ref(0), loading=ref(true), saving=ref(false), error=ref(''), notice=ref('')
|
||||
const form=ref({source_id:0,title:'',category:'未分类',content:'',collected_at:new Date().toISOString().slice(0,16),notes:''})
|
||||
async function refresh(next=page.value){loading.value=true;error.value='';try{const result=await getDocuments(query.value,next);documents.value=result.items;total.value=result.total;page.value=result.page;if(!sources.value.length)sources.value=(await getSources()).items}catch(cause){error.value=cause instanceof Error?cause.message:'无法读取文档'}finally{loading.value=false}}
|
||||
async function submit(){error.value='';notice.value='';if(!form.value.source_id||!form.value.title.trim()||!form.value.content.trim()||!form.value.collected_at){error.value='请填写来源、标题、正文和采集时间。';return}saving.value=true;try{await createDocument({...form.value,title:form.value.title.trim(),content:form.value.content.trim(),collected_at:new Date(form.value.collected_at).toISOString()});notice.value='文档已创建,可在后续知识条目中关联并审核。';form.value={source_id:0,title:'',category:'未分类',content:'',collected_at:new Date().toISOString().slice(0,16),notes:''};await refresh(1)}catch(cause){error.value=cause instanceof Error?cause.message:'创建文档失败'}finally{saving.value=false}}
|
||||
onMounted(()=>refresh(1))
|
||||
</script>
|
||||
<template><main><RouterLink to="/admin">返回管理员工作台</RouterLink><h1>文档管理</h1><p>文档必须关联已有来源;数据直接来自管理员受保护的 FastAPI 接口。</p><p v-if="error" role="alert">{{error}}</p><p v-if="notice" role="status">{{notice}}</p><form @submit.prevent="submit"><h2>新增文档</h2><label>来源<select v-model.number="form.source_id" required><option :value="0" disabled>请选择来源</option><option v-for="source in sources" :key="source.id" :value="source.id">{{source.name}}</option></select></label><label>标题<input v-model.trim="form.title" required maxlength="250"/></label><label>分类<input v-model.trim="form.category" required maxlength="80"/></label><label>采集时间<input v-model="form.collected_at" type="datetime-local" required/></label><label>正文<textarea v-model.trim="form.content" required maxlength="100000" rows="6"/></label><label>备注<textarea v-model.trim="form.notes" maxlength="10000" rows="2"/></label><button :disabled="saving">{{saving?'保存中…':'保存文档'}}</button></form><p v-if="loading">正在读取文档…</p><template v-else><label>搜索<input v-model="query" @keyup.enter="refresh(1)"/></label><button @click="refresh(1)">查询</button><table><thead><tr><th>标题</th><th>分类</th><th>来源 ID</th><th>采集时间</th></tr></thead><tbody><tr v-for="doc in documents" :key="doc.id"><td><b>{{doc.title}}</b><p>{{doc.content.slice(0,120)}}</p></td><td>{{doc.category}}</td><td>{{doc.source_id}}</td><td>{{new Date(doc.collected_at).toLocaleString()}}</td></tr><tr v-if="!documents.length"><td colspan="4">暂无文档。</td></tr></tbody></table><div><button :disabled="page===1" @click="refresh(page-1)">上一页</button><span>第 {{page}} 页,共 {{total}} 条</span><button :disabled="page*20>=total" @click="refresh(page+1)">下一页</button></div></template></main></template>
|
||||
Reference in New Issue
Block a user