@@ -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 { 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 { createKnowledge , createSource , getHealth , getKnowledge , getSources , type SourceItem } from './api'
import { retrieveHybrid } from './services/hybrid'
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'
type Page = 'chat' | 'knowledge' | 'sources' | 'evaluation'
const page = ref < Page > ( 'chat' ) , apiStatus = ref ( '检测中' ) , question = ref ( '' ) , busy = ref ( false ) , search = ref ( '' ) , showForm = ref ( false ) , error = ref ( '' ) , notice = ref ( '' )
const router = useRouter ( ) , currentUser = ref < SessionUser > ( ) , page = ref < Page > ( '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 messages = ref ( [ { role : 'assistant' , content : '你好!我是边疆红驿文旅问答原型。当前后端仅为基础接口,尚未接入知识检索和大语言模型。' } ] as { role : string ; content : string ; sources ? : { title : string ; source _url ? : string | null } [ ] } [ ] )
const items = ref < any [ ] > ( [ ] ) , form = ref ( { title : '' , category : '旅游资源' , content : '' , source _url : '' } )
const items = ref < any [ ] > ( [ ] ) , form = ref ( { title : '' , category : '旅游资源' , content : '' , source _url : '' } )
const sources = ref < SourceItem [ ] > ( [ ] ) , sourceSearch = ref ( '' ) , showSourceForm = ref ( false ) , sourceForm = ref ( { name : '' , source _url : '' , notes : '' } )
const sources = ref < SourceItem [ ] > ( [ ] ) , 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 ( ) ) ) )
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 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 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 : '保存失败' } }
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
let sourceSearchTimer : number | undefined
watch ( sourceSearch , ( ) => { window . clearTimeout ( sourceSearchTimer ) ; sourceSearchTimer = window . setTimeout ( refreshSources , 260 ) } )
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 } }
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 } }
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 : '保存失败' } }
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 : '保存失败' } }
< / script >
< / script >
< template > < div class = "shell" > < aside class = "sidebar" > < div class = "brand" > < div class = "mark" > < Bot :size = "23" / > < / div > < div > < b > 边疆红驿 < / b > < small > NALATI KNOWLEDGE HUB < / small > < / div > < / div > < p class = "muted label" > 功能列表 < / p > < nav > < button v-for = "n in n av" :key="n.id" :class="{active:page===n.id}" @click="page=n.id"><component :is="n.icon" :size="18"/>{{n.label}}<ChevronRight v-if="page===n.id" class="chev" :size="15"/></button></nav><div class="quick-actions"><p class="muted label">快捷操作</p><button v-if="page==='knowledge'" class="side-action" @click="showForm=true"><Plus :size="16"/>新增知识</button><button v-if="page==='sources'" class="side-action" @click="showSourceForm=true"><Plus :size="16"/>新增来源</button><p v-if="page==='chat'" class="side-hint">选择示例问题,或直接开始提问。</p><p v-if="page==='evaluation'" class="side-hint">实验结果只展示真实运行数据。</p></div><div class="side-bottom"><div class="tip"><ShieldCheck :size="17"/><div><b>可信知识服务</b><p>优先展示可核验来源,信息不足时明确提示。 </p></div></div><p class="api" > ● API {{ apiStatus }} < span > v0 .1 .0 < / span > < / p > < / div > < / aside > < main > < header > < span > 边疆红驿 / { { nav . find ( n => n . id === page ) ? . label } } < / span > < div > < span class = "pill" > ● 原型阶段 < / span > < i > 研 < / i > < / div > < / header > < section class = "content" >
< template > < div class = "shell" > < aside class = "sidebar" > < div class = "brand" > < div class = "mark" > < Bot :size = "23" / > < / div > < div > < b > 边疆红驿 < / b > < small > NALATI KNOWLEDGE HUB < / small > < / div > < / div > < p class = "muted label" > 功能列表 < / p > < nav > < button v-for = "n in visibleN av" :key="n.id" :class="{active:page===n.id}" @click="page=n.id"><component :is="n.icon" :size="18"/>{{n.label}}<ChevronRight v-if="page===n.id" class="chev" :size="15"/></button><button @click="router.push('/profile')"><ShieldCheck :size="18"/>个人中心</button><button v-if="currentUser?.role==='admin'" @click="router.push('/admin')"><Activity :size="18"/>用户管理</button>< /nav><div class="quick-actions"><p class="muted label">快捷操作</p><button v-if="page==='knowledge'" class="side-action" @click="showForm=true"><Plus :size="16"/>新增知识</button><button v-if="page==='sources'" class="side-action" @click="showSourceForm=true"><Plus :size="16"/>新增来源</button><p v-if="page==='chat'" class="side-hint">选择示例问题,或直接开始提问。</p><p v-if="page==='evaluation'" class="side-hint">实验结果只展示真实运行数据。</p></div><div class="side-bottom"><div class="tip"><ShieldCheck :size="17"/><div><b>{{currentUser?.role==='admin'?'管理员':'已登录用户'}}</b><p>{{currentUser?.nickname||currentUser?.username||'正在载入账户'}} </p></div></div><p class="api" > ● API {{ apiStatus }} < span > v0 .1 .0 < / span > < / p > < / div > < / aside > < main > < header > < span > 边疆红驿 / { { nav . find ( n => n . id === page ) ? . label } } < / span > < div > < span class = "pill" > ● { { currentUser ? . role === 'admin' ? '管理员' : '用户' } } < / span > < i > 研 < / i > < / div > < / header > < section class = "content" >
< div v-if = "page==='chat'"><div class="heading"><div><small class="eyebrow"><Sparkles :size="14"/> LOCAL KNOWLEDGE ASSISTANT</small><h1>文旅知识,问得明白</h1><p>面向那拉提文旅与民宿服务的知识问答原型</p></div><span class="chip"><ShieldCheck :size="15"/> 来源可追溯 · 设计目标</span></div><div class="chat-layout"><div class="card chat"><div class="chat-head"><div class="mark"><Bot :size="20"/></div><div><b>边疆红驿助手</b><small>问答服务原型</small></div></div><div class="messages"><div v-for="(m,i) in messages" :key="i" class="message" :class="m.role"><small>{{m.role==='assistant'?'智能助手':'你'}}</small><p>{{m.content}}</p><div v-if="m.sources?.length" class="sources"><b><Link2 :size="14"/> 参考来源</b><a v-for="s in m.sources" :key="s.title" :href="s.source_url ?? undefined" target="_blank">{{s.title}}</a></div></div><p v-if="busy">正在请求后端…</p></div><form class="composer" @submit.prevent="send"><textarea v-model="question" placeholder="输入你想了解的文旅问题…" rows="2" @keydown.enter.exact.prevent="send"/><div><small>当前为基础接口,尚未接入真实检索</small><button class="primary" :disabled="busy||!question.trim()"><Send :size="15"/> 发送</button></div></form></div><aside class="suggest"><b><Sparkles :size="16"/> 你可以这样问</b><button @click="question='那拉提有哪些值得了解的旅游资源?'"><Globe2/>旅游资源 <ChevronRight/></button><button @click="question='有哪些经过核验的红色文化资料?'"><BookOpen/>红色文化 <ChevronRight/></button><button @click="question='民宿服务信息应如何核验?'"><ShieldCheck/>民宿服务 <ChevronRight/></button><div class="notice"><b><AlertCircle :size="15" /> 信息说明 < / b > < p > 本项目仍处于原型阶段 。 演示内容不代表已核验的地方文旅事实 。 < / p > < / div > < / aside > < / div > < / div >
< div v-if = "page==='chat'"><div class="heading"><div><small class="eyebrow"><Sparkles :size="14"/> LOCAL KNOWLEDGE ASSISTANT</small><h1>文旅知识,问得明白</h1><p>面向那拉提文旅与民宿服务的知识问答原型</p></div><span class="chip"><ShieldCheck :size="15"/> 来源可追溯 · 设计目标</span></div><div class="chat-layout"><div class="card chat"><div class="chat-head"><div class="mark"><Bot :size="20"/></div><div><b>边疆红驿助手</b><small>问答服务原型</small></div></div><div class="messages"><div v-for="(m,i) in messages" :key="i" class="message" :class="m.role"><small>{{m.role==='assistant'?'智能助手':'你'}}</small><p>{{m.content}}</p><div v-if="m.sources?.length" class="sources"><b><Link2 :size="14"/> 参考来源</b><a v-for="s in m.sources" :key="s.title" :href="s.source_url ?? undefined" target="_blank">{{s.title}}</a></div></div><p v-if="busy">正在请求后端…</p></div><form class="composer" @submit.prevent="send"><textarea v-model="question" placeholder="输入你想了解的文旅问题…" rows="2" @keydown.enter.exact.prevent="send"/><div><small>当前为基础接口,尚未接入真实检索</small><button class="primary" :disabled="busy||!question.trim()"><Send :size="15"/> 发送</button></div></form></div><aside class="suggest"><b><Sparkles :size="16"/> 你可以这样问</b><button @click="question='那拉提有哪些值得了解的旅游资源?'"><Globe2/>旅游资源 <ChevronRight/></button><button @click="question='有哪些经过核验的红色文化资料?'"><BookOpen/>红色文化 <ChevronRight/></button><button @click="question='民宿服务信息应如何核验?'"><ShieldCheck/>民宿服务 <ChevronRight/></button><div class="notice"><b><AlertCircle :size="15" /> 信息说明 < / b > < p > 本项目仍处于原型阶段 。 演示内容不代表已核验的地方文旅事实 。 < / p > < / div > < / aside > < / div > < / div >
< div v-else-if = "page==='knowledge'"><div class="heading"><div><small class="eyebrow"><Database :size="14"/> KNOWLEDGE MANAGEMENT</small><h1>知识库管理</h1><p>集中管理领域知识、来源和审核状态。</p></div><button class="primary" @click="showForm=!showForm"><Plus :size="16"/> 新增知识</button></div><div v-if="showForm" class="card form-card"><b>新增知识条目</b><form class="knowledge-form" @submit.prevent="save"><label>标题<input v-model="form.title" required maxlength="250"/></label><label>分类<select v-model="form.category"><option>红色文化</option><option>旅游资源</option><option>民宿服务</option><option>交通出行</option><option>其他</option></select></label><label class="wide">正文<textarea v-model="form.content" required rows="3"/></label><label class="wide">来源网址(可选)<input v-model="form.source_url" type="url" placeholder="https://..."/></label><div class="wide actions"><button type="button" class="secondary" @click="showForm=false">取消</button><button class="primary">保存条目</button></div></form></div><div class="stats"><div class="card stat"><small>知识条目</small><strong>{{items.length}}</strong><span>来自当前数据库</span></div><div class="card stat"><small>已核验</small><strong>{{items.filter(x=>x.verification_status==='verified').length}}</strong><span>需经过真实审核</span></div><div class="card stat"><small>待审核</small><strong>{{items.filter(x=>x.verification_status==='pending').length}}</strong><span>尚未进入检索索引</span></div></div><p v-if="notice" class="success">{{notice}}</p><p v-if="error" class="error">{{error}}</p><div class="card table-card"><div class="table-head"><div><b>知识条目</b><p>列表来自后端接口,不含预设真实资料。</p></div><label class="search"><Search :size="15"/><input v-model="search" placeholder="搜索知识"/></label></div><div class="scroll"><table><thead><tr><th>标题</th><th>分类</th><th>来源</th><th>状态</th><th>创建时间</th></tr></thead><tbody><tr v-for="x in filtered" :key="x.id"><td><b>{{x.title}}</b><small>{{x.content.slice(0,45)}}</small></td><td>{{x.category}}</td><td><a v-if="x.source_url" :href="x.source_url" target="_blank">打开来源</a><span v-else>未填写</span></td><td><em>{{x.verification_status==='pending'?'待审核':x.verification_status}}</em></td><td>{{new Date(x.created_at).toLocaleDateString()}}</td></tr><tr v-if="!filtered.length"><td colspan="5" class="empty" > 暂无条目 , 点击 “ 新增知识 ” 添加 。 < / td > < / tr > < / tbody > < / table > < / div > < / div > < / div >
< div v-else-if = "page==='knowledge'"><div class="heading"><div><small class="eyebrow"><Database :size="14"/> KNOWLEDGE MANAGEMENT</small><h1>知识库管理</h1><p>集中管理领域知识、来源和审核状态。</p></div><button class="primary" @click="showForm=!showForm"><Plus :size="16"/> 新增知识</button></div><div v-if="showForm" class="card form-card"><b>新增知识条目</b><form class="knowledge-form" @submit.prevent="save"><label>标题<input v-model="form.title" required maxlength="250"/></label><label>分类<select v-model="form.category"><option>红色文化</option><option>旅游资源</option><option>民宿服务</option><option>交通出行</option><option>其他</option></select></label><label class="wide">正文<textarea v-model="form.content" required rows="3"/></label><label class="wide">来源网址(可选)<input v-model="form.source_url" type="url" placeholder="https://..."/></label><div class="wide actions"><button type="button" class="secondary" @click="showForm=false">取消</button><button class="primary">保存条目</button></div></form></div><div class="stats"><div class="card stat"><small>知识条目</small><strong>{{items.length}}</strong><span>来自当前数据库</span></div><div class="card stat"><small>已核验</small><strong>{{items.filter(x=>x.verification_status==='verified').length}}</strong><span>需经过真实审核</span></div><div class="card stat"><small>待审核</small><strong>{{items.filter(x=>x.verification_status==='pending').length}}</strong><span>尚未进入检索索引</span></div></div><p v-if="notice" class="success">{{notice}}</p><p v-if="error" class="error">{{error}}</p><div class="card table-card"><div class="table-head"><div><b>知识条目</b><p>列表来自后端接口,不含预设真实资料。</p></div><label class="search"><Search :size="15"/><input v-model="search" placeholder="搜索知识"/></label></div><div class="scroll"><table><thead><tr><th>标题</th><th>分类</th><th>来源</th><th>状态</th><th>创建时间</th></tr></thead><tbody><tr v-for="x in filtered" :key="x.id"><td><b>{{x.title}}</b><small>{{x.content.slice(0,45)}}</small></td><td>{{x.category}}</td><td><a v-if="x.source_url" :href="x.source_url" target="_blank">打开来源</a><span v-else>未填写</span></td><td><em>{{x.verification_status==='pending'?'待审核':x.verification_status}}</em></td><td>{{new Date(x.created_at).toLocaleDateString()}}</td></tr><tr v-if="!filtered.length"><td colspan="5" class="empty" > 暂无条目 , 点击 “ 新增知识 ” 添加 。 < / td > < / tr > < / tbody > < / table > < / div > < / div > < / div >
< div v-else-if = "page==='sources'"><div class="heading"><div><small class="eyebrow"><Globe2 :size="14"/> SOURCE OPERATIONS</small><h1>来源与更新</h1><p>登记可追溯来源,为文档采集和知识审核建立基础。</p></div><button class="primary" @click="showSourceForm=!showSourceForm"><Plus :size="16"/> 新增来源</button></div><div v-if="showSourceForm" class="card form-card"><b>登记来源</b><form class="knowledge-form" @submit.prevent="saveSource"><label>来源名称<input v-model="sourceForm.name" required maxlength="250" placeholder="例如:那拉提景区官网"/></label><label>来源网址(可选)<input v-model="sourceForm.source_url" type="url" placeholder="https://..."/></label><label class="wide">备注<textarea v-model="sourceForm.notes" rows="3" placeholder="记录来源用途、可靠性判断或待核验事项"/></label><div class="wide actions"><button type="button" class="secondary" @click="showSourceForm=false">取消</button><button class="primary">保存来源</button></div></form></div><p v-if="notice" class="success">{{notice}}</p><p v-if="error" class="error">{{error}}</p><div class="stats"><div class="card stat"><small>来源总数</small><strong>{{sources.length}}</strong><span>来自当前数据库</span></div><div class="card stat"><small>关联文档</small><strong>—</strong><span>文档工作台待接入</span></div><div class="card stat"><small>自动更新</small><strong class="pending">未启用</strong><span>当前不会自动抓取网页</span></div></div><div class="card table-card"><div class="table-head"><div><b>已登记来源</b><p>来源不是审核通过的证明,关键事实仍需交叉核验。</p></div><label class="search"><Search :size="15"/><input v-model="sourceSearch" placeholder="搜索名称或备注" @keyup.enter="refreshSources"/></label></div><div class="scroll"><table><thead><tr><th>名称</th><th>网址</th><th>备注</th><th>登记时间</th></tr></thead><tbody><tr v-for="source in sources" :key="source.id"><td><b>{{source.name}}</b></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>{{new Date(source.created_at).toLocaleDateString()}}</td></tr><tr v-if="!sources.length"><td colspan="4" class="empty">暂无来源,点击“新增来源”登记。</td></tr></tbody></table></div></div><div class="card roadmap" > < b > 来源处理边界 < / b > < div > < span > 01 登记来源 < / span > < ChevronRight / > < span > 02 人工核验 < / span > < ChevronRight / > < span > 03 关联文档 < / span > < ChevronRight / > < span > 04 后续检索 < / span > < / div > < / div > < / div >
< div v-else-if = "page==='sources'"><div class="heading"><div><small class="eyebrow"><Globe2 :size="14"/> SOURCE OPERATIONS</small><h1>来源与更新</h1><p>登记可追溯来源,为文档采集和知识审核建立基础。</p></div><button class="primary" @click="showSourceForm=!showSourceForm"><Plus :size="16"/> 新增来源</button></div><div v-if="showSourceForm" class="card form-card"><b>登记来源</b><form class="knowledge-form" @submit.prevent="saveSource"><label>来源名称<input v-model="sourceForm.name" required maxlength="250" placeholder="例如:那拉提景区官网"/></label><label>来源网址(可选)<input v-model="sourceForm.source_url" type="url" placeholder="https://..."/></label><label class="wide">备注<textarea v-model="sourceForm.notes" rows="3" placeholder="记录来源用途、可靠性判断或待核验事项"/></label><div class="wide actions"><button type="button" class="secondary" @click="showSourceForm=false">取消</button><button class="primary">保存来源</button></div></form></div><p v-if="notice" class="success">{{notice}}</p><p v-if="error" class="error">{{error}}</p><div class="stats"><div class="card stat"><small>来源总数</small><strong>{{sources.length}}</strong><span>来自当前数据库</span></div><div class="card stat"><small>关联文档</small><strong>—</strong><span>文档工作台待接入</span></div><div class="card stat"><small>自动更新</small><strong class="pending">未启用</strong><span>当前不会自动抓取网页</span></div></div><div class="card table-card"><div class="table-head"><div><b>已登记来源</b><p>来源不是审核通过的证明,关键事实仍需交叉核验。</p></div><label class="search"><Search :size="15"/><input v-model="sourceSearch" placeholder="搜索名称或备注" @keyup.enter="refreshSources"/></label></div><div class="scroll"><table><thead><tr><th>名称</th><th>网址</th><th>备注</th><th>登记时间</th></tr></thead><tbody><tr v-for="source in sources" :key="source.id"><td><b>{{source.name}}</b></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>{{new Date(source.created_at).toLocaleDateString()}}</td></tr><tr v-if="!sources.length"><td colspan="4" class="empty">暂无来源,点击“新增来源”登记。</td></tr></tbody></table></div></div><div class="card roadmap" > < b > 来源处理边界 < / b > < div > < span > 01 登记来源 < / span > < ChevronRight / > < span > 02 人工核验 < / span > < ChevronRight / > < span > 03 关联文档 < / span > < ChevronRight / > < span > 04 后续检索 < / span > < / div > < / div > < / div >