feat: 完善知识库、策略预览与OnlyOffice集成

## 配置与环境
- .env.example: 更新环境变量配置
- docker-compose.yml: 完善Docker编排配置
- docker/README.md: 更新Docker文档

## 后端知识库模块
- endpoints/knowledge.py: 增强知识库API端点
- schemas/knowledge.py: 扩展知识库数据模型
- services/knowledge.py: 完善知识库业务逻辑
- config.py: 优化配置管理
- storage/knowledge/.index.json: 更新知识库索引

## 前端功能
- api.js: 完善API服务层
- knowledge.js: 优化知识库服务
- onlyoffice.js: 新增OnlyOffice文档服务集成
- TopBar.vue: 优化顶部导航栏
- PoliciesView.vue: 完善策略视图
- AppShellRouteView.vue: 新增应用外壳路由视图
- views/scripts/PoliciesView.js: 优化策略脚本
- policiesPreviewFormatters.js: 新增策略预览格式化工具

## 样式
- policies-view.css: 完善策略页样式

## 测试
- api-request.test.mjs: API请求测试
- onlyoffice-service.test.mjs: OnlyOffice服务测试
- policies-preview-formatters.test.mjs: 策略预览格式化测试
This commit is contained in:
caoxiaozhu
2026-05-09 04:25:30 +00:00
parent 619281afc3
commit d9ffa9ce2c
21 changed files with 1469 additions and 508 deletions

View File

