20 lines
5.6 KiB
Vue
20 lines
5.6 KiB
Vue
<script setup lang="ts">
|
||
import { onMounted, ref } from 'vue'
|
||
import { createSource, deleteSource, getSourceHistory, getSources, updateSource, type AuditItem, type SourceItem } from '../api'
|
||
|
||
type SourceForm={name:string;source_url:string;notes:string}
|
||
const blank=():SourceForm=>({name:'',source_url:'',notes:''})
|
||
const sources=ref<SourceItem[]>([]), form=ref<SourceForm>(blank()), editing=ref<SourceItem>(), historyFor=ref<SourceItem>(), historyItems=ref<AuditItem[]>([])
|
||
const query=ref(''), page=ref(1), total=ref(0), loading=ref(true), saving=ref(false), historyLoading=ref(false), error=ref(''), notice=ref('')
|
||
function failure(cause:unknown,fallback:string){return cause instanceof Error?cause.message:fallback}
|
||
function validUrl(value:string){if(!value)return true;try{const url=new URL(value);return url.protocol==='http:'||url.protocol==='https:'}catch{return false}}
|
||
async function refresh(next=page.value){loading.value=true;error.value='';try{const result=await getSources(query.value,next);sources.value=result.items;total.value=result.total;page.value=result.page}catch(cause){error.value=failure(cause,'无法读取来源')}finally{loading.value=false}}
|
||
function beginEdit(source:SourceItem){editing.value=source;form.value={name:source.name,source_url:source.source_url??'',notes:source.notes};notice.value=''}
|
||
function cancelEdit(){editing.value=undefined;form.value=blank()}
|
||
async function submit(){error.value='';notice.value='';if(!form.value.name.trim()){error.value='请填写来源名称。';return}if(!validUrl(form.value.source_url.trim())){error.value='来源网址必须是有效的 http(s) 地址。';return}saving.value=true;const wasEditing=Boolean(editing.value);try{const payload={name:form.value.name.trim(),source_url:form.value.source_url.trim()||null,notes:form.value.notes.trim()};if(editing.value){await updateSource(editing.value.id,{expected_version:editing.value.version,...payload});notice.value='来源已更新;关联知识会由后端重新置为待审核。'}else{await createSource({...payload,source_url:payload.source_url??undefined});notice.value='来源已登记。'}cancelEdit();await refresh(wasEditing?page.value:1)}catch(cause){error.value=failure(cause,wasEditing?'更新来源失败':'创建来源失败')}finally{saving.value=false}}
|
||
async function remove(source:SourceItem){if(!window.confirm(`确定删除“${source.name}”吗?若仍被文档或知识条目引用,后端会拒绝删除。`))return;error.value='';notice.value='';try{await deleteSource(source.id,source.version);notice.value='来源已软删除,历史记录仍可追溯。';if(editing.value?.id===source.id)cancelEdit();await refresh(sources.value.length===1&&page.value>1?page.value-1:page.value)}catch(cause){error.value=failure(cause,'删除来源失败')}}
|
||
async function showHistory(source:SourceItem){historyFor.value=source;historyLoading.value=true;error.value='';try{historyItems.value=(await getSourceHistory(source.id)).items}catch(cause){error.value=failure(cause,'无法读取来源历史');historyFor.value=undefined}finally{historyLoading.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>{{editing?'编辑来源':'新增来源'}}</h2><label>名称<input v-model.trim="form.name" required maxlength="250"/></label><label>网址(可选)<input v-model.trim="form.source_url" type="url" placeholder="https://..." maxlength="1000"/></label><label>备注<textarea v-model.trim="form.notes" maxlength="10000" rows="3"/></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>备注</th><th>操作</th></tr></thead><tbody><tr v-for="source in sources" :key="source.id"><td>{{source.name}}</td><td><a v-if="source.source_url" :href="source.source_url" target="_blank" rel="noreferrer">打开来源</a><span v-else>未填写</span></td><td>{{source.notes||'—'}}</td><td><button @click="beginEdit(source)">编辑</button><button @click="showHistory(source)">历史</button><button @click="remove(source)">删除</button></td></tr><tr v-if="!sources.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><section v-if="historyFor"><h2>“{{historyFor.name}}”的历史记录</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>{{new Date(item.created_at).toLocaleString()}}</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>
|