feat(web): 更新应用外壳路由视图和政策制度页面,增强前端路由嵌套和页面展示能力

This commit is contained in:
caoxiaozhu
2026-05-15 09:37:42 +00:00
parent 02f54ea208
commit 72ea05ae0d
3 changed files with 150 additions and 48 deletions

View File

@@ -20,6 +20,7 @@
'policies-main': activeView === 'policies', 'policies-main': activeView === 'policies',
'audit-main': activeView === 'audit', 'audit-main': activeView === 'audit',
'audit-detail-main': activeView === 'audit' && auditDetailOpen, 'audit-detail-main': activeView === 'audit' && auditDetailOpen,
'logs-main': activeView === 'logs',
'employees-main': activeView === 'employees', 'employees-main': activeView === 'employees',
'settings-main': activeView === 'settings' 'settings-main': activeView === 'settings'
}" }"
@@ -33,8 +34,10 @@
:active-range="activeRange" :active-range="activeRange"
:employee-summary="employeeSummary" :employee-summary="employeeSummary"
:knowledge-summary="knowledgeSummary" :knowledge-summary="knowledgeSummary"
:logs-summary="logsSummary"
:request-summary="requestSummary" :request-summary="requestSummary"
:detail-mode="detailMode" :detail-mode="detailMode"
:log-detail-mode="logDetailMode"
:detail-alerts="detailAlerts" :detail-alerts="detailAlerts"
:custom-range="customRange" :custom-range="customRange"
@update:search="search = $event" @update:search="search = $event"
@@ -45,7 +48,7 @@
/> />
<FilterBar <FilterBar
v-if="activeView !== 'overview' && activeView !== 'workbench' && activeView !== 'requests' && activeView !== 'approval' && activeView !== 'policies' && activeView !== 'audit' && activeView !== 'employees' && activeView !== 'settings'" v-if="activeView !== 'overview' && activeView !== 'workbench' && activeView !== 'requests' && activeView !== 'approval' && activeView !== 'policies' && activeView !== 'audit' && activeView !== 'logs' && activeView !== 'employees' && activeView !== 'settings'"
:compact="activeView === 'overview'" :compact="activeView === 'overview'"
:filters="filters" :filters="filters"
:ranges="ranges" :ranges="ranges"
@@ -60,6 +63,7 @@
'approval-workarea': activeView === 'approval', 'approval-workarea': activeView === 'approval',
'policies-workarea': activeView === 'policies', 'policies-workarea': activeView === 'policies',
'audit-workarea': activeView === 'audit', 'audit-workarea': activeView === 'audit',
'logs-workarea': activeView === 'logs',
'employees-workarea': activeView === 'employees', 'employees-workarea': activeView === 'employees',
'settings-workarea': activeView === 'settings' 'settings-workarea': activeView === 'settings'
}" }"
@@ -102,6 +106,8 @@
<ApprovalCenterView v-else-if="activeView === 'approval'" /> <ApprovalCenterView v-else-if="activeView === 'approval'" />
<PoliciesView v-else-if="activeView === 'policies'" @summary-change="knowledgeSummary = $event" /> <PoliciesView v-else-if="activeView === 'policies'" @summary-change="knowledgeSummary = $event" />
<AuditView v-else-if="activeView === 'audit'" @detail-open-change="auditDetailOpen = $event" /> <AuditView v-else-if="activeView === 'audit'" @detail-open-change="auditDetailOpen = $event" />
<LogDetailView v-else-if="activeView === 'logs' && logDetailMode" />
<LogsView v-else-if="activeView === 'logs'" @summary-change="logsSummary = $event" />
<EmployeeManagementView v-else-if="activeView === 'employees'" @overview-change="employeeSummary = $event" /> <EmployeeManagementView v-else-if="activeView === 'employees'" @overview-change="employeeSummary = $event" />
<SettingsView v-else /> <SettingsView v-else />
</section> </section>
@@ -135,6 +141,8 @@ import RequestsView from './RequestsView.vue'
import ApprovalCenterView from './ApprovalCenterView.vue' import ApprovalCenterView from './ApprovalCenterView.vue'
import PoliciesView from './PoliciesView.vue' import PoliciesView from './PoliciesView.vue'
import AuditView from './AuditView.vue' import AuditView from './AuditView.vue'
import LogsView from './LogsView.vue'
import LogDetailView from './LogDetailView.vue'
import EmployeeManagementView from './EmployeeManagementView.vue' import EmployeeManagementView from './EmployeeManagementView.vue'
import SettingsView from './SettingsView.vue' import SettingsView from './SettingsView.vue'
@@ -144,6 +152,7 @@ import { filterNavItemsByAccess } from '../utils/accessControl.js'
const employeeSummary = ref(null) const employeeSummary = ref(null)
const knowledgeSummary = ref(null) const knowledgeSummary = ref(null)
const logsSummary = ref(null)
const auditDetailOpen = ref(false) const auditDetailOpen = ref(false)
const { const {
@@ -154,6 +163,7 @@ const {
customRange, customRange,
detailAlerts, detailAlerts,
detailMode, detailMode,
logDetailMode,
filteredRequests, filteredRequests,
filters, filters,
handleApprove, handleApprove,

View File

@@ -97,7 +97,7 @@
v-if="isAdmin" v-if="isAdmin"
class="more-btn ingest" class="more-btn ingest"
type="button" type="button"
:disabled="Boolean(ingestingId) || deletingId === doc.id" :disabled="Boolean(ingestingId) || deletingId === doc.id || Number(doc.stateCode || 0) === 2"
:aria-label="resolveIngestActionTitle(doc)" :aria-label="resolveIngestActionTitle(doc)"
:title="resolveIngestActionTitle(doc)" :title="resolveIngestActionTitle(doc)"
@click="handleManualIngest(doc)" @click="handleManualIngest(doc)"
@@ -356,6 +356,21 @@
<div v-else-if="llmWikiError" class="preview-status error">{{ llmWikiError }}</div> <div v-else-if="llmWikiError" class="preview-status error">{{ llmWikiError }}</div>
<div v-else-if="llmWikiDocument" class="llm-wiki-grid"> <div v-else-if="llmWikiDocument" class="llm-wiki-grid">
<section class="llm-wiki-section llm-wiki-summary-section"> <section class="llm-wiki-section llm-wiki-summary-section">
<div
v-if="llmWikiDocument.quality_status !== 'formal'"
class="llm-wiki-alert"
:class="resolveLlmWikiQualityTone(llmWikiDocument)"
>
<strong>{{ resolveLlmWikiQualityLabel(llmWikiDocument) }}</strong>
<p>{{ llmWikiDocument.quality_note || '当前展示内容不是正式 Hermes 归纳,请人工复核后再使用。' }}</p>
</div>
<div
v-else-if="llmWikiDocument.quality_note"
class="llm-wiki-alert info"
>
<strong>{{ resolveLlmWikiQualityLabel(llmWikiDocument) }}</strong>
<p>{{ llmWikiDocument.quality_note }}</p>
</div>
<div class="llm-wiki-section-head"> <div class="llm-wiki-section-head">
<div> <div>
<h3>知识总结</h3> <h3>知识总结</h3>
@@ -363,6 +378,12 @@
</div> </div>
<span class="llm-wiki-count">{{ llmWikiDocument.knowledge_candidate_count }} 条知识</span> <span class="llm-wiki-count">{{ llmWikiDocument.knowledge_candidate_count }} 条知识</span>
</div> </div>
<div class="llm-wiki-stat-grid">
<span>正文分块 {{ llmWikiDocument.candidate_chunk_count }}</span>
<span>过滤分块 {{ llmWikiDocument.filtered_chunk_count }}</span>
<span>成功分组 {{ llmWikiDocument.successful_group_count }}/{{ llmWikiDocument.group_count }}</span>
<span>正式知识 {{ llmWikiDocument.formal_knowledge_candidate_count }}</span>
</div>
<textarea <textarea
v-model="llmWikiSummaryDraft" v-model="llmWikiSummaryDraft"
class="llm-wiki-editor" class="llm-wiki-editor"

View File

@@ -31,6 +31,8 @@ import { resolveKnowledgePreviewLayoutState } from './knowledgePreviewLayout.js'
import { resolveInitialKnowledgeFolder } from './knowledgeFolderSelection.js' import { resolveInitialKnowledgeFolder } from './knowledgeFolderSelection.js'
import { buildOnlyOfficePreviewConfig } from './onlyOfficePreviewConfig.js' import { buildOnlyOfficePreviewConfig } from './onlyOfficePreviewConfig.js'
const KNOWLEDGE_POLL_INTERVAL_MS = 5000
function triggerFileDownload(blob, filename) { function triggerFileDownload(blob, filename) {
const url = URL.createObjectURL(blob) const url = URL.createObjectURL(blob)
const anchor = document.createElement('a') const anchor = document.createElement('a')
@@ -42,6 +44,7 @@ function triggerFileDownload(blob, filename) {
let bodyOverflowSnapshot = '' let bodyOverflowSnapshot = ''
let bodyOverscrollBehaviorSnapshot = '' let bodyOverscrollBehaviorSnapshot = ''
let libraryPollTimer = 0
function setBodyScrollLocked(isLocked) { function setBodyScrollLocked(isLocked) {
if (typeof document === 'undefined') { if (typeof document === 'undefined') {
@@ -281,9 +284,15 @@ export default {
activeFolder.value = resolveInitialKnowledgeFolder(folders.value, activeFolder.value) activeFolder.value = resolveInitialKnowledgeFolder(folders.value, activeFolder.value)
if (options.preserveSelection && selectedDocument.value?.id) { if (options.preserveSelection && selectedDocument.value?.id) {
const exists = documents.value.some((doc) => doc.id === selectedDocument.value.id) const nextDocument = documents.value.find((doc) => doc.id === selectedDocument.value.id)
const exists = Boolean(nextDocument)
if (!exists) { if (!exists) {
closePreview() closePreview()
} else {
selectedDocument.value = {
...selectedDocument.value,
...nextDocument
}
} }
} }
if (options.preserveSelection && llmWikiDocument.value?.document_id) { if (options.preserveSelection && llmWikiDocument.value?.document_id) {
@@ -292,6 +301,7 @@ export default {
closeLlmWikiSummary() closeLlmWikiSummary()
} }
} }
syncLibraryPolling()
} catch (error) { } catch (error) {
emit('summary-change', { totalDocuments: 0 }) emit('summary-change', { totalDocuments: 0 })
toast(error.message || '知识库加载失败。') toast(error.message || '知识库加载失败。')
@@ -351,10 +361,37 @@ export default {
...patch ...patch
} }
} }
syncLibraryPolling()
}
function hasSyncingDocuments() {
return documents.value.some((doc) => Number(doc?.stateCode || 0) === 2)
}
function stopLibraryPolling() {
if (libraryPollTimer) {
window.clearInterval(libraryPollTimer)
libraryPollTimer = 0
}
}
function startLibraryPolling() {
stopLibraryPolling()
libraryPollTimer = window.setInterval(() => {
loadLibrary({ preserveSelection: true })
}, KNOWLEDGE_POLL_INTERVAL_MS)
}
function syncLibraryPolling() {
if (hasSyncingDocuments()) {
startLibraryPolling()
return
}
stopLibraryPolling()
} }
function resolveIngestActionLabel(document) { function resolveIngestActionLabel(document) {
if (ingestingId.value === document.id) { if (ingestingId.value === document.id || Number(document?.stateCode || 0) === 2) {
return '归纳中' return '归纳中'
} }
return Number(document?.stateCode || 0) === 3 ? '重新归纳' : '归纳' return Number(document?.stateCode || 0) === 3 ? '重新归纳' : '归纳'
@@ -372,27 +409,64 @@ export default {
} }
function canViewLlmWiki(document) { function canViewLlmWiki(document) {
return isAdmin.value && Number(document?.stateCode || 0) === 3 return isAdmin.value && Boolean(document?.llmWikiAvailable)
} }
function resolveViewLlmWikiTitle(document) { function resolveViewLlmWikiTitle(document) {
if (!isAdmin.value) { if (!isAdmin.value) {
return '仅管理员可查看 LLM Wiki 归纳内容' return '仅管理员可查看 LLM Wiki 归纳内容'
} }
if (document?.llmWikiAvailable && Number(document?.stateCode || 0) === 4) {
return '查看本次降级归纳结果,仅供人工排查,不能视为正式知识'
}
if (document?.llmWikiAvailable && document?.llmWikiQualityStatus === 'partial_degraded') {
return '查看当前归纳结果,存在部分降级分组,请人工复核'
}
if (document?.llmWikiAvailable) {
return '查看并编辑当前文档的 LLM Wiki 归纳内容'
}
if (Number(document?.stateCode || 0) === 2) { if (Number(document?.stateCode || 0) === 2) {
return 'Hermes 正在归纳当前文档,完成后可查看 LLM Wiki 知识总结' return 'Hermes 正在归纳当前文档,完成后可查看 LLM Wiki 知识总结'
} }
if (Number(document?.stateCode || 0) === 4) { if (Number(document?.stateCode || 0) === 4) {
return '当前文档上次归纳失败,请重新归纳后再查看' return '当前文档上次归纳失败,且没有可查看的 LLM Wiki 产物'
} }
if (Number(document?.stateCode || 0) !== 3) { if (Number(document?.stateCode || 0) !== 3) {
return '文档尚未完成归纳,暂无可查看的 LLM Wiki 知识总结' return '文档尚未完成归纳,暂无可查看的 LLM Wiki 知识总结'
} }
return '查看并编辑当前文档的 LLM Wiki 归纳内容' return '查看当前文档的 LLM Wiki 归纳内容'
}
function resolveLlmWikiQualityLabel(document) {
const qualityStatus = String(document?.quality_status || '').trim()
if (qualityStatus === 'partial_degraded') {
return '部分降级'
}
if (qualityStatus === 'fallback_only') {
return '降级兜底'
}
if (qualityStatus === 'runtime_only') {
return '非 Hermes 结果'
}
if (qualityStatus === 'failed') {
return '归纳失败'
}
return '正式归纳'
}
function resolveLlmWikiQualityTone(document) {
const qualityStatus = String(document?.quality_status || '').trim()
if (qualityStatus === 'formal') {
return 'success'
}
if (qualityStatus === 'partial_degraded') {
return 'warning'
}
return 'danger'
} }
async function handleManualIngest(document) { async function handleManualIngest(document) {
if (!isAdmin.value || ingestingId.value || !document?.id) { if (!isAdmin.value || ingestingId.value || !document?.id || Number(document?.stateCode || 0) === 2) {
return return
} }
@@ -406,19 +480,13 @@ export default {
try { try {
const payload = await syncKnowledgeDocumentToLlmWiki({ const payload = await syncKnowledgeDocumentToLlmWiki({
folder: document.folder, folder: document.folder,
documentId: document.id documentId: document.id,
force: true
}) })
await loadLibrary({ preserveSelection: true }) await loadLibrary({ preserveSelection: true })
if (selectedDocument.value?.id === document.id) { toast(payload.summary || 'Hermes 已进入后台归纳。')
await selectDocument(document.id)
}
toast(payload.summary || 'Hermes 已完成文档归纳。')
} catch (error) { } catch (error) {
patchDocumentState(document.id, { await loadLibrary({ preserveSelection: true })
stateCode: 4,
state: '归纳失败',
stateTone: 'danger'
})
toast(error.message || 'Hermes 归纳文档失败。') toast(error.message || 'Hermes 归纳文档失败。')
} finally { } finally {
ingestingId.value = '' ingestingId.value = ''
@@ -654,6 +722,7 @@ export default {
revokePreviewBlob() revokePreviewBlob()
destroyOnlyOfficeEditor() destroyOnlyOfficeEditor()
setBodyScrollLocked(false) setBodyScrollLocked(false)
stopLibraryPolling()
window.removeEventListener('keydown', handleWindowKeydown) window.removeEventListener('keydown', handleWindowKeydown)
}) })
@@ -711,6 +780,8 @@ export default {
selectedDocument, selectedDocument,
resolveIngestActionLabel, resolveIngestActionLabel,
resolveIngestActionTitle, resolveIngestActionTitle,
resolveLlmWikiQualityLabel,
resolveLlmWikiQualityTone,
resolveViewLlmWikiTitle, resolveViewLlmWikiTitle,
saveLlmWikiSummary, saveLlmWikiSummary,
totalCount, totalCount,