档案管理功能

This commit is contained in:
lSun 2025-11-29 13:57:13 +08:00
parent 6c17051046
commit 6b33583f74
11 changed files with 2445 additions and 5 deletions

View File

@ -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,
})
}

View File

@ -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
})
}

View File

@ -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: '档案数据管理'
}
}
];

View File

@ -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>

View File

@ -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>

View File

@ -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 }
]

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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)
}
// base64Blob
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>