feat(documents): implement document library management
This commit is contained in:
@@ -1,12 +1,293 @@
|
||||
<script setup lang="ts">
|
||||
import PagePlaceholder from '@/components/PagePlaceholder.vue'
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
import DocumentFilterTabs from './components/DocumentFilterTabs.vue'
|
||||
import DocumentLibraryHeader from './components/DocumentLibraryHeader.vue'
|
||||
import DocumentPreviewDialog from './components/DocumentPreviewDialog.vue'
|
||||
import DocumentTemplateCard from './components/DocumentTemplateCard.vue'
|
||||
import { documentLibrary, documentStatusOptions } from './mock'
|
||||
import type {
|
||||
DepartmentTab,
|
||||
DocumentDepartment,
|
||||
DocumentFilterForm,
|
||||
DocumentTemplate,
|
||||
StatusTab,
|
||||
} from './types'
|
||||
|
||||
const loading = ref(true)
|
||||
const library = ref<DocumentDepartment[]>([])
|
||||
const selectedTemplate = ref<DocumentTemplate | null>(null)
|
||||
|
||||
const filters = reactive<DocumentFilterForm>({
|
||||
keyword: '',
|
||||
department: null,
|
||||
status: 'enabled',
|
||||
})
|
||||
|
||||
const allTemplates = computed<DocumentTemplate[]>(() =>
|
||||
library.value.flatMap((department) =>
|
||||
department.categories.flatMap((category) =>
|
||||
category.documents.map((template) => ({
|
||||
...template,
|
||||
departmentKey: department.key,
|
||||
department: department.name,
|
||||
category: category.name,
|
||||
})),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
const departmentTabs = computed<DepartmentTab[]>(() => [
|
||||
{ key: null, label: '全部', count: allTemplates.value.length },
|
||||
...library.value.map((department) => ({
|
||||
key: department.key,
|
||||
label: department.name,
|
||||
count: allTemplates.value.filter((template) => template.departmentKey === department.key)
|
||||
.length,
|
||||
})),
|
||||
])
|
||||
|
||||
const statusTabs = computed<StatusTab[]>(() =>
|
||||
documentStatusOptions.map((option) => ({
|
||||
...option,
|
||||
count: allTemplates.value.filter((template) => template.status === option.key).length,
|
||||
})),
|
||||
)
|
||||
|
||||
const filteredTemplates = computed(() => {
|
||||
const keyword = filters.keyword.trim().toLowerCase()
|
||||
|
||||
return allTemplates.value.filter((template) => {
|
||||
const matchesDepartment = !filters.department || template.departmentKey === filters.department
|
||||
const matchesStatus = template.status === filters.status
|
||||
const matchesKeyword =
|
||||
!keyword ||
|
||||
[template.name, template.code, template.department].join(' ').toLowerCase().includes(keyword)
|
||||
|
||||
return matchesDepartment && matchesStatus && matchesKeyword
|
||||
})
|
||||
})
|
||||
|
||||
async function loadLibrary() {
|
||||
loading.value = true
|
||||
await Promise.resolve()
|
||||
library.value = documentLibrary
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
function handlePreview(template: DocumentTemplate) {
|
||||
selectedTemplate.value = template
|
||||
}
|
||||
|
||||
function closePreview() {
|
||||
selectedTemplate.value = null
|
||||
}
|
||||
|
||||
function showAddMessage() {
|
||||
ElMessage.info('原型演示:新增文档模板(类 Word 在线编辑器)')
|
||||
}
|
||||
|
||||
function showExportMessage() {
|
||||
ElMessage.info('原型演示:导出模板包(ZIP)')
|
||||
}
|
||||
|
||||
function showEditMessage(template: DocumentTemplate) {
|
||||
ElMessage.info('原型演示:编辑模板《' + template.name + '》')
|
||||
}
|
||||
|
||||
function showInitiateMessage(template: DocumentTemplate) {
|
||||
ElMessage.info('已选择《' + template.name + '》,签署工作台暂未实现')
|
||||
}
|
||||
|
||||
function clearFilters() {
|
||||
filters.keyword = ''
|
||||
filters.department = null
|
||||
}
|
||||
|
||||
function useTemplate(template: DocumentTemplate) {
|
||||
closePreview()
|
||||
ElMessage.info('原型演示:从模板《' + template.name + '》发起签署')
|
||||
}
|
||||
|
||||
onMounted(loadLibrary)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PagePlaceholder
|
||||
group="管理"
|
||||
title="文档库管理"
|
||||
icon="▤"
|
||||
description="文档库管理页面占位,待根据 Web 端需求实现。"
|
||||
/>
|
||||
<section class="documents-page">
|
||||
<DocumentLibraryHeader
|
||||
v-model="filters.keyword"
|
||||
:total="filteredTemplates.length"
|
||||
@add="showAddMessage"
|
||||
@export="showExportMessage"
|
||||
/>
|
||||
|
||||
<DocumentFilterTabs
|
||||
v-model:department="filters.department"
|
||||
v-model:status="filters.status"
|
||||
:departments="departmentTabs"
|
||||
:statuses="statusTabs"
|
||||
/>
|
||||
|
||||
<div v-if="loading" class="template-grid" aria-label="正在加载文档模板">
|
||||
<div v-for="index in 8" :key="index" class="template-skeleton">
|
||||
<span class="skeleton-title" />
|
||||
<span class="skeleton-line wide" />
|
||||
<span class="skeleton-line" />
|
||||
<span class="skeleton-line short" />
|
||||
<span class="skeleton-footer" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="filteredTemplates.length" class="template-grid">
|
||||
<DocumentTemplateCard
|
||||
v-for="template in filteredTemplates"
|
||||
:key="template.id"
|
||||
:template="template"
|
||||
@preview="handlePreview"
|
||||
@edit="showEditMessage"
|
||||
@initiate="showInitiateMessage"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-else class="empty-state">
|
||||
<div class="empty-icon" aria-hidden="true">⌕</div>
|
||||
<p>没有符合条件的模板</p>
|
||||
<button
|
||||
v-if="filters.keyword || filters.department"
|
||||
type="button"
|
||||
class="reset-button"
|
||||
@click="clearFilters"
|
||||
>
|
||||
清除筛选
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<DocumentPreviewDialog
|
||||
:visible="Boolean(selectedTemplate)"
|
||||
:template="selectedTemplate"
|
||||
@close="closePreview"
|
||||
@use="useTemplate"
|
||||
/>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.documents-page {
|
||||
width: 100%;
|
||||
max-width: 1440px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.template-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.template-skeleton {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
min-height: 206px;
|
||||
padding: 16px;
|
||||
background: var(--card);
|
||||
border-radius: var(--r);
|
||||
box-shadow: var(--sh);
|
||||
}
|
||||
|
||||
.template-skeleton span {
|
||||
display: block;
|
||||
background: linear-gradient(90deg, #edf3f5 25%, #f7fafb 37%, #edf3f5 63%);
|
||||
background-size: 400% 100%;
|
||||
border-radius: 6px;
|
||||
animation: skeleton-shimmer 1.4s ease infinite;
|
||||
}
|
||||
|
||||
.skeleton-title {
|
||||
width: 70%;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.skeleton-line {
|
||||
width: 90%;
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
.skeleton-line.wide {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.skeleton-line.short {
|
||||
width: 62%;
|
||||
}
|
||||
|
||||
.skeleton-footer {
|
||||
width: 100%;
|
||||
height: 26px;
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
@keyframes skeleton-shimmer {
|
||||
0% {
|
||||
background-position: 100% 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
background-position: -100% 0;
|
||||
}
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
padding: 56px 24px;
|
||||
color: var(--mut);
|
||||
text-align: center;
|
||||
background: var(--card);
|
||||
border-radius: var(--r);
|
||||
box-shadow: var(--sh);
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
margin-bottom: 8px;
|
||||
color: var(--brand);
|
||||
font-size: 38px;
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
.empty-state p {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.reset-button {
|
||||
padding: 6px 12px;
|
||||
margin-top: 14px;
|
||||
color: var(--brand);
|
||||
font-size: 12px;
|
||||
background: #fff;
|
||||
border: 1px solid var(--brand);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.reset-button:hover {
|
||||
background: var(--brand-l);
|
||||
}
|
||||
|
||||
@media (max-width: 1400px) {
|
||||
.template-grid {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1100px) {
|
||||
.template-grid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 680px) {
|
||||
.template-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user