@@ -1,10 +1,20 @@
< script setup lang = "ts" >
< script setup lang = "ts" >
import { onMounted , ref } from 'vue'
import { onMounted , ref } from 'vue'
import { createDocument , getDocuments , getSources , type DocumentItem , type SourceItem } from '../api'
import { createDocument , deleteDocument , getDocumentHistory , getDocuments , getSources , updateDocument , type AuditItem , type DocumentItem , type SourceItem } from '../api'
type DocumentForm = { source _id : number ; title : string ; category : string ; content : string ; collected _at : string ; notes : string }
const blankForm = ( ) : DocumentForm => ( { source _id : 0 , title : '' , category : '未分类' , content : '' , collected _at : new Date ( ) . toISOString ( ) . slice ( 0 , 16 ) , notes : '' } )
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 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 : '' } )
const form = ref < DocumentForm > ( blankForm ( ) ) , editing = ref < DocumentItem > ( ) , historyFor = ref < DocumentItem > ( ) , historyItems = ref < AuditItem [ ] > ( [ ] ) , historyLoading = ref ( false )
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 } }
function message ( cause : unknown , fallback : string ) { return cause instanceof Error ? cause . message : fallback }
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 } }
function localTime ( value : string ) { return new Date ( value ) . toLocaleString ( ) }
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 = message ( cause , '无法读取文档' ) } finally { loading . value = false } }
function beginEdit ( doc : DocumentItem ) { editing . value = doc ; form . value = { source _id : doc . source _id , title : doc . title , category : doc . category , content : doc . content , collected _at : new Date ( doc . collected _at ) . toISOString ( ) . slice ( 0 , 16 ) , notes : doc . notes } ; notice . value = '' }
function cancelEdit ( ) { editing . value = undefined ; form . value = blankForm ( ) }
function valid ( ) { if ( ! form . value . source _id || ! form . value . title . trim ( ) || ! form . value . content . trim ( ) || ! form . value . collected _at ) { error . value = '请填写来源、标题、正文和采集时间。' ; return false } return true }
async function submit ( ) { error . value = '' ; notice . value = '' ; if ( ! valid ( ) ) return ; saving . value = true ; const isEditing = Boolean ( editing . value ) ; try { const common = { title : form . value . title . trim ( ) , category : form . value . category . trim ( ) , content : form . value . content . trim ( ) , collected _at : new Date ( form . value . collected _at ) . toISOString ( ) , notes : form . value . notes . trim ( ) } ; if ( editing . value ) { await updateDocument ( editing . value . id , { expected _version : editing . value . version , ... common } ) ; notice . value = '文档已更新;关联知识会由后端重新置为待审核。' } else { await createDocument ( { source _id : form . value . source _id , ... common } ) ; notice . value = '文档已创建,可在后续知识条目中关联并审核。' } cancelEdit ( ) ; await refresh ( isEditing ? page . value : 1 ) } catch ( cause ) { error . value = message ( cause , isEditing ? '更新文档失败' : '创建文档失败' ) } finally { saving . value = false } }
async function remove ( doc : DocumentItem ) { if ( ! window . confirm ( ` 确定删除“ ${ doc . title } ”吗?若仍被知识条目引用,后端会拒绝删除。 ` ) ) return ; error . value = '' ; notice . value = '' ; try { await deleteDocument ( doc . id , doc . version ) ; notice . value = '文档已软删除,历史记录仍可追溯。' ; if ( editing . value ? . id === doc . id ) cancelEdit ( ) ; await refresh ( documents . value . length === 1 && page . value > 1 ? page . value - 1 : page . value ) } catch ( cause ) { error . value = message ( cause , '删除文档失败' ) } }
async function showHistory ( doc : DocumentItem ) { historyFor . value = doc ; historyLoading . value = true ; error . value = '' ; try { historyItems . value = ( await getDocumentHistory ( doc . id ) ) . items } catch ( cause ) { error . value = message ( cause , '无法读取文档历史' ) ; historyFor . value = undefined } finally { historyLoading . value = false } }
onMounted ( ( ) => refresh ( 1 ) )
onMounted ( ( ) => refresh ( 1 ) )
< / script >
< / 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 Dat e(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 >
< 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>{{editing?'编辑文档':' 新增文档'}} </h2><label>来源<select v-model.number="form.source_id" required :disabled="Boolean(editing)" ><option :value="0" disabled>请选择来源</option><option v-for="source in sources" :key="source.id" :value="source.id">{{source.name}}</option></select></label><p v-if="editing">来源归属不可修改;如需变更来源,请新建文档。</p>< 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?(editing?'更新中…':'保存中…'):(editing?'保存修改 ':'保存文档') }}</button><button v-if="editing" type="button" :disabled="saving" @click="cancelEdit">取消编辑</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><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>{{localTim e(doc.collected_at)}}</td><td><button @click="beginEdit(doc)">编辑</button><button @click="showHistory(doc)">历史</button><button @click="remove(doc)">删除</button> </td></tr><tr v-if="!documents.length"><td colspan="5 ">暂无文档。</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><section v-if="historyFor"><h2>“{{historyFor.title}}”的历史记录</h2><button @click="historyFor=undefined">关闭历史</button><p v-if="historyLoading">正在读取历史…</p><table v-else><thead><tr><th>操作</th><th>时间</th><th>操作者</th><th>版本快照</th></tr></thead><tbody><tr v-for="item in historyItems" :key="item.id"><td>{{item.action}}</td><td>{{localTime(item.created_at)}}</td><td>{{item.actor||'系统'}}</td><td><details><summary>查看快照</summary><pre>{{JSON.stringify(item.snapshot,null,2)}}</pre></details></td></tr><tr v-if="!historyItems.length"><td colspan="4" > 暂无历史记录 。 < / td > < / tr > < / tbody > < / table > < / section > < / main > < / template >