324 lines
7.4 KiB
Vue
324 lines
7.4 KiB
Vue
<script setup lang="ts">
|
||
import { computed, onMounted, reactive, ref } from 'vue'
|
||
import { ElMessage } from 'element-plus'
|
||
|
||
import NewSigningTaskDialog from '@/components/signing/NewSigningTaskDialog.vue'
|
||
import type { SigningTemplate } from '@/api/workbench/types'
|
||
|
||
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 signingTemplate = ref<SigningTemplate | null>(null)
|
||
const signingDialogVisible = ref(false)
|
||
|
||
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 toSigningTemplate(template: DocumentTemplate): SigningTemplate {
|
||
return {
|
||
id: template.id,
|
||
versionId: template.id,
|
||
name: template.name,
|
||
code: template.code,
|
||
version: template.version,
|
||
department: template.department,
|
||
category: template.category,
|
||
description: template.description,
|
||
supportedMethods: ['pad', 'sms'],
|
||
}
|
||
}
|
||
|
||
function openSigningDialog(template: DocumentTemplate) {
|
||
closePreview()
|
||
signingTemplate.value = toSigningTemplate(template)
|
||
signingDialogVisible.value = true
|
||
}
|
||
|
||
function closeSigningDialog() {
|
||
signingDialogVisible.value = false
|
||
signingTemplate.value = null
|
||
}
|
||
|
||
function clearFilters() {
|
||
filters.keyword = ''
|
||
filters.department = null
|
||
}
|
||
|
||
function useTemplate(template: DocumentTemplate) {
|
||
openSigningDialog(template)
|
||
}
|
||
|
||
onMounted(loadLibrary)
|
||
</script>
|
||
|
||
<template>
|
||
<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="openSigningDialog"
|
||
/>
|
||
</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"
|
||
/>
|
||
<NewSigningTaskDialog
|
||
:visible="signingDialogVisible"
|
||
:initial-template="signingTemplate"
|
||
@close="closeSigningDialog"
|
||
/>
|
||
</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>
|