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

@@ -16,25 +16,28 @@
'overview-main': activeView === 'overview',
'workbench-main': activeView === 'workbench',
'requests-main': activeView === 'requests',
'approval-main': activeView === 'approval',
'approval-main': activeView === 'approval',
'policies-main': activeView === 'policies',
'audit-main': activeView === 'audit',
'audit-detail-main': activeView === 'audit' && auditDetailOpen,
'logs-main': activeView === 'logs',
'employees-main': activeView === 'employees',
'settings-main': activeView === 'settings'
}"
>
<TopBar
v-if="activeView !== 'settings' && !(activeView === 'audit' && auditDetailOpen)"
:current-view="topBarView"
:search="search"
:active-view="activeView"
:ranges="ranges"
:current-view="topBarView"
:search="search"
:active-view="activeView"
:ranges="ranges"
:active-range="activeRange"
:employee-summary="employeeSummary"
:knowledge-summary="knowledgeSummary"
:logs-summary="logsSummary"
:request-summary="requestSummary"
:detail-mode="detailMode"
:log-detail-mode="logDetailMode"
:detail-alerts="detailAlerts"
:custom-range="customRange"
@update:search="search = $event"
@@ -45,7 +48,7 @@
/>
<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'"
:filters="filters"
:ranges="ranges"
@@ -59,10 +62,11 @@
'requests-workarea': activeView === 'requests',
'approval-workarea': activeView === 'approval',
'policies-workarea': activeView === 'policies',
'audit-workarea': activeView === 'audit',
'employees-workarea': activeView === 'employees',
'settings-workarea': activeView === 'settings'
}"
'audit-workarea': activeView === 'audit',
'logs-workarea': activeView === 'logs',
'employees-workarea': activeView === 'employees',
'settings-workarea': activeView === 'settings'
}"
>
<OverviewView
v-if="activeView === 'overview'"
@@ -99,11 +103,13 @@
@create-request="openTravelCreate"
/>
<ApprovalCenterView v-else-if="activeView === 'approval'" />
<PoliciesView v-else-if="activeView === 'policies'" @summary-change="knowledgeSummary = $event" />
<ApprovalCenterView v-else-if="activeView === 'approval'" />
<PoliciesView v-else-if="activeView === 'policies'" @summary-change="knowledgeSummary = $event" />
<AuditView v-else-if="activeView === 'audit'" @detail-open-change="auditDetailOpen = $event" />
<EmployeeManagementView v-else-if="activeView === 'employees'" @overview-change="employeeSummary = $event" />
<SettingsView v-else />
<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" />
<SettingsView v-else />
</section>
</main>
@@ -133,10 +139,12 @@ import TravelReimbursementCreateView from './TravelReimbursementCreateView.vue'
import TravelRequestDetailView from './TravelRequestDetailView.vue'
import RequestsView from './RequestsView.vue'
import ApprovalCenterView from './ApprovalCenterView.vue'
import PoliciesView from './PoliciesView.vue'
import AuditView from './AuditView.vue'
import EmployeeManagementView from './EmployeeManagementView.vue'
import SettingsView from './SettingsView.vue'
import PoliciesView from './PoliciesView.vue'
import AuditView from './AuditView.vue'
import LogsView from './LogsView.vue'
import LogDetailView from './LogDetailView.vue'
import EmployeeManagementView from './EmployeeManagementView.vue'
import SettingsView from './SettingsView.vue'
import { useAppShell } from '../composables/useAppShell.js'
import { useSystemState } from '../composables/useSystemState.js'
@@ -144,6 +152,7 @@ import { filterNavItemsByAccess } from '../utils/accessControl.js'
const employeeSummary = ref(null)
const knowledgeSummary = ref(null)
const logsSummary = ref(null)
const auditDetailOpen = ref(false)
const {
@@ -154,6 +163,7 @@ const {
customRange,
detailAlerts,
detailMode,
logDetailMode,
filteredRequests,
filters,
handleApprove,

View File

@@ -97,7 +97,7 @@
v-if="isAdmin"
class="more-btn ingest"
type="button"
:disabled="Boolean(ingestingId) || deletingId === doc.id"
:disabled="Boolean(ingestingId) || deletingId === doc.id || Number(doc.stateCode || 0) === 2"
:aria-label="resolveIngestActionTitle(doc)"
:title="resolveIngestActionTitle(doc)"
@click="handleManualIngest(doc)"
@@ -356,6 +356,21 @@
<div v-else-if="llmWikiError" class="preview-status error">{{ llmWikiError }}</div>
<div v-else-if="llmWikiDocument" class="llm-wiki-grid">
<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>
<h3>知识总结</h3>
@@ -363,6 +378,12 @@
</div>
<span class="llm-wiki-count">{{ llmWikiDocument.knowledge_candidate_count }} 条知识</span>
</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
v-model="llmWikiSummaryDraft"
class="llm-wiki-editor"

View File

@@ -31,6 +31,8 @@ import { resolveKnowledgePreviewLayoutState } from './knowledgePreviewLayout.js'
import { resolveInitialKnowledgeFolder } from './knowledgeFolderSelection.js'
import { buildOnlyOfficePreviewConfig } from './onlyOfficePreviewConfig.js'
const KNOWLEDGE_POLL_INTERVAL_MS = 5000
function triggerFileDownload(blob, filename) {
const url = URL.createObjectURL(blob)
const anchor = document.createElement('a')
@@ -42,6 +44,7 @@ function triggerFileDownload(blob, filename) {
let bodyOverflowSnapshot = ''
let bodyOverscrollBehaviorSnapshot = ''
let libraryPollTimer = 0
function setBodyScrollLocked(isLocked) {
if (typeof document === 'undefined') {
@@ -270,20 +273,26 @@ export default {
}
}
async function loadLibrary(options = {}) {
loading.value = true
try {
const payload = await fetchKnowledgeLibrary()
folders.value = payload.folders || []
documents.value = payload.documents || []
emit('summary-change', { totalDocuments: documents.value.length })
async function loadLibrary(options = {}) {
loading.value = true
try {
const payload = await fetchKnowledgeLibrary()
folders.value = payload.folders || []
documents.value = payload.documents || []
emit('summary-change', { totalDocuments: documents.value.length })
activeFolder.value = resolveInitialKnowledgeFolder(folders.value, activeFolder.value)
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) {
closePreview()
} else {
selectedDocument.value = {
...selectedDocument.value,
...nextDocument
}
}
}
if (options.preserveSelection && llmWikiDocument.value?.document_id) {
@@ -292,11 +301,12 @@ export default {
closeLlmWikiSummary()
}
}
syncLibraryPolling()
} catch (error) {
emit('summary-change', { totalDocuments: 0 })
toast(error.message || '知识库加载失败。')
} finally {
loading.value = false
emit('summary-change', { totalDocuments: 0 })
toast(error.message || '知识库加载失败。')
} finally {
loading.value = false
}
}
@@ -351,10 +361,37 @@ export default {
...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) {
if (ingestingId.value === document.id) {
if (ingestingId.value === document.id || Number(document?.stateCode || 0) === 2) {
return '归纳中'
}
return Number(document?.stateCode || 0) === 3 ? '重新归纳' : '归纳'
@@ -372,27 +409,64 @@ export default {
}
function canViewLlmWiki(document) {
return isAdmin.value && Number(document?.stateCode || 0) === 3
return isAdmin.value && Boolean(document?.llmWikiAvailable)
}
function resolveViewLlmWikiTitle(document) {
if (!isAdmin.value) {
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) {
return 'Hermes 正在归纳当前文档,完成后可查看 LLM Wiki 知识总结'
}
if (Number(document?.stateCode || 0) === 4) {
return '当前文档上次归纳失败,请重新归纳后再查看'
return '当前文档上次归纳失败,且没有可查看的 LLM Wiki 产物'
}
if (Number(document?.stateCode || 0) !== 3) {
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) {
if (!isAdmin.value || ingestingId.value || !document?.id) {
if (!isAdmin.value || ingestingId.value || !document?.id || Number(document?.stateCode || 0) === 2) {
return
}
@@ -406,19 +480,13 @@ export default {
try {
const payload = await syncKnowledgeDocumentToLlmWiki({
folder: document.folder,
documentId: document.id
documentId: document.id,
force: true
})
await loadLibrary({ preserveSelection: true })
if (selectedDocument.value?.id === document.id) {
await selectDocument(document.id)
}
toast(payload.summary || 'Hermes 已完成文档归纳。')
toast(payload.summary || 'Hermes 已进入后台归纳。')
} catch (error) {
patchDocumentState(document.id, {
stateCode: 4,
state: '归纳失败',
stateTone: 'danger'
})
await loadLibrary({ preserveSelection: true })
toast(error.message || 'Hermes 归纳文档失败。')
} finally {
ingestingId.value = ''
@@ -654,6 +722,7 @@ export default {
revokePreviewBlob()
destroyOnlyOfficeEditor()
setBodyScrollLocked(false)
stopLibraryPolling()
window.removeEventListener('keydown', handleWindowKeydown)
})
@@ -711,6 +780,8 @@ export default {
selectedDocument,
resolveIngestActionLabel,
resolveIngestActionTitle,
resolveLlmWikiQualityLabel,
resolveLlmWikiQualityTone,
resolveViewLlmWikiTitle,
saveLlmWikiSummary,
totalCount,