@@ -8,19 +8,14 @@
<h2>文档库 / 文件夹</h2>
<p>默认展示文件列表点击具体文件后可在右侧展开预览</p>
</div>
<span class="preview-hint" :class="{ active: selectedDocument }">
{{ selectedDocument ? '预览已展开' : '点击文件可预览' }}
</span>
<label class="file-search">
<i class="mdi mdi-magnify"></i>
<input v-model="documentSearch" type="search" placeholder="搜索当前文件夹内文件" />
</label>
</header>
<div class="library-body">
<aside class="folder-rail">
<label class="folder-search">
<i class="mdi mdi-magnify"></i>
<input v-model="folderSearch" type="search" placeholder="搜索文件夹" />
<button type="button" aria-label="新增文件夹"><i class="mdi mdi-plus"></i></button>
</label>
<nav class="folder-tree" aria-label="知识库文件夹">
<button
v-for="folder in filteredFolders"
@@ -35,17 +30,30 @@
</button>
</nav>
<button class="new-folder-btn" type="button">
<i class="mdi mdi-plus"></i>
<span>新建文件夹</span>
<button class="new-folder-btn fixed" type="button" disabled>
<i class="mdi mdi-lock-outline"></i>
<span>固定文件夹</span>
</button>
</aside>
<section class="document-area">
<div class="upload-zone">
<div
class="upload-zone"
:class="{ disabled: !isAdmin, busy: uploading }"
@click="triggerUpload"
@dragover.prevent
@drop.prevent="handleDrop"
>
<input
ref="uploadInput"
class="upload-input"
type="file"
multiple
@change="handleFileInput"
/>
<i class="mdi mdi-cloud-upload"></i>
<strong>拖拽文档到此处或点击上传</strong>
<span>支持 PDF / Word / Excel / PPT 文档单个文件不超过 100MB</span>
<strong>{{ isAdmin ? (uploading ? '正在上传文件...' : '拖拽文档到此处,或点击上传') : '知识文件只读查阅' }}</strong>
<span>{{ uploadHint }}</span>
</div>
<div class="doc-table-wrap">
@@ -64,10 +72,10 @@
<tbody>
<tr
v-for="doc in visibleDocuments"
:key="doc.name"
:key="doc.id"
class="doc-row"
:class="{ selected: selectedDocument?.name === doc.name }"
@click="selectedDocument = doc"
:class="{ selected: selectedDocument?.id === doc.id }"
@click="selectDocument(doc.id)"
>
<td>
<span class="file-name">
@@ -83,9 +91,26 @@
<td><span class="state-tag" :class="doc.stateTone">{{ doc.state }}</span></td>
<td>{{ doc.owner }}</td>
<td>
<button class="more-btn" type="button" aria-label="更多操作" @click.stop>
<i class="mdi mdi-dots-horizontal"></i>
</button>
<div class="row-actions" @click.stop>
<button class="more-btn" type="button" aria-label="下载文件" @click="handleDownload(doc)">
<i class="mdi mdi-download"></i>
</button>
<button
v-if="isAdmin"
class="more-btn danger"
type="button"
:disabled="deletingId === doc.id"
aria-label="删除文件"
@click="handleDelete(doc)"
>
<i class="mdi mdi-delete-outline"></i>
</button>
</div>
</td>
</tr>
<tr v-if="!visibleDocuments.length">
<td colspan="7" class="empty-row">
{{ loading ? '正在加载知识库文件...' : '当前文件夹暂无文件' }}
</td>
</tr>
</tbody>
@@ -93,17 +118,6 @@
</div>
<footer class="list-foot">
<template v-if="false">
<span> {{ filteredDocuments.length }} </span>
<button type="button">10/ <i class="mdi mdi-chevron-down"></i></button>
<div class="pager" aria-label="分页">
<button type="button" aria-label="上一页"><i class="mdi mdi-chevron-left"></i></button>
<button class="active" type="button" aria-current="page">1</button>
<button type="button">2</button>
<button type="button" aria-label="下一页"><i class="mdi mdi-chevron-right"></i></button>
</div>
<label>前往 <input value="1" aria-label="页码" /> </label>
</template>
<span class="page-summary"> {{ totalCount }} 目前第 {{ currentPage }} </span>
<div class="pager" aria-label="分页">
<button class="page-nav" type="button" :disabled="currentPage === 1" aria-label="上一页" @click="currentPage--">
@@ -152,67 +166,78 @@
<aside v-if="selectedDocument" class="preview-column">
<article class="preview-panel panel">
<header class="preview-head">
<div>
<span class="preview-kicker">文件预览</span>
<div class="preview-copy">
<h2>{{ selectedDocument.name }}</h2>
<p>{{ selectedDocument.summary }}</p>
<p class="preview-summary-line">
<span v-for="part in previewMetaLine" :key="part">{{ part }}</span>
</p>
<div v-if="previewSecondaryMetaLine.length" class="preview-secondary-line">
<span v-for="part in previewSecondaryMetaLine" :key="part">{{ part }}</span>
</div>
</div>
<div class="preview-actions">
<button type="button" class="mini-action">
<button type="button" class="mini-action" @click="handleDownload(selectedDocument)">
<i class="mdi mdi-download"></i>
<span>下载</span>
</button>
<button type="button" class="icon-action" aria-label="关闭预览" @click="selectedDocument = null">
<button type="button" class="icon-action" aria-label="关闭预览" @click="closePreview">
<i class="mdi mdi-close"></i>
</button>
</div>
</header>
<div class="preview-meta">
<span><i class="mdi mdi-tag-outline"></i>{{ selectedDocument.tag }}</span>
<span><i class="mdi mdi-history"></i>{{ selectedDocument.time }}</span>
<span><i class="mdi mdi-account-circle-outline"></i>{{ selectedDocument.owner }}</span>
<span><i class="mdi mdi-source-branch"></i>{{ selectedDocument.version }}</span>
</div>
<div class="preview-viewer">
<div class="viewer-toolbar">
<div class="viewer-filetype" :class="selectedDocument.fileType">
<i :class="selectedDocument.icon"></i>
<span>{{ selectedDocument.fileTypeLabel }}</span>
</div>
<div class="viewer-toolbar-actions">
<button type="button"><i class="mdi mdi-magnify-minus-outline"></i></button>
<button type="button"><i class="mdi mdi-magnify-plus-outline"></i></button>
<button type="button"><i class="mdi mdi-fit-to-page-outline"></i></button>
</div>
<div v-if="previewLoading" class="preview-status">正在加载预览...</div>
<div v-else-if="previewError" class="preview-status error">{{ previewError }}</div>
<div v-else-if="selectedDocument.previewKind === 'pdf' && previewBlobUrl" class="preview-embed-wrap">
<iframe :src="previewBlobUrl" class="preview-embed" title="PDF 预览"></iframe>
</div>
<div class="page-stage">
<div v-else-if="selectedDocument.previewKind === 'image' && previewBlobUrl" class="preview-image-wrap">
<img :src="previewBlobUrl" :alt="selectedDocument.name" class="preview-image" />
</div>
<div v-else-if="shouldUseOnlyOffice" class="onlyoffice-preview-wrap">
<div v-if="onlyOfficeLoading" class="preview-status">正在加载 ONLYOFFICE 预览...</div>
<div v-else-if="onlyOfficeError" class="preview-status error">{{ onlyOfficeError }}</div>
<div v-else :id="onlyOfficeHostId" class="onlyoffice-preview-host"></div>
</div>
<div v-else-if="selectedDocument.previewKind === 'table'" class="excel-preview-wrap">
<div v-if="selectedDocument.previewPages.length > 1" class="excel-sheet-tabs" role="tablist" aria-label="Excel 工作表页签">
<button
v-for="(page, index) in selectedDocument.previewPages"
:key="`${selectedDocument.id}-sheet-${index}`"
type="button"
class="excel-sheet-tab"
:class="{ active: currentPreviewPageIndex === index }"
:aria-selected="currentPreviewPageIndex === index"
@click="selectPreviewPage(index)"
>
{{ page.title }}
</button>
</div>
<div v-if="excelPreviewTable.headers.length" class="excel-preview-scroll">
<table class="excel-preview-table">
<thead>
<tr>
<th v-for="header in excelPreviewTable.headers" :key="header">{{ header }}</th>
</tr>
</thead>
<tbody>
<tr v-for="(row, rowIndex) in excelPreviewTable.rows" :key="`row-${rowIndex}`">
<td v-for="(cell, cellIndex) in row" :key="`cell-${rowIndex}-${cellIndex}`">{{ cell }}</td>
</tr>
</tbody>
</table>
</div>
<div v-else class="preview-status">当前表格暂未提取到可展示内容</div>
</div>
<div v-else class="page-stage">
<article
v-for="(page, index) in selectedDocument.previewPages"
:key="`${selectedDocument.name}-${index}`"
:key="`${selectedDocument.id}-${index}`"
class="page-sheet"
:style="{ '--page-delay': `${index * 70}ms` }"
>
<header class="page-title">
<div>
<strong>{{ page.title }}</strong>
<span>{{ page.subtitle }}</span>
</div>
<b> {{ index + 1 }} </b>
</header>
<section class="page-summary">
<div class="summary-grid">
<div v-for="item in page.stats" :key="item.label" class="summary-item">
<span>{{ item.label }}</span>
<strong>{{ item.value }}</strong>
</div>
</div>
</section>
<section class="page-content">
<div v-for="block in page.blocks" :key="block.heading" class="content-block">
<h3>{{ block.heading }}</h3>
@@ -222,6 +247,9 @@
</div>
</section>
</article>
<div v-if="!selectedDocument.previewPages.length" class="preview-status">
当前文件暂未生成结构化预览请下载后查看
</div>
</div>
</div>
</article>