档案管理功能
This commit is contained in:
parent
6c17051046
commit
6b33583f74
|
|
@ -93,7 +93,7 @@ export function getArchivalCatalogueByIdApi(params) {
|
|||
export function getFileManageApi(params) {
|
||||
return request({
|
||||
url: '/blade-system/fileManage/getFileManage',
|
||||
method: 'get',
|
||||
method: 'POST',
|
||||
data:params,
|
||||
})
|
||||
}
|
||||
|
|
@ -102,7 +102,7 @@ export function getFileManageApi(params) {
|
|||
export function geMaxSortApi(params) {
|
||||
return request({
|
||||
url: '/blade-system/fileManage/getMaxSort',
|
||||
method: 'get',
|
||||
method: 'POST',
|
||||
data:params,
|
||||
})
|
||||
}
|
||||
|
|
@ -111,7 +111,7 @@ export function geMaxSortApi(params) {
|
|||
export function getFileManageByIdApi(params) {
|
||||
return request({
|
||||
url: '/blade-system/fileManage/getFileManageById',
|
||||
method: 'get',
|
||||
method: 'POST',
|
||||
data:params,
|
||||
})
|
||||
}
|
||||
|
|
@ -138,7 +138,7 @@ export function fileExtractApi(data) {
|
|||
export function getFileAsBase64Api(params) {
|
||||
return request({
|
||||
url: '/blade-system/fileManage/getFileAsBase64',
|
||||
method: 'get',
|
||||
method: 'POST',
|
||||
data:params,
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ export async function getClassifyMarkSelApi(params) {
|
|||
return await request({
|
||||
url: '/blade-system/archive/getFilesClassifyMarkSelect',
|
||||
method: 'post',
|
||||
data: params
|
||||
data: {}
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -26,3 +26,12 @@ export async function getRoleSelectApi(params) {
|
|||
data: params
|
||||
});
|
||||
}
|
||||
|
||||
// 根据字典类型获取字典数据
|
||||
export function getDictDataByTypeApi(data) {
|
||||
return request({
|
||||
url: '/blade-system/system/dict/data/type',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -88,5 +88,14 @@ export default [
|
|||
component: () => import(/* webpackChunkName: "views" */ '@/views/system/userinfo.vue'),
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
path: '/archivesManagement/fileData',
|
||||
name: 'FileData',
|
||||
component: () => import('@/views/fileManager/file-data.vue'),
|
||||
meta: {
|
||||
title: '档案数据管理'
|
||||
}
|
||||
}
|
||||
];
|
||||
|
|
|
|||
|
|
@ -0,0 +1,508 @@
|
|||
<template>
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
:title="title"
|
||||
:show-close="true"
|
||||
:close-on-click-modal="false"
|
||||
@close="handleClose"
|
||||
:append-to-body="true"
|
||||
width="600px"
|
||||
>
|
||||
<div>
|
||||
<el-form :model="form" :rules="rules" ref="ruleFormRef" label-width="110px">
|
||||
<el-form-item label="所属案卷">
|
||||
<el-input type="textarea" class="form-item" :value="belongName" :disabled="true"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="档案名称" prop="contentName">
|
||||
<el-input
|
||||
type="textarea"
|
||||
class="form-item"
|
||||
v-model="form.contentName"
|
||||
clearable
|
||||
show-word-limit
|
||||
placeholder="请输入档案名称"
|
||||
maxlength="64"
|
||||
:disabled="isDetailMode"
|
||||
></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="案卷期限" prop="term">
|
||||
<el-input
|
||||
class="form-item"
|
||||
v-model="form.term"
|
||||
clearable
|
||||
show-word-limit
|
||||
placeholder="请输入案卷期限"
|
||||
maxlength="32"
|
||||
:disabled="isDetailMode"
|
||||
></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="归档责任单位" prop="unitName">
|
||||
<el-input
|
||||
class="form-item"
|
||||
v-model="form.unitName"
|
||||
clearable
|
||||
show-word-limit
|
||||
placeholder="请输入归档责任单位"
|
||||
maxlength="32"
|
||||
:disabled="isDetailMode"
|
||||
></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="所属专业" prop="major">
|
||||
<el-input
|
||||
class="form-item"
|
||||
v-model="form.major"
|
||||
clearable
|
||||
show-word-limit
|
||||
placeholder="请输入所属专业"
|
||||
maxlength="32"
|
||||
:disabled="isDetailMode"
|
||||
></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="档案标识代码" prop="markCode">
|
||||
<el-select
|
||||
class="form-item"
|
||||
v-model="form.markCode"
|
||||
:disabled="isDetailMode"
|
||||
filterable
|
||||
clearable
|
||||
placeholder="请选择档案标识代码"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in dictData.mark_code"
|
||||
:key="item.id"
|
||||
:label="item.dictLabel"
|
||||
:value="item.dictValue"
|
||||
></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="文件分类标记" prop="classifyMark">
|
||||
<el-select
|
||||
class="form-item"
|
||||
v-model="form.classifyMark"
|
||||
:disabled="isDetailMode"
|
||||
filterable
|
||||
clearable
|
||||
placeholder="请选择文件分类标记"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in classifyMarkList"
|
||||
:key="item.id"
|
||||
:label="item.name"
|
||||
:value="item.id"
|
||||
></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="附件上传" v-if="!isDetailMode" prop="fileList">
|
||||
<el-upload
|
||||
ref="uploadRef"
|
||||
action="#"
|
||||
:file-list="fileList"
|
||||
:on-remove="handleRemove"
|
||||
:on-change="handleFileChange"
|
||||
:before-upload="beforeUpload"
|
||||
:auto-upload="false"
|
||||
:limit="1"
|
||||
accept=".pdf,.jpg,.jpeg,.png"
|
||||
>
|
||||
<el-button size="small" type="primary" icon="Upload" :disabled="fileList.length > 0">选择文件</el-button>
|
||||
<template #tip>
|
||||
<div class="el-upload__tip">
|
||||
<span style="color: #f00;">
|
||||
只能上传PDF和图片文件,且不超过{{ maxFileTips }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-upload>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 详情模式下显示附件信息 -->
|
||||
<el-form-item label="附件" v-if="isDetailMode && fileList.length > 0">
|
||||
<div class="file-info">
|
||||
<el-icon><Document /></el-icon>
|
||||
<span>{{ fileList[0].name }}</span>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<template #footer v-if="!isDetailMode">
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="handleClose">取消</el-button>
|
||||
<el-button type="primary" @click="submitForm">确认</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted, computed, nextTick } from 'vue'
|
||||
import { ElMessage, ElLoading } from 'element-plus'
|
||||
import { Upload, Document } from '@element-plus/icons-vue'
|
||||
import {
|
||||
addFileManageRightApi,
|
||||
updateFileManageRightApi,
|
||||
getFileManageByIdApi
|
||||
} from '@/api/archivesManagement/fileManager/fileManager.js'
|
||||
import { getClassifyMarkSelApi ,getDictDataByTypeApi} from '@/api/select.js'
|
||||
|
||||
const props = defineProps({
|
||||
width: {
|
||||
type: Number,
|
||||
default: 600
|
||||
},
|
||||
dataForm: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
isAdd: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
rowData: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
projectId: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['closeDialog', 'showColose', 'handleQuery'])
|
||||
|
||||
// 响应式数据
|
||||
const dialogVisible = ref(true)
|
||||
const form = reactive({
|
||||
contentName: null,
|
||||
term: null,
|
||||
unitName: null,
|
||||
major: null,
|
||||
markCode: null,
|
||||
classifyMark: null,
|
||||
})
|
||||
const belongName = ref('')
|
||||
const classifyMarkList = ref([])
|
||||
const fileList = ref([])
|
||||
const maxFileSize = ref(10 * 1024 * 1024)
|
||||
const maxFileTips = ref('10MB')
|
||||
const ruleFormRef = ref()
|
||||
const uploadRef = ref()
|
||||
|
||||
// 字典数据(需要根据实际情况获取)
|
||||
const dictData = reactive({
|
||||
mark_code: [],
|
||||
file_size_limit: []
|
||||
})
|
||||
|
||||
// 添加获取字典数据的方法
|
||||
const fetchDictData = async () => {
|
||||
try {
|
||||
// 获取档案标识代码字典
|
||||
const markCodeRes = await getDictDataByTypeApi({ dictType: 'mark_code' })
|
||||
if (markCodeRes.data.code === 200) {
|
||||
dictData.mark_code = markCodeRes.data.data || []
|
||||
} else {
|
||||
console.error('获取档案标识代码字典失败:', markCodeRes.data.msg)
|
||||
}
|
||||
|
||||
// 获取文件大小限制字典
|
||||
const fileSizeRes = await getDictDataByTypeApi({ dictType: 'file_size_limit' })
|
||||
if (fileSizeRes.data.code === 200) {
|
||||
dictData.file_size_limit = fileSizeRes.data.data || []
|
||||
|
||||
// 如果有文件大小限制字典,可以设置最大文件大小
|
||||
if (fileSizeRes.data.data && fileSizeRes.data.data.length > 0) {
|
||||
// 假设字典中存储的是类似 "10" 这样的数字(单位MB)
|
||||
const sizeItem = fileSizeRes.data.data[0]
|
||||
const sizeValue = parseInt(sizeItem.dictValue)
|
||||
if (!isNaN(sizeValue)) {
|
||||
maxFileSize.value = sizeValue * 1024 * 1024 // 转换为字节
|
||||
maxFileTips.value = sizeValue + 'MB'
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.error('获取文件大小限制字典失败:', fileSizeRes.data.msg)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取字典数据失败:', error)
|
||||
// 使用默认值
|
||||
dictData.mark_code = []
|
||||
dictData.file_size_limit = []
|
||||
}
|
||||
}
|
||||
|
||||
// 计算属性
|
||||
const isDetailMode = computed(() => {
|
||||
return props.isAdd === 'detail'
|
||||
})
|
||||
|
||||
// 验证规则
|
||||
const rules = reactive({
|
||||
contentName: [
|
||||
{ required: true, message: '档案名称不能为空', trigger: 'blur' }
|
||||
],
|
||||
fileList: [
|
||||
{
|
||||
required: true,
|
||||
validator: (rule, value, callback) => {
|
||||
if (!Array.isArray(fileList.value) || fileList.value.length === 0) {
|
||||
callback(new Error('请上传附件文件'))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
},
|
||||
trigger: 'change'
|
||||
}
|
||||
],
|
||||
term: [
|
||||
{ required: true, message: '案卷期限不能为空', trigger: 'blur' }
|
||||
],
|
||||
unitName: [
|
||||
{ required: true, message: '归档责任单位不能为空', trigger: 'blur' }
|
||||
],
|
||||
major: [
|
||||
{ required: true, message: '所属专业不能为空', trigger: 'blur' }
|
||||
],
|
||||
markCode: [
|
||||
{ required: true, message: '请选择档案标识代码', trigger: 'change' }
|
||||
],
|
||||
classifyMark: [
|
||||
{ required: true, message: '请选择文件分类标记', trigger: 'change' }
|
||||
],
|
||||
})
|
||||
|
||||
const errorObj = reactive({
|
||||
contentName: '档案名称',
|
||||
term: '案卷期限',
|
||||
unitName: '归档责任单位',
|
||||
major: '所属专业',
|
||||
})
|
||||
|
||||
// 方法
|
||||
const initFormData = async () => {
|
||||
// 先获取字典数据
|
||||
await fetchDictData()
|
||||
|
||||
const res = await getClassifyMarkSelApi()
|
||||
classifyMarkList.value = res.data.data
|
||||
belongName.value = props.rowData.belongName
|
||||
|
||||
if ((props.isAdd === 'edit' || props.isAdd === 'detail') && props.rowData) {
|
||||
const res2 = await getFileManageByIdApi({ id: props.rowData.id, proId: props.projectId })
|
||||
const obj = res2.data.data
|
||||
|
||||
Object.assign(form, {
|
||||
id: obj.id,
|
||||
contentName: obj.contentName || null,
|
||||
term: obj.term || null,
|
||||
major: obj.major || null,
|
||||
unitName: obj.unitName || null,
|
||||
markCode: obj.markCode || null,
|
||||
classifyMark: obj.classifyMark || null,
|
||||
parentId: props.rowData.parentId || null,
|
||||
level: 5,
|
||||
proId: props.projectId
|
||||
})
|
||||
|
||||
fileList.value = [{ name: obj.fileName, businessId: obj.businessId }]
|
||||
} else {
|
||||
const id = props.rowData.id
|
||||
const proId = props.projectId
|
||||
const res = await getFileManageByIdApi({ id, proId })
|
||||
const obj = res.data
|
||||
|
||||
Object.assign(form, {
|
||||
contentName: null,
|
||||
term: obj.term || null,
|
||||
unitName: obj.unitName || null,
|
||||
major: obj.major || null,
|
||||
markCode: obj.markCode || null,
|
||||
classifyMark: obj.classifyMark || null,
|
||||
parentId: props.rowData.id || null,
|
||||
level: 5,
|
||||
proId: props.projectId
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const handleClose = () => {
|
||||
dialogVisible.value = false
|
||||
emit("closeDialog")
|
||||
}
|
||||
|
||||
const beforeUpload = (file) => {
|
||||
if (!(file instanceof File)) {
|
||||
return true
|
||||
}
|
||||
|
||||
const isValidType = checkFileType(file)
|
||||
const isValidSize = checkFileSize(file)
|
||||
|
||||
if (!isValidType) {
|
||||
ElMessage.error('只能上传PDF和图片文件!')
|
||||
return false
|
||||
}
|
||||
|
||||
if (!isValidSize) {
|
||||
ElMessage.error(`文件大小不能超过${maxFileTips.value}!`)
|
||||
return false
|
||||
}
|
||||
|
||||
if (fileList.value.length >= 1) {
|
||||
ElMessage.warning('只能上传一个文件,请先删除现有文件!')
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
const checkFileType = (file) => {
|
||||
const allowedTypes = [
|
||||
'application/pdf',
|
||||
'image/jpeg',
|
||||
'image/jpg',
|
||||
'image/png',
|
||||
]
|
||||
return allowedTypes.includes(file.type)
|
||||
}
|
||||
|
||||
const checkFileSize = (file) => {
|
||||
const maxSize = maxFileSize.value
|
||||
return file.size <= maxSize
|
||||
}
|
||||
|
||||
const handleRemove = (file, fileList) => {
|
||||
if (!(file instanceof File)) {
|
||||
form.businessId = file.businessId
|
||||
}
|
||||
fileList.value = fileList
|
||||
// 移除文件后重新验证
|
||||
if (ruleFormRef.value) {
|
||||
ruleFormRef.value.validateField('fileList')
|
||||
}
|
||||
}
|
||||
|
||||
const handleFileChange = (file, files) => {
|
||||
fileList.value = files
|
||||
if (ruleFormRef.value) {
|
||||
ruleFormRef.value.validateField('fileList')
|
||||
}
|
||||
}
|
||||
|
||||
const submitForm = async () => {
|
||||
if (!ruleFormRef.value) return
|
||||
|
||||
// 先进行表单验证
|
||||
try {
|
||||
await ruleFormRef.value.validate()
|
||||
} catch (error) {
|
||||
console.log('表单验证失败:', error.errorFields)
|
||||
return // 验证失败直接返回,不执行后续代码
|
||||
}
|
||||
|
||||
let loading = null
|
||||
try {
|
||||
loading = ElLoading.service({
|
||||
lock: true,
|
||||
text: "数据提交中,请稍候...",
|
||||
background: 'rgba(0,0,0,0.5)'
|
||||
})
|
||||
|
||||
const formData = new FormData()
|
||||
const params = { ...form }
|
||||
|
||||
if (fileList.value.length > 0) {
|
||||
fileList.value.forEach(file => {
|
||||
if (file.raw instanceof File) {
|
||||
formData.append('file', file.raw)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
let res
|
||||
if (props.isAdd === 'add') {
|
||||
formData.append('params', JSON.stringify(params))
|
||||
res = await addFileManageRightApi(formData)
|
||||
} else {
|
||||
formData.append('params', JSON.stringify(params))
|
||||
res = await updateFileManageRightApi(formData)
|
||||
}
|
||||
|
||||
if (res.data.code === 200) {
|
||||
ElMessage.success(res.data.msg)
|
||||
resetForm()
|
||||
emit('handleQuery')
|
||||
handleClose()
|
||||
} else {
|
||||
ElMessage.error(res.data.msg)
|
||||
// 失败时不关闭对话框,让用户修正
|
||||
}
|
||||
} finally {
|
||||
// 确保 loading 被关闭
|
||||
if (loading) {
|
||||
loading.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const resetForm = () => {
|
||||
Object.assign(form, {
|
||||
contentName: null,
|
||||
term: null,
|
||||
unitName: null,
|
||||
major: null,
|
||||
markCode: null,
|
||||
classifyMark: null,
|
||||
parentId: null,
|
||||
level: 5
|
||||
})
|
||||
fileList.value = []
|
||||
nextTick(() => {
|
||||
if (uploadRef.value) {
|
||||
uploadRef.value.clearFiles()
|
||||
}
|
||||
})
|
||||
if (ruleFormRef.value) {
|
||||
ruleFormRef.value.resetFields()
|
||||
}
|
||||
}
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
initFormData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.form-item {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.file-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 8px 12px;
|
||||
background: #f5f7fa;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.file-info .el-icon {
|
||||
color: #409EFF;
|
||||
margin-right: 8px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.file-info span {
|
||||
color: #606266;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,354 @@
|
|||
<template>
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
:title="title"
|
||||
:show-close="true"
|
||||
:close-on-click-modal="false"
|
||||
@close="handleClose"
|
||||
:append-to-body="true"
|
||||
width="600px"
|
||||
>
|
||||
<div>
|
||||
<el-form :model="form" :rules="dynamicRules" ref="ruleFormRef" label-width="110px">
|
||||
<el-form-item label="上级节点" prop="parentId">
|
||||
<el-tree-select
|
||||
v-model="form.parentId"
|
||||
:data="treeDataList"
|
||||
placeholder="请选择上级节点"
|
||||
:props="treeProps"
|
||||
check-strictly
|
||||
@node-click="onParentSelect"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item :label="getFormLabel('contentName')" prop="contentName">
|
||||
<el-input
|
||||
type="textarea"
|
||||
:autosize="{ minRows: 4, maxRows: 6 }"
|
||||
class="form-item"
|
||||
v-model="form.contentName"
|
||||
clearable
|
||||
show-word-limit
|
||||
:placeholder="getFormPlaceholder('contentName')"
|
||||
maxlength="64"
|
||||
></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item :label="getFormLabel('sort')" prop="sort">
|
||||
<el-input-number
|
||||
v-model="form.sort"
|
||||
:min="0"
|
||||
:placeholder="getFormPlaceholder('sort')"
|
||||
controls-position="right"
|
||||
style="width: 100%;"
|
||||
></el-input-number>
|
||||
|
||||
<span style="color: #f00000; font-size: 14px; display: block; margin-top: 8px;">
|
||||
{{ sortTitle }}
|
||||
</span>
|
||||
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="handleClose" :disabled="disabled">取消</el-button>
|
||||
<el-button type="primary" :disabled="disabled" @click="submitForm">确认</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted, computed, nextTick } from 'vue';
|
||||
import { ElMessage, ElLoading } from 'element-plus';
|
||||
import {
|
||||
getFileManageTreeByAddOrUpdateApi,
|
||||
addFileManageLeftApi,
|
||||
updateFileManageLeftApi,
|
||||
geMaxSortApi
|
||||
} from '@/api/archivesManagement/fileManager/fileManager.js';
|
||||
|
||||
const props = defineProps({
|
||||
width: {
|
||||
type: Number,
|
||||
default: 600
|
||||
},
|
||||
dataForm: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
isAdd: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
rowData: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
projectId: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits(['closeDialog', 'showColose', 'handleQuery']);
|
||||
|
||||
const dialogVisible = ref(true);
|
||||
const form = reactive({
|
||||
parentId: undefined,
|
||||
contentName: '',
|
||||
sort: 1
|
||||
});
|
||||
const treeDataList = ref([]);
|
||||
const selectedParentLevel = ref(null);
|
||||
const sortTitle = ref('');
|
||||
const ruleFormRef = ref();
|
||||
|
||||
const treeProps = {
|
||||
value: 'id',
|
||||
label: 'label',
|
||||
children: 'children'
|
||||
};
|
||||
|
||||
// 动态验证规则
|
||||
const dynamicRules = computed(() => {
|
||||
const isNonRootParent = form.parentId && form.parentId !== 0;
|
||||
return {
|
||||
parentId: [
|
||||
{ required: true, message: '上级节点不能为空', trigger: 'change' }
|
||||
],
|
||||
contentName: [
|
||||
{
|
||||
required: true,
|
||||
message: isNonRootParent ? '案卷题名不能为空' : '分类名称不能为空',
|
||||
trigger: 'blur'
|
||||
}
|
||||
],
|
||||
sort: [
|
||||
{
|
||||
required: true,
|
||||
message: isNonRootParent ? '案卷排序号不能为空' : '分类号不能为空',
|
||||
trigger: 'blur'
|
||||
}
|
||||
]
|
||||
};
|
||||
});
|
||||
|
||||
const errorObj = reactive({
|
||||
contentName: '分类名称',
|
||||
sort: '分类号'
|
||||
});
|
||||
|
||||
// 方法
|
||||
const getMaxSort = async (value) => {
|
||||
console.log('qqq');
|
||||
const res = await geMaxSortApi({ parentId: value });
|
||||
sortTitle.value = '当前已有最大排序为' + (res.data.data || 0);
|
||||
console.log(sortTitle.value);
|
||||
};
|
||||
|
||||
const getFormLabel = (field) => {
|
||||
const isNonRootParent = form.parentId && form.parentId !== 0;
|
||||
if (isNonRootParent) {
|
||||
if (field === 'contentName') {
|
||||
errorObj.contentName = '案卷题名';
|
||||
return '案卷题名';
|
||||
} else if (field === 'sort') {
|
||||
errorObj.sort = '案卷排序号';
|
||||
return '案卷排序号';
|
||||
}
|
||||
}
|
||||
if (field === 'contentName') {
|
||||
errorObj.contentName = '分类名称';
|
||||
return '分类名称';
|
||||
} else if (field === 'sort') {
|
||||
errorObj.sort = '分类号';
|
||||
return '分类号';
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
const getFormPlaceholder = (field) => {
|
||||
const isNonRootParent = form.parentId && form.parentId !== 0;
|
||||
if (isNonRootParent) {
|
||||
if (field === 'contentName') {
|
||||
return '请输入案卷题名';
|
||||
} else if (field === 'sort') {
|
||||
return '请输入案卷排序号';
|
||||
}
|
||||
}
|
||||
if (field === 'contentName') {
|
||||
return '请输入分类名称';
|
||||
} else if (field === 'sort') {
|
||||
return '请输入分类号';
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
const onParentSelect = (selectedNode) => {
|
||||
selectedParentLevel.value = selectedNode ? selectedNode.level : null;
|
||||
nextTick(() => {
|
||||
if (ruleFormRef.value) {
|
||||
ruleFormRef.value.clearValidate(['contentName', 'sort']);
|
||||
}
|
||||
});
|
||||
form.level = Number(selectedNode.level) + 1;
|
||||
getMaxSort(selectedNode.id);
|
||||
};
|
||||
|
||||
const findParentLevel = (treeData, parentId) => {
|
||||
for (const node of treeData) {
|
||||
if (node.id === parentId) {
|
||||
selectedParentLevel.value = node.level;
|
||||
return;
|
||||
}
|
||||
if (node.children && node.children.length > 0) {
|
||||
findParentLevel(node.children, parentId);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const initFormData = async () => {
|
||||
let value = 0;
|
||||
let treeId = props.isAdd === 'edit' && props.rowData ? props.rowData.id : null;
|
||||
await getLeftTreeList(treeId);
|
||||
|
||||
if (props.isAdd === 'edit' && props.rowData) {
|
||||
Object.assign(form, {
|
||||
id: props.rowData.id,
|
||||
parentId: props.rowData.parentId || undefined,
|
||||
contentName: props.rowData.label || null,
|
||||
sort: props.rowData.sort || 0,
|
||||
level: props.rowData.level,
|
||||
proId: props.projectId
|
||||
});
|
||||
value = props.rowData.parentId;
|
||||
} else {
|
||||
Object.assign(form, {
|
||||
parentId: props.rowData && props.rowData.id ? props.rowData.id : undefined,
|
||||
contentName: null,
|
||||
sort: 0,
|
||||
level: props.rowData && props.rowData.level ? props.rowData.level : undefined,
|
||||
proId: props.projectId
|
||||
});
|
||||
value = props.rowData.id;
|
||||
}
|
||||
|
||||
if (props.isAdd === 'edit' && form.parentId) {
|
||||
findParentLevel(treeDataList.value, form.parentId);
|
||||
}
|
||||
|
||||
if (value) {
|
||||
await getMaxSort(value);
|
||||
}
|
||||
};
|
||||
|
||||
const convertToVueTree = (data) => {
|
||||
if (!data || !Array.isArray(data)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return data.map(item => {
|
||||
if (item.level === 4) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const node = {
|
||||
id: item.id,
|
||||
label: item.contentName,
|
||||
level: item.level
|
||||
};
|
||||
|
||||
if (item.children && Array.isArray(item.children) && item.children.length > 0) {
|
||||
const children = convertToVueTree(item.children);
|
||||
const filteredChildren = children.filter(child => child !== null);
|
||||
if (filteredChildren.length > 0) {
|
||||
node.children = filteredChildren;
|
||||
}
|
||||
}
|
||||
|
||||
return node;
|
||||
}).filter(node => node !== null);
|
||||
};
|
||||
|
||||
const getLeftTreeList = async (value) => {
|
||||
const res = await getFileManageTreeByAddOrUpdateApi({ id: value, proId: props.projectId });
|
||||
const transformedData = convertToVueTree(res.data.data);
|
||||
treeDataList.value = transformedData;
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
dialogVisible.value = false;
|
||||
emit('closeDialog');
|
||||
};
|
||||
|
||||
const submitForm = async () => {
|
||||
if (!ruleFormRef.value) return;
|
||||
// 先进行表单验证
|
||||
try {
|
||||
await ruleFormRef.value.validate();
|
||||
} catch (error) {
|
||||
return;
|
||||
}
|
||||
let loading = null;
|
||||
try {
|
||||
loading = ElLoading.service({
|
||||
lock: true,
|
||||
text: '数据提交中,请稍候...',
|
||||
background: 'rgba(0,0,0,0.5)'
|
||||
});
|
||||
|
||||
const params = { ...form };
|
||||
let res;
|
||||
if (props.isAdd === 'add') {
|
||||
res = await addFileManageLeftApi(params);
|
||||
} else {
|
||||
res = await updateFileManageLeftApi(params);
|
||||
}
|
||||
|
||||
if (res.data.code === 200) {
|
||||
ElMessage.success(res.data.msg);
|
||||
resetForm();
|
||||
emit('handleQuery');
|
||||
handleClose();
|
||||
} else {
|
||||
ElMessage.error(res.data.msg);
|
||||
}
|
||||
} finally {
|
||||
// 无论成功还是失败,都会执行这里
|
||||
if (loading) {
|
||||
loading.close();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const resetForm = () => {
|
||||
Object.assign(form, {
|
||||
parentId: undefined,
|
||||
contentName: '',
|
||||
sort: 1,
|
||||
level: null
|
||||
});
|
||||
if (ruleFormRef.value) {
|
||||
ruleFormRef.value.resetFields();
|
||||
}
|
||||
};
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
initFormData();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.form-item {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
export const formLabel = [
|
||||
{
|
||||
isShow: false,
|
||||
f_type: 'ipt',
|
||||
f_label: '文件名称',
|
||||
f_model: 'contentName',
|
||||
f_max: 32,
|
||||
},
|
||||
{
|
||||
isShow: false,
|
||||
f_type: 'date',
|
||||
f_label: '上传时间',
|
||||
f_model: 'uploadTime',
|
||||
},
|
||||
]
|
||||
|
||||
export const columnsList = [
|
||||
{ t_props: 'contentName', t_label: '档案名称', t_width: 200 },
|
||||
{ t_slot: 'fileName', t_label: '档案文件', t_width: 140 },
|
||||
{ t_props: 'createUserName', t_label: '上传人' },
|
||||
{ t_props: 'term', t_label: '保管期限' },
|
||||
{ t_slot: 'dataSource', t_label: '来源' },
|
||||
{ t_props: 'unitName', t_label: '责任单位' },
|
||||
{ t_props: 'createTime', t_label: '上传时间', t_width: 160 }
|
||||
]
|
||||
|
|
@ -0,0 +1,386 @@
|
|||
<template>
|
||||
<div>
|
||||
<el-card style="height: calc(100vh - 190px)">
|
||||
<el-row :gutter="24" style="display: flex; align-items: center">
|
||||
<el-col :span="14">
|
||||
<el-input v-model="filterText" placeholder="输入关键字" @keyup.enter="onHandleSearch">
|
||||
</el-input>
|
||||
</el-col>
|
||||
<el-col :span="10">
|
||||
<el-button type="primary" size="small" @click="onHandleSearch">
|
||||
查询
|
||||
</el-button>
|
||||
<el-button plain size="small" type="primary" icon="Plus"
|
||||
v-hasPermi="['archive:catalogue:add']" @click="addTree" v-if="fileStatus === '0'">
|
||||
新增
|
||||
</el-button>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<div class="tree-container">
|
||||
<el-tree ref="leftTreeRef" :data="treeDataList" default-expand-all class="left-tree-list"
|
||||
@node-click="onHandleNodeClick" :filter-node-method="filterNode" highlight-current node-key="id">
|
||||
<template #default="{ node, data }">
|
||||
<span class="custom-tree-node">
|
||||
<template v-if="isTruncated(node.label)">
|
||||
<el-tooltip effect="dark" :content="node.label" placement="right">
|
||||
<span class="node-label">{{ truncateLabel(node.label) }}</span>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span class="node-label">{{ node.label }}</span>
|
||||
</template>
|
||||
<span class="btn-box">
|
||||
<el-button type="text" icon="Plus" v-if="((data.level > 1 && data.level !==4 && Number(data.isUnique) === 1) || (data.level === 1 && Number(data.isUnique) === 0)) && fileStatus === '0'"
|
||||
@click.stop="() => addTree(data)" v-hasPermi="['file:manage:add']">
|
||||
</el-button>
|
||||
<el-button type="text" v-if="data.level > 1 && Number(data.isUnique) === 1 && fileStatus === '0'" icon="Edit"
|
||||
style="color: #007ce0" @click.stop="() => editTree(data, data)"
|
||||
v-hasPermi="['file:manage:update']">
|
||||
</el-button>
|
||||
<el-button type="text" v-if="data.level > 1 && Number(data.isUnique) === 1 && fileStatus === '0'" icon="Delete" style="color: #f00000;"
|
||||
@click.stop="() => delTree(node, data)" v-hasPermi="['file:manage:del']">
|
||||
</el-button>
|
||||
</span>
|
||||
</span>
|
||||
</template>
|
||||
</el-tree>
|
||||
</div>
|
||||
</el-card>
|
||||
<!-- 树的操作(新增、修改) -->
|
||||
<FileAddTreeData v-if="isflag" :isAdd="isAdd" :rowData="row" :title="title" @closeDialog="closeDialog"
|
||||
@showColose="showColose" :dataForm="row" :width="600" :projectId="projectId" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, watch, nextTick } from 'vue'
|
||||
import { ElMessage, ElMessageBox,ElLoading } from 'element-plus'
|
||||
import { Plus, Edit, Delete } from '@element-plus/icons-vue'
|
||||
import { getFileManageTreeApi, delFileManageApi } from '@/api/archivesManagement/fileManager/fileManager.js'
|
||||
import FileAddTreeData from './addTreeData.vue'
|
||||
|
||||
const props = defineProps({
|
||||
projectId: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
fileStatus: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['handleNodeClick'])
|
||||
|
||||
// 响应式数据
|
||||
const treeDataList = ref([])
|
||||
const filterText = ref('')
|
||||
const originalTreeData = ref([])
|
||||
const isflag = ref(false)
|
||||
const isAdd = ref('')
|
||||
const title = ref('')
|
||||
const row = ref({})
|
||||
const selectedNodeId = ref(null)
|
||||
const leftTreeRef = ref()
|
||||
|
||||
// 计算属性
|
||||
const filteredTreeData = computed(() => {
|
||||
if (!filterText.value) {
|
||||
return treeDataList.value
|
||||
}
|
||||
return filterTreeData(treeDataList.value)
|
||||
})
|
||||
|
||||
// 方法
|
||||
const handleQuery = async () => {
|
||||
const currentNode = leftTreeRef.value?.getCurrentNode()
|
||||
if (currentNode) {
|
||||
selectedNodeId.value = currentNode.id
|
||||
}
|
||||
|
||||
await getLeftTreeList()
|
||||
|
||||
nextTick(() => {
|
||||
if (selectedNodeId.value && leftTreeRef.value) {
|
||||
leftTreeRef.value.setCurrentKey(selectedNodeId.value)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const closeDialog = () => {
|
||||
isflag.value = false
|
||||
handleQuery()
|
||||
}
|
||||
|
||||
const showColose = () => {
|
||||
isflag.value = false
|
||||
}
|
||||
|
||||
const addTree = (data) => {
|
||||
title.value = "新增"
|
||||
isAdd.value = 'add'
|
||||
isflag.value = true
|
||||
if (data) {
|
||||
row.value = data
|
||||
row.value.level = Number(data.level) + 1
|
||||
}
|
||||
}
|
||||
|
||||
const editTree = (rowData, data) => {
|
||||
title.value = "修改"
|
||||
isAdd.value = 'edit'
|
||||
row.value = data
|
||||
isflag.value = true
|
||||
}
|
||||
|
||||
const delTree = (node, data) => {
|
||||
let loading = null;
|
||||
ElMessageBox.confirm(`是否确认删除节点名称为"${node.label}"的数据项?`, '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
// 用户确认删除,直接执行删除操作
|
||||
loading = ElLoading.service({
|
||||
lock: true,
|
||||
text: '正在删除,请稍候...',
|
||||
background: 'rgba(0,0,0,0.5)'
|
||||
});
|
||||
delFileManageApi({ id: data.id }).then(res => {
|
||||
if (res.data.code === 200) {
|
||||
ElMessage.success("删除成功");
|
||||
handleQuery();
|
||||
} else {
|
||||
ElMessage.error(res.data.msg);
|
||||
}
|
||||
});
|
||||
|
||||
}).catch(() => {
|
||||
// 用户取消删除
|
||||
}).finally (() =>{
|
||||
// 无论成功还是失败,都会执行这里
|
||||
if (loading) {
|
||||
loading.close();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const onHandleNodeClick = (data) => {
|
||||
selectedNodeId.value = data.id
|
||||
emit('handleNodeClick', data)
|
||||
}
|
||||
|
||||
const convertToVueTree = (data) => {
|
||||
if (!data || !Array.isArray(data)) {
|
||||
return []
|
||||
}
|
||||
|
||||
return data.map(item => {
|
||||
const node = {
|
||||
id: item.id,
|
||||
label: item.contentName,
|
||||
level: item.level,
|
||||
sort: item.sort,
|
||||
parentId: item.parentId,
|
||||
parentName: item.parentName,
|
||||
isUnique: item.isUnique
|
||||
}
|
||||
|
||||
if (item.children && Array.isArray(item.children) && item.children.length > 0) {
|
||||
const children = convertToVueTree(item.children)
|
||||
if (children.length > 0) {
|
||||
node.children = children
|
||||
}
|
||||
}
|
||||
|
||||
return node
|
||||
})
|
||||
}
|
||||
|
||||
const truncateLabel = (text) => {
|
||||
if (!text) return ''
|
||||
const max = 100
|
||||
return text.length > max ? text.slice(0, max) + '...' : text
|
||||
}
|
||||
|
||||
const isTruncated = (text) => {
|
||||
if (!text) return false
|
||||
return text.length > 100
|
||||
}
|
||||
|
||||
const getLeftTreeList = async () => {
|
||||
const res = await getFileManageTreeApi({ proId: props.projectId })
|
||||
const transformedData = convertToVueTree(res.data.data)
|
||||
treeDataList.value = transformedData
|
||||
originalTreeData.value = JSON.parse(JSON.stringify(treeDataList.value))
|
||||
}
|
||||
|
||||
const onHandleSearch = () => {
|
||||
if (filterText.value) {
|
||||
ElMessage.error('搜索内容包含非法字符,请重新输入')
|
||||
return
|
||||
}
|
||||
leftTreeRef.value?.filter(filterText.value)
|
||||
}
|
||||
|
||||
const filterNode = (value, data) => {
|
||||
if (!value) return true
|
||||
return data.label.indexOf(value) !== -1
|
||||
}
|
||||
|
||||
const filterTreeData = (treeData) => {
|
||||
const result = []
|
||||
for (const node of treeData) {
|
||||
const newNode = { ...node }
|
||||
if (node.children && node.children.length > 0) {
|
||||
const filteredChildren = filterTreeData(node.children)
|
||||
if (filteredChildren.length > 0) {
|
||||
newNode.children = filteredChildren
|
||||
result.push(newNode)
|
||||
} else if (node.label.indexOf(filterText.value) !== -1) {
|
||||
result.push(node)
|
||||
}
|
||||
} else if (node.label.indexOf(filterText.value) !== -1) {
|
||||
result.push(newNode)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// 监听器
|
||||
watch(filterText, (val) => {
|
||||
leftTreeRef.value?.filter(val)
|
||||
})
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
getLeftTreeList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.tree-container {
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
max-height: calc(100vh - 270px);
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.tree-container::-webkit-scrollbar {
|
||||
width: 0px;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.tree-container::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.tree-container::-webkit-scrollbar-thumb {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.left-tree-list {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.custom-tree-node {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
min-height: 20px;
|
||||
padding: 1px 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.node-label {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: visible;
|
||||
text-overflow: initial;
|
||||
white-space: normal;
|
||||
word-break: break-word;
|
||||
word-wrap: break-word;
|
||||
line-height: 1.2;
|
||||
padding-right: 4px;
|
||||
}
|
||||
|
||||
.left-tree-list .el-tree-node__content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: auto;
|
||||
min-height: 20px;
|
||||
padding: 1px 4px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.left-tree-list .el-tree-node__content:has(.node-label[style*="height"]) {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.el-tree--highlight-current .el-tree-node.is-current>.el-tree-node__content {
|
||||
height: auto !important;
|
||||
align-items: center;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.left-tree-list .el-tree-node.is-current>.el-tree-node__content {
|
||||
background-color: #b3d9ff;
|
||||
color: #006e6a !important;
|
||||
}
|
||||
|
||||
.left-tree-list .el-tree-node.is-current>.el-tree-node__content .el-tree-node__label {
|
||||
color: #006e6a !important;
|
||||
}
|
||||
|
||||
.left-tree-list .el-tree-node.is-current>.el-tree-node__content .custom-tree-node {
|
||||
color: #006e6a !important;
|
||||
}
|
||||
|
||||
.left-tree-list .el-tree-node__content:hover {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.el-tree--highlight-current .el-tree-node.is-current>.el-tree-node__content {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.left-tree-list .el-tree-node.is-current>.el-tree-node__content:hover {
|
||||
background-color: #8cc8ff;
|
||||
}
|
||||
|
||||
.btn-box {
|
||||
margin-left: 4px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.15s ease-in-out;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1px;
|
||||
}
|
||||
|
||||
.left-tree-list .el-tree-node__content:hover .btn-box {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
:deep(.el-tree-node__content) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
height: auto;
|
||||
min-height: 38px;
|
||||
padding: 1px 4px;
|
||||
}
|
||||
|
||||
:deep(.el-tree-node__expand-icon) {
|
||||
padding: 1px;
|
||||
margin-right: 1px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
:deep(.el-tree-node__content > .el-tree-node__expand-icon) {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,326 @@
|
|||
<template>
|
||||
<div>
|
||||
<el-card style="min-height: calc(100vh - 190px);">
|
||||
<avue-crud
|
||||
:option="option"
|
||||
:data="tableData"
|
||||
:page="page"
|
||||
:table-loading="loading"
|
||||
@search-change="searchChange"
|
||||
@search-reset="searchReset"
|
||||
@current-change="currentChange"
|
||||
@size-change="sizeChange"
|
||||
@refresh-change="refreshChange"
|
||||
@on-load="onLoad"
|
||||
>
|
||||
<!-- 工具栏按钮 -->
|
||||
<template #menu-left>
|
||||
<el-button plain size="small" type="primary" icon="Plus" v-hasPermi="['file:manage:add']"
|
||||
@click="handleAdd" :disabled="addBtnIsShow" v-if="fileStatus === '0'">
|
||||
新增
|
||||
</el-button>
|
||||
</template>
|
||||
|
||||
<!-- 自定义列 -->
|
||||
<template #dataSource="{ row }">
|
||||
<span>{{ row.dataSource === '1' ? '本系统上传' : '智慧现场' }}</span>
|
||||
</template>
|
||||
|
||||
<template #fileName="{ row }">
|
||||
<span class="file-name-link" @click="viewFile(row)">{{ row.fileName }}</span>
|
||||
</template>
|
||||
|
||||
<!-- 操作栏 -->
|
||||
<template #menu="{ row, size }">
|
||||
<el-button
|
||||
:size="size"
|
||||
type="primary"
|
||||
link
|
||||
icon="View"
|
||||
@click="handleDetail(row)"
|
||||
v-hasPermi="['file:manage:query']"
|
||||
>详情</el-button>
|
||||
<el-button
|
||||
:size="size"
|
||||
type="primary"
|
||||
link
|
||||
icon="Edit"
|
||||
@click="handleUpdate(row)"
|
||||
v-hasPermi="['file:manage:update']"
|
||||
>修改</el-button>
|
||||
<el-button
|
||||
:size="size"
|
||||
type="danger"
|
||||
link
|
||||
icon="Delete"
|
||||
@click="handleDelete(row)"
|
||||
v-hasPermi="['file:manage:del']"
|
||||
>删除</el-button>
|
||||
</template>
|
||||
</avue-crud>
|
||||
|
||||
<!-- 新增/编辑 -->
|
||||
<AddTableData v-if="isflag" :isAdd="isAdd" :rowData="row" @handleQuery="handleQuery" :title="title"
|
||||
@closeDialog="closeDialog" @showColose="showColose" :dataForm="row" :width="600" :projectId="projectId" />
|
||||
|
||||
<!-- 预览文件 -->
|
||||
<ViewFile v-if="isViewflag" :rowData="row" :title="title" :isAdd="isAdd" @closeDialog="closeDialog"
|
||||
@showColose="showColose" :width="600" />
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, watch, nextTick } from 'vue'
|
||||
import { ElLoading, ElMessage, ElMessageBox } from 'element-plus';
|
||||
import { Plus, View, Edit, Delete } from '@element-plus/icons-vue'
|
||||
import { columnsList, formLabel } from './config.js'
|
||||
import {
|
||||
delFileManageApi,
|
||||
getFileManageApi,
|
||||
} from '@/api/archivesManagement/fileManager/fileManager.js'
|
||||
import AddTableData from './addTableData.vue'
|
||||
import ViewFile from '@/views/viewFile/viewFile.vue'
|
||||
|
||||
const props = defineProps({
|
||||
projectId: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
selectedNode: {
|
||||
type: Object,
|
||||
default: null
|
||||
},
|
||||
fileStatus: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
})
|
||||
|
||||
// 响应式数据
|
||||
const title = ref("")
|
||||
const isflag = ref(false)
|
||||
const isViewflag = ref(false)
|
||||
const isAdd = ref('')
|
||||
const row = ref({})
|
||||
const loading = ref(false)
|
||||
const addBtnIsShow = ref(true)
|
||||
const defaultParams = ref({})
|
||||
|
||||
// 表格数据
|
||||
const tableData = ref([])
|
||||
const page = reactive({
|
||||
currentPage: 1,
|
||||
pageSize: 10,
|
||||
total: 0
|
||||
})
|
||||
|
||||
const query = reactive({})
|
||||
|
||||
// Avue 配置
|
||||
const option = reactive({
|
||||
height: 'auto',
|
||||
calcHeight: 32,
|
||||
tip: false,
|
||||
searchShow: true,
|
||||
searchMenuSpan: 6,
|
||||
border: true,
|
||||
index: true,
|
||||
viewBtn: false,
|
||||
selection: false,
|
||||
addBtn: false,
|
||||
editBtn: false,
|
||||
delBtn: false,
|
||||
dialogClickModal: false,
|
||||
column: [
|
||||
{
|
||||
label: '档案名称',
|
||||
prop: 'contentName',
|
||||
width: 200,
|
||||
search: true
|
||||
},
|
||||
{
|
||||
label: '档案文件',
|
||||
prop: 'fileName',
|
||||
width: 140,
|
||||
slot: true
|
||||
},
|
||||
{
|
||||
label: '上传人',
|
||||
prop: 'createUserName'
|
||||
},
|
||||
{
|
||||
label: '保管期限',
|
||||
prop: 'term'
|
||||
},
|
||||
{
|
||||
label: '来源',
|
||||
prop: 'dataSource',
|
||||
slot: true
|
||||
},
|
||||
{
|
||||
label: '责任单位',
|
||||
prop: 'unitName'
|
||||
},
|
||||
{
|
||||
label: '上传时间',
|
||||
prop: 'createTime',
|
||||
width: 160
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
// 方法
|
||||
const closeDialog = () => {
|
||||
isflag.value = false
|
||||
isViewflag.value = false
|
||||
}
|
||||
|
||||
const showColose = () => {
|
||||
isflag.value = false
|
||||
isViewflag.value = false
|
||||
}
|
||||
|
||||
/** 详情操作 */
|
||||
const handleDetail = (rowData) => {
|
||||
title.value = "详情"
|
||||
isAdd.value = 'detail'
|
||||
row.value = rowData
|
||||
isflag.value = true
|
||||
}
|
||||
|
||||
/** 新增按钮操作 */
|
||||
const handleAdd = () => {
|
||||
title.value = "新增"
|
||||
isAdd.value = 'add'
|
||||
isflag.value = true
|
||||
row.value = props.selectedNode
|
||||
row.value.detailStatus = false
|
||||
row.value.belongName = props.selectedNode.parentName + '/' + props.selectedNode.label
|
||||
}
|
||||
|
||||
/** 修改操作 */
|
||||
const handleUpdate = (rowData) => {
|
||||
title.value = "修改"
|
||||
isAdd.value = 'edit'
|
||||
row.value = rowData
|
||||
row.value.belongName = props.selectedNode.parentName + '/' + props.selectedNode.label
|
||||
row.value.detailStatus = false
|
||||
isflag.value = true
|
||||
}
|
||||
|
||||
// 预览文件
|
||||
const viewFile = (rowData) => {
|
||||
title.value = "预览"
|
||||
isAdd.value = 'view'
|
||||
row.value = rowData
|
||||
console.log(rowData)
|
||||
isViewflag.value = true
|
||||
}
|
||||
|
||||
/* 搜索操作 */
|
||||
const handleQuery = () => {
|
||||
onLoad(page, query)
|
||||
}
|
||||
|
||||
/** 删除操作 */
|
||||
const handleDelete = (rowData) => {
|
||||
let loading = null;
|
||||
ElMessageBox.confirm(`是否确认删除文件名称为"${rowData.contentName}"的数据项?`, '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
// 用户确认删除,直接执行删除操作
|
||||
loading = ElLoading.service({
|
||||
lock: true,
|
||||
text: '正在删除,请稍候...',
|
||||
background: 'rgba(0,0,0,0.5)'
|
||||
});
|
||||
delFileManageApi({ id: rowData.id }).then(res => {
|
||||
if (res.data.code === 200) {
|
||||
ElMessage.success("删除成功")
|
||||
handleQuery()
|
||||
} else {
|
||||
ElMessage.error(res.data.msg)
|
||||
}
|
||||
}).catch(error => {
|
||||
ElMessage.error(error)
|
||||
})
|
||||
}).catch(() => {
|
||||
// 用户取消删除
|
||||
}).finally (() =>{
|
||||
// 无论成功还是失败,都会执行这里
|
||||
if (loading) {
|
||||
loading.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 表格相关方法
|
||||
const searchChange = (params, done) => {
|
||||
Object.assign(query, params)
|
||||
page.currentPage = 1
|
||||
onLoad(page, params)
|
||||
done()
|
||||
}
|
||||
|
||||
const searchReset = () => {
|
||||
Object.keys(query).forEach(key => delete query[key])
|
||||
onLoad(page)
|
||||
}
|
||||
|
||||
const currentChange = (currentPage) => {
|
||||
page.currentPage = currentPage
|
||||
}
|
||||
|
||||
const sizeChange = (pageSize) => {
|
||||
page.pageSize = pageSize
|
||||
}
|
||||
|
||||
const refreshChange = () => {
|
||||
onLoad(page, query)
|
||||
}
|
||||
|
||||
const onLoad = async (pageParam, params = {}) => {
|
||||
loading.value = true
|
||||
try {
|
||||
const requestData = {
|
||||
...params,
|
||||
...defaultParams.value,
|
||||
pageNum: pageParam.currentPage,
|
||||
pageSize: pageParam.pageSize
|
||||
}
|
||||
const res = await getFileManageApi(requestData)
|
||||
if (res.data.code === 200) {
|
||||
tableData.value = res.data.rows
|
||||
page.total = res.data.total
|
||||
}
|
||||
} catch (error) {
|
||||
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 监听选中的节点
|
||||
watch(() => props.selectedNode, (newVal) => {
|
||||
addBtnIsShow.value = !(newVal && Number(newVal.level) === 4)
|
||||
|
||||
const parentId = newVal && newVal.id ? newVal.id : 0
|
||||
const proId = props.projectId
|
||||
defaultParams.value = { parentId, proId }
|
||||
|
||||
onLoad(page, { parentId, proId })
|
||||
}, { immediate: true })
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.file-name-link {
|
||||
color: #409EFF;
|
||||
cursor: pointer;
|
||||
}
|
||||
.file-name-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,317 @@
|
|||
<template>
|
||||
<div class="app-container">
|
||||
<el-card class="toolbar-card">
|
||||
<div class="toolbar">
|
||||
<div class="toolbar-left">
|
||||
<el-button type="warning" plain icon="Bottom" size="small" @click="handleFileExtract"
|
||||
v-if="fileStatus === '0' && !integrityStatus">档案抽取</el-button>
|
||||
<el-button type="success" plain icon="Finished" size="small" @click="moveListConfirm"
|
||||
v-if="fileStatus === '0' && !integrityStatus">移交清单确认</el-button>
|
||||
<el-button type="success" plain icon="Finished" size="small" @click="handleIntegrityStatus"
|
||||
v-if="fileStatus === '0' && integrityStatus">完整性确认</el-button>
|
||||
<el-button type="danger" plain icon="Close" size="small" @click="handleClose">返回</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<el-row :gutter="24" class="content-row" v-if="projectId && fileStatus">
|
||||
<el-col :span="8" class="pane-left">
|
||||
<LeftTree @handleNodeClick="handleNodeClick" :projectId="projectId" :fileStatus="fileStatus" />
|
||||
</el-col>
|
||||
<el-col :span="16" class="pane-right">
|
||||
<RightTable :selectedNode="selectedNode" :projectId="projectId" :fileStatus="fileStatus" />
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 档案同步进度弹框 -->
|
||||
<el-dialog
|
||||
v-model="syncDialogVisible"
|
||||
:close-on-click-modal="false"
|
||||
:close-on-press-escape="false"
|
||||
:show-close="false"
|
||||
width="400px"
|
||||
>
|
||||
<template #header>
|
||||
<div class="dialog-title">
|
||||
<span>档案同步</span>
|
||||
</div>
|
||||
</template>
|
||||
<div class="sync-content">
|
||||
<div class="sync-icon">
|
||||
<el-icon class="is-loading" v-if="isSyncing"><Loading /></el-icon>
|
||||
<el-icon v-else-if="syncSuccess" color="#67C23A"><SuccessFilled /></el-icon>
|
||||
<el-icon v-else-if="syncError" color="#F56C6C"><CircleCloseFilled /></el-icon>
|
||||
</div>
|
||||
<div class="sync-text">
|
||||
<p v-if="isSyncing">档案同步中,请稍候...</p>
|
||||
<p v-else-if="syncSuccess" class="success-text">同步档案已完成</p>
|
||||
<p v-else-if="syncError" class="error-text">档案同步失败</p>
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button class="clear-btn" @click="closeSyncDialog" v-if="!isSyncing">取消</el-button>
|
||||
<el-button type="primary" class="search-btn" @click="closeSyncDialog" v-if="!isSyncing">确定</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 完整性确认弹框 -->
|
||||
<el-dialog
|
||||
v-model="confirmDialogVisible"
|
||||
:close-on-click-modal="false"
|
||||
:close-on-press-escape="false"
|
||||
:show-close="true"
|
||||
width="450px"
|
||||
>
|
||||
<template #header>
|
||||
<div class="dialog-title">
|
||||
<span>操作确认</span>
|
||||
</div>
|
||||
</template>
|
||||
<div class="confirm-content">
|
||||
<div class="confirm-icon">
|
||||
<el-icon color="#E6A23C" :size="48"><QuestionFilled /></el-icon>
|
||||
</div>
|
||||
<div class="confirm-text">
|
||||
<p class="main-message">确认所有档案已完整,可以进行移交?</p>
|
||||
<p class="sub-message">确认后不可再上传文件。</p>
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button class="clear-btn" @click="confirmDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" class="search-btn" @click="confirmIntegrityStatus">确定</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import {
|
||||
Bottom,
|
||||
Finished,
|
||||
Close,
|
||||
Loading,
|
||||
SuccessFilled,
|
||||
CircleCloseFilled,
|
||||
QuestionFilled
|
||||
} from '@element-plus/icons-vue'
|
||||
import LeftTree from './components/leftTree.vue'
|
||||
import RightTable from './components/rightTable.vue'
|
||||
import { fileExtractApi, updateIntegrityStatusApi } from '@/api/archivesManagement/fileManager/fileManager'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
// 响应式数据
|
||||
const projectId = ref(null)
|
||||
const fileStatus = ref(null)
|
||||
const selectedNode = ref(null)
|
||||
const syncDialogVisible = ref(false)
|
||||
const isSyncing = ref(false)
|
||||
const syncSuccess = ref(false)
|
||||
const syncError = ref(false)
|
||||
const integrityStatus = ref(false)
|
||||
const confirmDialogVisible = ref(false)
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
projectId.value = route.query.id
|
||||
fileStatus.value = route.query.fileStatus
|
||||
})
|
||||
|
||||
// 方法
|
||||
const handleClose = () => {
|
||||
router.go(-1)
|
||||
}
|
||||
|
||||
const handleNodeClick = (data) => {
|
||||
selectedNode.value = data
|
||||
}
|
||||
|
||||
const moveListConfirm = () => {
|
||||
integrityStatus.value = true
|
||||
}
|
||||
|
||||
const handleIntegrityStatus = () => {
|
||||
confirmDialogVisible.value = true
|
||||
}
|
||||
|
||||
const confirmIntegrityStatus = async () => {
|
||||
try {
|
||||
const res = await updateIntegrityStatusApi({ proId: projectId.value })
|
||||
if (res.code === 200) {
|
||||
ElMessage.success('完整性确认成功')
|
||||
confirmDialogVisible.value = false
|
||||
setTimeout(() => {
|
||||
router.push('/archivesManagement/fileManager')
|
||||
}, 200)
|
||||
} else {
|
||||
ElMessage.error(res.msg || '完整性确认失败')
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error('完整性确认失败,请重试')
|
||||
console.error('完整性确认失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const handleFileExtract = async () => {
|
||||
syncDialogVisible.value = true
|
||||
isSyncing.value = true
|
||||
syncSuccess.value = false
|
||||
syncError.value = false
|
||||
|
||||
try {
|
||||
const res = await fileExtractApi({ projectId: projectId.value })
|
||||
isSyncing.value = false
|
||||
|
||||
if (res.code === 200) {
|
||||
syncSuccess.value = true
|
||||
} else {
|
||||
syncError.value = true
|
||||
}
|
||||
} catch (error) {
|
||||
isSyncing.value = false
|
||||
syncError.value = true
|
||||
console.error('档案抽取失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const closeSyncDialog = () => {
|
||||
syncDialogVisible.value = false
|
||||
isSyncing.value = false
|
||||
syncSuccess.value = false
|
||||
syncError.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.app-container {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.toolbar-card {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.toolbar-left :deep(.el-button) + :deep(.el-button) {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.toolbar-right :deep(.el-tag) {
|
||||
border-color: #dcdfe6;
|
||||
}
|
||||
|
||||
.content-row {
|
||||
min-height: calc(100vh - 200px);
|
||||
}
|
||||
|
||||
.pane-left,
|
||||
.pane-right {
|
||||
background: #fff;
|
||||
border-radius: 6px;
|
||||
height: 100%;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.pane-left {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.pane-right {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 档案同步弹框样式 */
|
||||
.dialog-title {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
text-align: right !important;
|
||||
}
|
||||
|
||||
.dialog-footer .el-button+.el-button {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.sync-content {
|
||||
text-align: center;
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
.sync-icon {
|
||||
font-size: 48px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.sync-icon .el-icon-loading {
|
||||
color: #409EFF;
|
||||
animation: rotating 2s linear infinite;
|
||||
}
|
||||
|
||||
.sync-text p {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.success-text {
|
||||
color: #67C23A !important;
|
||||
}
|
||||
|
||||
.error-text {
|
||||
color: #F56C6C !important;
|
||||
}
|
||||
|
||||
@keyframes rotating {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
/* 确认弹框样式 */
|
||||
.confirm-content {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
.confirm-icon {
|
||||
margin-right: 16px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.confirm-text {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.main-message {
|
||||
margin: 0 0 8px 0;
|
||||
font-size: 16px;
|
||||
color: #303133;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.sub-message {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
color: #909399;
|
||||
line-height: 1.4;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -23,6 +23,18 @@
|
|||
formatFileStatus(row.fileStatus)
|
||||
}}
|
||||
</template>
|
||||
|
||||
|
||||
|
||||
<!-- 操作列 -->
|
||||
<template #menu="{ row }">
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="openFileManager(row)"
|
||||
>
|
||||
档案管理
|
||||
</el-button>
|
||||
</template>
|
||||
</avue-crud>
|
||||
</basic-container>
|
||||
</template>
|
||||
|
|
@ -223,6 +235,17 @@ export default {
|
|||
this.selectionClear();
|
||||
});
|
||||
},
|
||||
|
||||
openFileManager(row) {
|
||||
this.$router.push({
|
||||
path: '/archivesManagement/fileData',
|
||||
query: {
|
||||
id: row.id, // 项目ID
|
||||
fileStatus: row.fileStatus // 档案状态
|
||||
}
|
||||
});
|
||||
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,483 @@
|
|||
<template>
|
||||
<!-- 预览文件 -->
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
:title="title"
|
||||
:show-close="true"
|
||||
:close-on-click-modal="false"
|
||||
@close="handleClose"
|
||||
:append-to-body="true"
|
||||
:width="width > 500 ? '700px' : '500px'"
|
||||
>
|
||||
<div style="text-align:center">
|
||||
<!-- 图片预览 -->
|
||||
<template v-if="isImage">
|
||||
<div class="image-toolbar">
|
||||
<!-- <el-button size="small" @click="downloadFile"><el-icon><Download /></el-icon> 下载</el-button> -->
|
||||
</div>
|
||||
<el-image
|
||||
:src="processedFileUrl"
|
||||
:preview-src-list="previewList"
|
||||
fit="contain"
|
||||
style="max-width:100%;max-height:70vh"
|
||||
>
|
||||
<template #error>
|
||||
<div class="image-slot">
|
||||
<el-icon><Picture /></el-icon>
|
||||
</div>
|
||||
</template>
|
||||
</el-image>
|
||||
</template>
|
||||
|
||||
<!-- PDF 预览 -->
|
||||
<template v-else>
|
||||
<div class="pdf-container">
|
||||
<!-- PDF加载状态 -->
|
||||
<div v-if="pdfLoading" class="pdf-loading">
|
||||
<el-icon class="is-loading"><Loading /></el-icon>
|
||||
<p>PDF加载中...</p>
|
||||
</div>
|
||||
|
||||
<!-- PDF错误状态 -->
|
||||
<div v-else-if="pdfError" class="pdf-error">
|
||||
<el-icon><Warning /></el-icon>
|
||||
<p>PDF加载失败</p>
|
||||
<el-button size="small" @click="retryLoadPdf">重试</el-button>
|
||||
</div>
|
||||
|
||||
<!-- PDF内容 -->
|
||||
<div v-else class="pdf-content">
|
||||
<!-- PDF工具栏 -->
|
||||
<div class="pdf-toolbar">
|
||||
<div class="pdf-controls">
|
||||
<el-button
|
||||
size="small"
|
||||
:disabled="currentPage <= 1"
|
||||
@click="prevPage"
|
||||
>
|
||||
<el-icon><ArrowLeft /></el-icon> 上一页
|
||||
</el-button>
|
||||
<span class="page-info">
|
||||
{{ currentPage }} / {{ numPages }}
|
||||
</span>
|
||||
<el-button
|
||||
size="small"
|
||||
:disabled="currentPage >= numPages"
|
||||
@click="nextPage"
|
||||
>
|
||||
下一页 <el-icon><ArrowRight /></el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="pdf-actions">
|
||||
<!-- <el-button size="small" @click="downloadFile"><el-icon><Download /></el-icon> 下载</el-button> -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- PDF显示区域 -->
|
||||
<div class="pdf-viewer" @wheel="handleWheel" ref="pdfViewer">
|
||||
<!-- 注意:vue-pdf 需要安装兼容 Vue3 的版本 -->
|
||||
<!-- <pdf
|
||||
:src="processedFileUrl"
|
||||
:page="currentPage"
|
||||
@num-pages="numPages = $event"
|
||||
@loaded="onPdfLoaded"
|
||||
@error="onPdfError"
|
||||
style="width:100%;height:70vh"
|
||||
/> -->
|
||||
<div class="pdf-placeholder">
|
||||
<el-icon><Document /></el-icon>
|
||||
<p>PDF预览功能</p>
|
||||
<p>需要安装 vue-pdf 的 Vue3 兼容版本</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, computed, onMounted, watch, nextTick } from 'vue'
|
||||
import {
|
||||
Document,
|
||||
Picture,
|
||||
Loading,
|
||||
Warning,
|
||||
ArrowLeft,
|
||||
ArrowRight,
|
||||
Download
|
||||
} from '@element-plus/icons-vue'
|
||||
// 注意:需要安装兼容 Vue3 的 pdf 组件
|
||||
// import pdf from 'vue-pdf'
|
||||
import { getFileAsBase64Api } from '@/api/archivesManagement/fileManager/fileManager'
|
||||
|
||||
const props = defineProps({
|
||||
width: {
|
||||
type: [Number, String],
|
||||
default: 600
|
||||
},
|
||||
hight: {
|
||||
type: [Number, String],
|
||||
default: 400
|
||||
},
|
||||
dataForm: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: '文件预览'
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
isAdd: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
rowData: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
projectId: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['closeDialog', 'showColose'])
|
||||
|
||||
// 响应式数据
|
||||
const dialogVisible = ref(true)
|
||||
const fileUrl = ref('')
|
||||
const fileName = ref('')
|
||||
const previewList = ref([])
|
||||
const pdfLoading = ref(false)
|
||||
const pdfError = ref(false)
|
||||
const currentPage = ref(1)
|
||||
const numPages = ref(0)
|
||||
const wheelTimeout = ref(null)
|
||||
const isWheelScrolling = ref(false)
|
||||
const pdfViewer = ref()
|
||||
|
||||
// 计算属性
|
||||
const isImage = computed(() => {
|
||||
const exts = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp']
|
||||
const url = (fileUrl.value || '')
|
||||
const name = (fileName.value || '').toLowerCase()
|
||||
return exts.some(ext => url.endsWith(ext) || name.endsWith(ext))
|
||||
})
|
||||
|
||||
const isPdf = computed(() => {
|
||||
const exts = ['.pdf']
|
||||
const url = (fileUrl.value || '')
|
||||
const name = (fileName.value || '').toLowerCase()
|
||||
return exts.some(ext => url.endsWith(ext) || name.endsWith(ext))
|
||||
})
|
||||
|
||||
const processedFileUrl = computed(() => {
|
||||
if (fileUrl.value && !fileUrl.value.startsWith('data:')) {
|
||||
if (isPdf.value) {
|
||||
return `data:application/pdf;base64,${fileUrl.value}`
|
||||
} else if (isImage.value) {
|
||||
const imageType = getImageMimeType()
|
||||
return `data:${imageType};base64,${fileUrl.value}`
|
||||
}
|
||||
}
|
||||
return fileUrl.value
|
||||
})
|
||||
|
||||
// 方法
|
||||
const handleClose = () => {
|
||||
dialogVisible.value = false
|
||||
emit('closeDialog')
|
||||
}
|
||||
|
||||
/* 获取文件的base64 */
|
||||
const getFileAsBase64 = async () => {
|
||||
if (isPdf.value) {
|
||||
pdfLoading.value = true
|
||||
pdfError.value = false
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await getFileAsBase64Api({ id: props.rowData.fileId })
|
||||
const obj = res.data
|
||||
fileUrl.value = obj?.fileBase64 || ''
|
||||
fileName.value = obj?.fileName || ''
|
||||
|
||||
if (isImage.value && fileUrl.value) {
|
||||
previewList.value = [processedFileUrl.value]
|
||||
}
|
||||
} catch (error) {
|
||||
if (isPdf.value) {
|
||||
pdfError.value = true
|
||||
pdfLoading.value = false
|
||||
}
|
||||
console.error('获取文件失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// PDF相关方法
|
||||
const resetPdfState = () => {
|
||||
currentPage.value = 1
|
||||
numPages.value = 0
|
||||
pdfLoading.value = false
|
||||
pdfError.value = false
|
||||
isWheelScrolling.value = false
|
||||
if (wheelTimeout.value) {
|
||||
clearTimeout(wheelTimeout.value)
|
||||
wheelTimeout.value = null
|
||||
}
|
||||
}
|
||||
|
||||
const onPdfLoaded = () => {
|
||||
pdfLoading.value = false
|
||||
pdfError.value = false
|
||||
}
|
||||
|
||||
const onPdfError = (error) => {
|
||||
pdfLoading.value = false
|
||||
pdfError.value = true
|
||||
console.error('PDF加载失败:', error)
|
||||
}
|
||||
|
||||
const retryLoadPdf = () => {
|
||||
resetPdfState()
|
||||
getFileAsBase64()
|
||||
}
|
||||
|
||||
const prevPage = () => {
|
||||
if (currentPage.value > 1) {
|
||||
currentPage.value--
|
||||
}
|
||||
}
|
||||
|
||||
const nextPage = () => {
|
||||
if (currentPage.value < numPages.value) {
|
||||
currentPage.value++
|
||||
}
|
||||
}
|
||||
|
||||
// 下载文件
|
||||
const downloadFile = () => {
|
||||
if (!fileUrl.value) return
|
||||
const filename = fileName.value || '文件'
|
||||
|
||||
let mime = isPdf.value ? 'application/pdf' : getImageMimeType()
|
||||
|
||||
let base64Data = fileUrl.value
|
||||
if (typeof base64Data === 'string' && base64Data.startsWith('data:')) {
|
||||
try {
|
||||
const parts = base64Data.split(',')
|
||||
const header = parts[0]
|
||||
const dataPart = parts[1]
|
||||
const match = header.match(/^data:(.*?);base64$/)
|
||||
if (match && match[1]) mime = match[1]
|
||||
base64Data = dataPart
|
||||
} catch (e) {
|
||||
// 解析失败则按原样处理
|
||||
}
|
||||
}
|
||||
|
||||
const blob = base64ToBlob(base64Data, mime)
|
||||
const url = URL.createObjectURL(blob)
|
||||
const a = document.createElement('a')
|
||||
a.style.display = 'none'
|
||||
a.href = url
|
||||
a.download = filename
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
document.body.removeChild(a)
|
||||
URL.revokeObjectURL(url)
|
||||
}
|
||||
|
||||
// base64转Blob
|
||||
const base64ToBlob = (base64, mime) => {
|
||||
const byteChars = atob(base64)
|
||||
const sliceSize = 1024
|
||||
const byteArrays = []
|
||||
for (let offset = 0; offset < byteChars.length; offset += sliceSize) {
|
||||
const slice = byteChars.slice(offset, offset + sliceSize)
|
||||
const byteNumbers = new Array(slice.length)
|
||||
for (let i = 0; i < slice.length; i++) {
|
||||
byteNumbers[i] = slice.charCodeAt(i)
|
||||
}
|
||||
const byteArray = new Uint8Array(byteNumbers)
|
||||
byteArrays.push(byteArray)
|
||||
}
|
||||
return new Blob(byteArrays, { type: mime || 'application/octet-stream' })
|
||||
}
|
||||
|
||||
// 获取图片MIME类型
|
||||
const getImageMimeType = () => {
|
||||
const name = (fileName.value || '').toLowerCase()
|
||||
if (name.endsWith('.jpg') || name.endsWith('.jpeg')) {
|
||||
return 'image/jpeg'
|
||||
} else if (name.endsWith('.png')) {
|
||||
return 'image/png'
|
||||
} else if (name.endsWith('.gif')) {
|
||||
return 'image/gif'
|
||||
} else if (name.endsWith('.bmp')) {
|
||||
return 'image/bmp'
|
||||
} else if (name.endsWith('.webp')) {
|
||||
return 'image/webp'
|
||||
}
|
||||
return 'image/jpeg'
|
||||
}
|
||||
|
||||
// 处理滚轮事件
|
||||
const handleWheel = (event) => {
|
||||
event.preventDefault()
|
||||
|
||||
if (isWheelScrolling.value) {
|
||||
return
|
||||
}
|
||||
|
||||
isWheelScrolling.value = true
|
||||
|
||||
if (wheelTimeout.value) {
|
||||
clearTimeout(wheelTimeout.value)
|
||||
}
|
||||
|
||||
if (event.deltaY > 0) {
|
||||
nextPage()
|
||||
} else if (event.deltaY < 0) {
|
||||
prevPage()
|
||||
}
|
||||
|
||||
wheelTimeout.value = setTimeout(() => {
|
||||
isWheelScrolling.value = false
|
||||
}, 300)
|
||||
}
|
||||
|
||||
// 监听器
|
||||
watch(isPdf, (newVal) => {
|
||||
if (newVal) {
|
||||
resetPdfState()
|
||||
}
|
||||
})
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
getFileAsBase64()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.image-slot {
|
||||
width: 100%;
|
||||
height: 240px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #909399;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
/* PDF预览样式 */
|
||||
.pdf-container {
|
||||
width: 100%;
|
||||
height: 70vh;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.pdf-loading,
|
||||
.pdf-error {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
color: #909399;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.pdf-loading .el-icon {
|
||||
font-size: 32px;
|
||||
margin-bottom: 16px;
|
||||
animation: rotating 2s linear infinite;
|
||||
}
|
||||
|
||||
.pdf-error .el-icon {
|
||||
font-size: 32px;
|
||||
margin-bottom: 16px;
|
||||
color: #F56C6C;
|
||||
}
|
||||
|
||||
.pdf-content {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.pdf-toolbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px 12px;
|
||||
background: #f5f5f5;
|
||||
border-bottom: 1px solid #e4e7ed;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.pdf-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.image-toolbar {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding: 4px 0 8px 0;
|
||||
}
|
||||
|
||||
.pdf-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.page-info {
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
min-width: 60px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.pdf-viewer {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
background: #f8f9fa;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.pdf-placeholder {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
color: #909399;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.pdf-placeholder .el-icon {
|
||||
font-size: 48px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
@keyframes rotating {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Loading…
Reference in New Issue