Files
border-red-post-nalati/frontend/src/views/SourceManagementView.vue
T

20 lines
5.6 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>