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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user