Files
xh-medical-sign-web/clinical-web/src/views/management/documents/index.vue

324 lines
7.4 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>