实现下载专区、精准服务前后端增删改查交互
This commit is contained in:
72
package-lock.json
generated
72
package-lock.json
generated
@@ -9,6 +9,10 @@
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"@element-plus/icons-vue": "^2.3.2",
|
||||
"@vue-office/docx": "^1.6.3",
|
||||
"@vue-office/excel": "^1.7.14",
|
||||
"@vue-office/pdf": "^2.0.10",
|
||||
"@vue-office/pptx": "^1.0.1",
|
||||
"@vueup/vue-quill": "^1.2.0",
|
||||
"element-plus": "^2.11.4",
|
||||
"pinia": "^2.2.6",
|
||||
@@ -1376,6 +1380,74 @@
|
||||
"vue": "^3.2.25"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue-office/docx": {
|
||||
"version": "1.6.3",
|
||||
"resolved": "https://registry.npmjs.org/@vue-office/docx/-/docx-1.6.3.tgz",
|
||||
"integrity": "sha512-Cs+3CAaRBOWOiW4XAhTwwxJ0dy8cPIf6DqfNvYcD3YACiLwO4kuawLF2IAXxyijhbuOeoFsfvoVbOc16A/4bZA==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@vue/composition-api": "^1.7.1",
|
||||
"vue": "^2.0.0 || >=3.0.0",
|
||||
"vue-demi": "^0.14.6"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@vue/composition-api": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@vue-office/excel": {
|
||||
"version": "1.7.14",
|
||||
"resolved": "https://registry.npmjs.org/@vue-office/excel/-/excel-1.7.14.tgz",
|
||||
"integrity": "sha512-pVUgt+emDQUnW7q22CfnQ+jl43mM/7IFwYzOg7lwOwPEbiVB4K4qEQf+y/bc4xGXz75w1/e3Kz3G6wAafmFBFg==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@vue/composition-api": "^1.7.1",
|
||||
"vue": "^2.0.0 || >=3.0.0",
|
||||
"vue-demi": "^0.14.6"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@vue/composition-api": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@vue-office/pdf": {
|
||||
"version": "2.0.10",
|
||||
"resolved": "https://registry.npmjs.org/@vue-office/pdf/-/pdf-2.0.10.tgz",
|
||||
"integrity": "sha512-yHVLrMAKpMPBkhBwofFyGEtEeJF0Zd7oGmf56Pe5aj/xObdRq3E1CIZqTqhWJNgHV8oLQqaX0vs4p5T1zq+GIA==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@vue/composition-api": "^1.7.1",
|
||||
"vue": "^2.0.0 || >=3.0.0",
|
||||
"vue-demi": "^0.14.6"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@vue/composition-api": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@vue-office/pptx": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@vue-office/pptx/-/pptx-1.0.1.tgz",
|
||||
"integrity": "sha512-+V7Kctzl6f6+Yk4NaD/wQGRIkqLWcowe0jEhPexWQb8Oilbzt1OyhWRWcMsxNDTdrgm6aMLP+0/tmw27cxddMg==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@vue/composition-api": "^1.7.1",
|
||||
"vue": "^2.0.0 || >=3.0.0",
|
||||
"vue-demi": "^0.14.6"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@vue/composition-api": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/babel-helper-vue-transform-on": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmmirror.com/@vue/babel-helper-vue-transform-on/-/babel-helper-vue-transform-on-1.2.5.tgz",
|
||||
|
||||
@@ -10,6 +10,10 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@element-plus/icons-vue": "^2.3.2",
|
||||
"@vue-office/docx": "^1.6.3",
|
||||
"@vue-office/excel": "^1.7.14",
|
||||
"@vue-office/pdf": "^2.0.10",
|
||||
"@vue-office/pptx": "^1.0.1",
|
||||
"@vueup/vue-quill": "^1.2.0",
|
||||
"element-plus": "^2.11.4",
|
||||
"pinia": "^2.2.6",
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-select v-model="searchForm.status" multiple collapse-tags collapse-tags-tooltip :max-collapse-tags="2"
|
||||
placeholder="请选择状态" clearable style="width: 230px">
|
||||
placeholder="请选择状态" clearable style="width: 120px">
|
||||
<el-option label="草稿" value="isDraft=1" />
|
||||
<el-option label="发布" value="isPublished=1" />
|
||||
<el-option label="下架" value="isPublished=0" />
|
||||
|
||||
@@ -33,8 +33,9 @@
|
||||
|
||||
<div class="upload-section">
|
||||
<el-upload class="file-uploader" :show-file-list="false" :on-success="handleFileSuccess"
|
||||
:before-upload="beforeFileUpload" :on-progress="handleUploadProgress" action="#" :accept="acceptedFileTypes">
|
||||
<div v-if="!downloadForm.fileUrl" class="upload-area">
|
||||
:before-upload="beforeFileUpload" :on-progress="handleUploadProgress"
|
||||
action="http://localhost:3000/api/upload" :accept="acceptedFileTypes">
|
||||
<div v-if="!downloadForm.attachmentUrl" class="upload-area">
|
||||
<el-icon class="upload-icon">
|
||||
<Upload />
|
||||
</el-icon>
|
||||
@@ -103,17 +104,12 @@
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="排序数字">
|
||||
<el-input-number v-model="fileSettings.sortOrder" :min="0" :max="9999" />
|
||||
<el-form-item label="上传者">
|
||||
<el-input v-model="fileSettings.uploader" placeholder="请输入上传者" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-form-item label="详细描述">
|
||||
<el-input v-model="fileSettings.description" type="textarea" :rows="3" placeholder="请输入文件详细描述" maxlength="500"
|
||||
show-word-limit />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="显示图片">
|
||||
<el-upload class="image-uploader" :show-file-list="false" :on-success="handleImageSuccess"
|
||||
:before-upload="beforeImageUpload" action="#" accept="image/*">
|
||||
@@ -122,14 +118,23 @@
|
||||
<el-icon>
|
||||
<Picture />
|
||||
</el-icon>
|
||||
<p>点击上传显示图片</p>
|
||||
<p>点击上传图片</p>
|
||||
</div>
|
||||
</el-upload>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="浏览次数">
|
||||
<el-input-number v-model="fileSettings.viewCount" :min="0" />
|
||||
</el-form-item>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="浏览次数">
|
||||
<el-input-number v-model="fileSettings.viewCount" :min="0" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="排序数字">
|
||||
<el-input-number v-model="fileSettings.sortOrder" :min="0" :max="9999" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-divider content-position="left">发布设置</el-divider>
|
||||
|
||||
@@ -183,6 +188,41 @@
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 回收站弹窗 -->
|
||||
<el-dialog v-model="recycleDialogVisible" title="回收站" width="60%">
|
||||
<div>
|
||||
<el-row :gutter="12" align="middle" style="margin-bottom:12px">
|
||||
<el-col :span="12" style="display: flex; align-items: center;">
|
||||
<span style="font-size: 16px; margin-right: 16px;">标题:</span>
|
||||
<el-input v-model="recycleSearch" placeholder="按文章标题搜索" style="width:200px" clearable />
|
||||
<el-button type="primary" @click="loadRecycleData" style="margin-left: 8px;">搜索</el-button>
|
||||
<el-button @click="recycleSearch = ''; loadRecycleData()" style="margin-left: 4px;">重置</el-button>
|
||||
</el-col>
|
||||
<el-col :span="12" style="text-align: right;">
|
||||
<el-button type="success" :disabled="recycleTableData.length === 0"
|
||||
@click="handleRecoverAll">一键恢复</el-button>
|
||||
<el-button type="danger" :disabled="recycleTableData.length === 0" @click="handleForceDeleteAll"
|
||||
style="margin-left: 8px;">一键删除</el-button>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-table :data="recycleTableData" stripe border style="width:100%">
|
||||
<el-table-column prop="id" label="ID" width="80" />
|
||||
<el-table-column prop="title" label="文章名称" min-width="240" show-overflow-tooltip />
|
||||
<el-table-column prop="updateTime" label="删除时间" width="200">
|
||||
<template #default="{ row }">{{ row.updateTime ? new Date(row.updateTime).toLocaleString() : ''
|
||||
}}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="220" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" size="small" @click="handleRecover(row)">恢复</el-button>
|
||||
<el-button type="danger" size="small" @click="handleForceDelete(row)">永久删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -253,33 +293,16 @@ onMounted(() => {
|
||||
|
||||
// 加载文件数据
|
||||
const loadFileData = () => {
|
||||
// 模拟加载数据
|
||||
Object.assign(downloadForm, {
|
||||
title: '福建省教育装备与基建中心2024年工作要点',
|
||||
fileUrl: '/files/2024年工作要点.pdf',
|
||||
fileName: '2024年工作要点.pdf',
|
||||
fileType: 'pdf',
|
||||
fileSize: '2.5MB',
|
||||
uploadTime: '2024-01-15 10:30:00',
|
||||
description: '本文件详细介绍了福建省教育装备与基建中心2024年的工作重点和计划安排,包括教育装备采购、基础设施建设、技术标准制定等方面的内容。'
|
||||
})
|
||||
|
||||
Object.assign(fileSettings, {
|
||||
titleColor: '#1890ff',
|
||||
url: 'https://example.com/download/2024-work-plan',
|
||||
publishTime: '2024-01-15 10:30:00',
|
||||
description: '本文件详细介绍了福建省教育装备与基建中心2024年的工作重点和计划安排,包括教育装备采购、基础设施建设、技术标准制定等方面的内容。',
|
||||
displayImage: '/images/file-preview-pdf.jpg',
|
||||
viewCount: 1250,
|
||||
sortOrder: 1,
|
||||
isPublished: true,
|
||||
isTop: false,
|
||||
isRecommended: true,
|
||||
isHot: false,
|
||||
isSlide: false,
|
||||
seoTitle: '福建省教育装备与基建中心2024年工作要点',
|
||||
seoKeywords: '教育装备,基建中心,工作要点,2024',
|
||||
seoDescription: '福建省教育装备与基建中心2024年工作要点,包含教育装备采购、基础设施建设等详细内容。'
|
||||
fetch('http://localhost:3000/api/files/' + route.params.id, {
|
||||
method: 'GET'
|
||||
}).then(response => response.json()).then(data => {
|
||||
const { title, attachmentUrl, fileName, fileSize, uploadTime, description, ...rest } = data.data;
|
||||
Object.assign(downloadForm, { title, attachmentUrl, fileName, fileType: fileName.split('.').pop().toLowerCase(), fileSize, uploadTime: new Date(uploadTime).toLocaleString(), description })
|
||||
Object.assign(fileSettings, rest)
|
||||
Object.assign(fileSettingsCopy, fileSettings)
|
||||
}).catch(error => {
|
||||
console.error('加载文件数据失败:', error)
|
||||
ElMessage.error('加载文件数据失败')
|
||||
})
|
||||
}
|
||||
|
||||
@@ -287,7 +310,7 @@ const loadFileData = () => {
|
||||
const initNewFile = () => {
|
||||
Object.assign(downloadForm, {
|
||||
title: '',
|
||||
fileUrl: '',
|
||||
attachmentUrl: '',
|
||||
fileName: '',
|
||||
fileType: '',
|
||||
fileSize: '',
|
||||
@@ -299,7 +322,6 @@ const initNewFile = () => {
|
||||
titleColor: '#333333',
|
||||
url: '',
|
||||
publishTime: '',
|
||||
description: '',
|
||||
displayImage: '',
|
||||
viewCount: 0,
|
||||
sortOrder: 0,
|
||||
@@ -321,7 +343,33 @@ const goBack = () => {
|
||||
|
||||
// 保存草稿
|
||||
const handleSaveDraft = () => {
|
||||
ElMessage.success('草稿保存成功')
|
||||
if (!downloadForm.title) {
|
||||
ElMessage.warning('请输入文件标题')
|
||||
return
|
||||
}
|
||||
if (!downloadForm.attachmentUrl) {
|
||||
ElMessage.warning('请上传文件')
|
||||
return
|
||||
}
|
||||
downloadForm.isDraft = true
|
||||
downloadForm.publishTime = null
|
||||
downloadForm.isPublished = false
|
||||
fetch('http://localhost:3000/api/files/' + (isEdit.value ? route.params.id : 0), {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(downloadForm),
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
}).then(response => response.json()).then(data => {
|
||||
if (data.success) {
|
||||
ElMessage.success('草稿保存成功')
|
||||
router.push('/admin/content/downloads')
|
||||
} else {
|
||||
ElMessage.error(data.message)
|
||||
}
|
||||
}).catch(error => {
|
||||
ElMessage.error('服务器错误' + error.message)
|
||||
})
|
||||
}
|
||||
|
||||
// 发布文件
|
||||
@@ -330,11 +378,29 @@ const handlePublish = () => {
|
||||
ElMessage.warning('请输入文件标题')
|
||||
return
|
||||
}
|
||||
if (!downloadForm.fileUrl) {
|
||||
if (!downloadForm.attachmentUrl) {
|
||||
ElMessage.warning('请上传文件')
|
||||
return
|
||||
}
|
||||
ElMessage.success('文件发布成功')
|
||||
downloadForm.isPublished = true
|
||||
downloadForm.isDraft = false
|
||||
downloadForm.publishTime = new Date().toLocaleString()
|
||||
fetch('http://localhost:3000/api/files/' + (isEdit.value ? route.params.id : 0), {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(downloadForm),
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
}).then(response => response.json()).then(data => {
|
||||
if (data.success) {
|
||||
ElMessage.success('文件发布成功')
|
||||
router.push('/admin/content/downloads')
|
||||
} else {
|
||||
ElMessage.error(data.message)
|
||||
}
|
||||
}).catch(error => {
|
||||
ElMessage.error('服务器错误' + error.message)
|
||||
})
|
||||
}
|
||||
|
||||
// 文件上传成功
|
||||
@@ -344,8 +410,8 @@ const handleFileSuccess = (response, file) => {
|
||||
uploadStatus.value = 'success'
|
||||
uploadProgressText.value = '上传完成'
|
||||
|
||||
// 模拟文件信息
|
||||
downloadForm.fileUrl = URL.createObjectURL(file.raw)
|
||||
// 文件信息
|
||||
downloadForm.attachmentUrl = response.data.url
|
||||
downloadForm.fileName = file.name
|
||||
downloadForm.fileType = getFileTypeFromName(file.name)
|
||||
downloadForm.fileSize = formatFileSize(file.size)
|
||||
@@ -439,9 +505,6 @@ const handleCloseSettings = () => {
|
||||
|
||||
// 保存设置
|
||||
const handleSaveSettings = () => {
|
||||
// 同步设置到表单
|
||||
downloadForm.description = fileSettings.description
|
||||
|
||||
fileSettingsCopy.value = Object.assign({}, fileSettings)
|
||||
showSettingsDialog.value = false
|
||||
ElMessage.success('设置保存成功')
|
||||
|
||||
@@ -71,12 +71,36 @@
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="action-right">
|
||||
<el-button @click="handleImport"><el-icon>
|
||||
<el-button @click="handleImport">
|
||||
<el-icon>
|
||||
<Upload />
|
||||
</el-icon> 导入</el-button>
|
||||
<el-button @click="handleExport"><el-icon>
|
||||
<Download />
|
||||
</el-icon> 导出</el-button>
|
||||
</el-icon>
|
||||
导入
|
||||
</el-button>
|
||||
<el-dropdown @command="handleExport" trigger="click">
|
||||
<el-button>
|
||||
<el-icon>
|
||||
<Download />
|
||||
</el-icon>
|
||||
导出
|
||||
<el-icon class="el-icon--right">
|
||||
<ArrowDown />
|
||||
</el-icon>
|
||||
</el-button>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item command="json">导出为JSON</el-dropdown-item>
|
||||
<el-dropdown-item command="xlsx">导出为Excel</el-dropdown-item>
|
||||
<el-dropdown-item command="csv">导出为CSV</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
<el-button @click="openRecycleBin" type="danger">
|
||||
<el-icon>
|
||||
<Delete />
|
||||
</el-icon>
|
||||
回收站
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
@@ -94,33 +118,44 @@
|
||||
</el-link>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="fileIcon" label="文件图标" width="80">
|
||||
<el-table-column prop="fileIcon" label="文件图标" width="90">
|
||||
<template #default="{ row }">
|
||||
<el-icon :size="24" :color="getFileTypeColor(row.fileType)">
|
||||
<component :is="getFileTypeIcon(row.fileType)" />
|
||||
<el-icon :size="24" :color="getFileTypeColor(row.fileName)">
|
||||
<component :is="getFileTypeIcon(row.fileName)" />
|
||||
</el-icon>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="fileName" label="文件名" width="200" show-overflow-tooltip />
|
||||
<el-table-column prop="fileType" label="文件类型" width="110">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getFileTypeTagType(row.fileType)">
|
||||
{{ getFileTypeName(row.fileType) }}
|
||||
<el-tag :type="getFileTypeTagType(row.fileName)">
|
||||
{{ getFileTypeName(row.fileName) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="fileSize" label="文件大小" width="100" />
|
||||
<el-table-column prop="downloadCount" label="下载次数" width="100" />
|
||||
<el-table-column prop="author" label="上传者" width="100" />
|
||||
<el-table-column prop="status" label="状态" width="100">
|
||||
<el-table-column prop="uploader" label="上传者" width="100" />
|
||||
<el-table-column prop="status" label="状态" width="200">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getStatusType(row.status)">
|
||||
{{ getStatusName(row.status) }}
|
||||
</el-tag>
|
||||
<div class="status-tags">
|
||||
<el-tag v-for="status in getArticleStatuses(row)" :key="status.key" :type="status.type" size="small"
|
||||
class="status-tag">
|
||||
{{ status.name }}
|
||||
</el-tag>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="uploadTime" label="上传时间" width="180">
|
||||
<template #default="{ row }">
|
||||
{{ row.uploadTime ? new Date(row.uploadTime).toLocaleString() : '' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="updatedTime" label="更新时间" width="180">
|
||||
<template #default="{ row }">
|
||||
{{ row.updatedTime ? new Date(row.updatedTime).toLocaleString() : '' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="uploadTime" label="上传时间" width="180" />
|
||||
<el-table-column prop="updateTime" label="更新时间" width="180" />
|
||||
<el-table-column label="操作" width="230" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" size="small" @click="handleEdit(row)">
|
||||
@@ -129,14 +164,14 @@
|
||||
</el-icon>
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button v-if="row.status === 'draft' || row.status === 'offline'" type="success" size="small"
|
||||
<el-button v-if="row.isDraft === 1 || row.isPublished === 0" type="success" size="small"
|
||||
@click="handlePublish(row)">
|
||||
<el-icon>
|
||||
<Upload />
|
||||
</el-icon>
|
||||
上架
|
||||
</el-button>
|
||||
<el-button v-if="row.status === 'published'" type="warning" size="small" @click="handleOffline(row)">
|
||||
<el-button v-if="row.isPublished === 1" type="warning" size="small" @click="handleOffline(row)">
|
||||
<el-icon>
|
||||
<Download />
|
||||
</el-icon>
|
||||
@@ -159,13 +194,92 @@
|
||||
@size-change="handleSizeChange" @current-change="handleCurrentChange" />
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 预览文件弹窗 -->
|
||||
<el-dialog v-model="previewDialogVisible" title="预览文件" width="80%">
|
||||
<h2 :style="{ color: previewFileData.titleColor, textAlign: 'center' }">{{ previewFileData.title }}</h2>
|
||||
<p style="font-size: 12px; color: #666; margin-bottom: 10px;">发布时间:{{ new
|
||||
Date(previewFileData.publishTime).toLocaleString() }}</p>
|
||||
<div v-html="previewFileData.description"></div>
|
||||
<br />
|
||||
<div v-if="previewFileData.fileType === 'pdf'">
|
||||
<vue-office-pdf :src="previewFileData.attachmentUrl" @rendered="renderedHandler" @error="errorHandler" />
|
||||
</div>
|
||||
<div v-if="previewFileData.fileType === 'docx'">
|
||||
<vue-office-docx :src="previewFileData.attachmentUrl" />
|
||||
</div>
|
||||
<div v-if="previewFileData.fileType === 'xlsx'">
|
||||
<vue-office-excel :src="previewFileData.attachmentUrl" />
|
||||
</div>
|
||||
<div v-if="previewFileData.fileType === 'pptx'">
|
||||
<vue-office-pptx :src="previewFileData.attachmentUrl" />
|
||||
</div>
|
||||
<div v-if="previewFileData.fileType === 'image'">
|
||||
<el-image :src="previewFileData.attachmentUrl" style="height: 20vh;" />
|
||||
</div>
|
||||
<div
|
||||
v-if="previewFileData.fileType === 'doc' || previewFileData.fileType === 'xls' || previewFileData.fileType === 'ppt'">
|
||||
<el-alert title="该文件类型不支持在线预览,请下载文件查看" type="warning" :closable="false" show-icon />
|
||||
<el-link href='https://ext.se.360.cn/#/extension-detail?id=mnlikebgdnocfckeochjapicehfkklal'
|
||||
target="_blank">或安装360浏览器扩展“SE Office”免下载查看</el-link>
|
||||
</div>
|
||||
<div>
|
||||
<br />
|
||||
<span style="font-size: 14px; color: #666; margin-right: 10px;">下载文件:</span>
|
||||
<el-link :href="previewFileData.attachmentUrl" target="_blank" style="font-size: 14px; color: #409eff;">
|
||||
<el-icon>
|
||||
<Download />
|
||||
</el-icon>
|
||||
{{ previewFileData.fileName }}
|
||||
</el-link>
|
||||
</div>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 回收站弹窗 -->
|
||||
<el-dialog v-model="recycleDialogVisible" title="回收站" width="60%">
|
||||
<div>
|
||||
<el-row :gutter="12" align="middle" style="margin-bottom:12px">
|
||||
<el-col :span="12" style="display: flex; align-items: center;">
|
||||
<span style="font-size: 16px; margin-right: 16px;">标题:</span>
|
||||
<el-input v-model="recycleSearch" placeholder="按文件标题搜索" style="width:200px" clearable />
|
||||
<el-button type="primary" @click="loadRecycleData" style="margin-left: 8px;">搜索</el-button>
|
||||
<el-button @click="recycleSearch = ''; loadRecycleData()" style="margin-left: 4px;">重置</el-button>
|
||||
</el-col>
|
||||
<el-col :span="12" style="text-align: right;">
|
||||
<el-button type="success" :disabled="recycleTableData.length === 0"
|
||||
@click="handleRecoverAll">一键恢复</el-button>
|
||||
<el-button type="danger" :disabled="recycleTableData.length === 0" @click="handleForceDeleteAll"
|
||||
style="margin-left: 8px;">一键删除</el-button>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-table :data="recycleTableData" stripe border style="width:100%">
|
||||
<el-table-column prop="id" label="ID" width="80" />
|
||||
<el-table-column prop="title" label="文件标题" min-width="240" show-overflow-tooltip />
|
||||
<el-table-column prop="updatedTime" label="删除时间" width="200">
|
||||
<template #default="{ row }">{{ row.updatedTime ? new Date(row.updatedTime).toLocaleString() : ''
|
||||
}}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="220" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" size="small" @click="handleRecover(row)">恢复</el-button>
|
||||
<el-button type="danger" size="small" @click="handleForceDelete(row)">永久删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, computed, onMounted } from 'vue'
|
||||
import { ref, reactive, computed, onMounted, watch } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import VueOfficePdf from '@vue-office/pdf'
|
||||
import VueOfficeDocx from '@vue-office/docx'
|
||||
import VueOfficeExcel from '@vue-office/excel'
|
||||
import VueOfficePptx from '@vue-office/pptx'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
@@ -192,69 +306,9 @@ const pagination = reactive({
|
||||
// 计算属性
|
||||
const hasSelection = computed(() => selectedRows.value.length > 0)
|
||||
|
||||
// 模拟数据
|
||||
const mockData = [
|
||||
{
|
||||
id: 1,
|
||||
title: '福建省教育装备与基建中心2024年工作要点',
|
||||
fileName: '2024年工作要点.pdf',
|
||||
fileType: 'pdf',
|
||||
fileSize: '2.5MB',
|
||||
downloadCount: 1250,
|
||||
author: '张三',
|
||||
status: 'published',
|
||||
uploadTime: '2024-01-15 10:30:00',
|
||||
updateTime: '2024-01-15 10:30:00'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: '教育装备采购技术规范指南',
|
||||
fileName: '采购技术规范.docx',
|
||||
fileType: 'doc',
|
||||
fileSize: '1.8MB',
|
||||
downloadCount: 890,
|
||||
author: '李四',
|
||||
status: 'published',
|
||||
uploadTime: '2024-01-14 14:20:00',
|
||||
updateTime: '2024-01-14 14:20:00'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: '智慧校园建设方案',
|
||||
fileName: '智慧校园方案.pptx',
|
||||
fileType: 'ppt',
|
||||
fileSize: '5.2MB',
|
||||
downloadCount: 0,
|
||||
author: '王五',
|
||||
status: 'draft',
|
||||
uploadTime: '',
|
||||
updateTime: '2024-01-13 16:45:00'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
title: '教育装备使用手册',
|
||||
fileName: '使用手册.zip',
|
||||
fileType: 'zip',
|
||||
fileSize: '15.6MB',
|
||||
downloadCount: 456,
|
||||
author: '赵六',
|
||||
status: 'offline',
|
||||
uploadTime: '2024-01-12 09:15:00',
|
||||
updateTime: '2024-01-12 09:15:00'
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
title: '教育装备产品图片集',
|
||||
fileName: '产品图片集.zip',
|
||||
fileType: 'image',
|
||||
fileSize: '28.3MB',
|
||||
downloadCount: 2100,
|
||||
author: '钱七',
|
||||
status: 'published',
|
||||
uploadTime: '2024-01-11 11:30:00',
|
||||
updateTime: '2024-01-11 11:30:00'
|
||||
}
|
||||
]
|
||||
// 预览弹窗
|
||||
const previewDialogVisible = ref(false)
|
||||
const previewFileData = ref({})
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
@@ -264,16 +318,39 @@ onMounted(() => {
|
||||
// 加载数据
|
||||
const loadData = () => {
|
||||
loading.value = true
|
||||
setTimeout(() => {
|
||||
tableData.value = mockData
|
||||
pagination.total = mockData.length
|
||||
fetch('http://localhost:3000/api/files?page=' + pagination.currentPage + '&pageSize=' + pagination.pageSize + '&' + buildSearchParams(), {
|
||||
method: 'GET'
|
||||
}).then(response => response.json()).then(data => {
|
||||
tableData.value = data.data
|
||||
pagination.total = data.pagination.total
|
||||
loading.value = false
|
||||
}, 500)
|
||||
}).catch(error => {
|
||||
ElMessage.error('加载数据失败' + error.message)
|
||||
loading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
// 构建搜索参数
|
||||
const buildSearchParams = () => {
|
||||
const params = new URLSearchParams()
|
||||
params.append('isDeleted', 0)
|
||||
if (searchForm.title) {
|
||||
params.append('title', searchForm.title)
|
||||
}
|
||||
if (searchForm.fileType) {
|
||||
params.append('fileType', searchForm.fileType)
|
||||
}
|
||||
if (searchForm.status) {
|
||||
params.append('status', searchForm.status)
|
||||
}
|
||||
if (searchForm.dateRange && searchForm.dateRange.length === 2) {
|
||||
params.append('startDate', searchForm.dateRange[0])
|
||||
params.append('endDate', searchForm.dateRange[1])
|
||||
}
|
||||
return params.toString()
|
||||
}
|
||||
// 搜索
|
||||
const handleSearch = () => {
|
||||
ElMessage.info('搜索功能开发中...')
|
||||
loadData()
|
||||
}
|
||||
|
||||
@@ -298,40 +375,117 @@ const handleEdit = (row) => {
|
||||
router.push(`/admin/content/downloads/${row.id}`)
|
||||
}
|
||||
|
||||
// 查看
|
||||
// 预览
|
||||
const handleView = (row) => {
|
||||
ElMessage.info(`查看文件: ${row.title}`)
|
||||
previewDialogVisible.value = true
|
||||
previewFileData.value = row
|
||||
previewFileData.value.fileType = row.fileName.split('.').pop().toLowerCase()
|
||||
}
|
||||
|
||||
// 删除
|
||||
const handleDelete = async (row) => {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定要删除文件"${row.title}"吗?`,
|
||||
'确认删除',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
ElMessageBox.confirm(`确定要删除文件"${row.title}"吗?`, '确认删除', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
}).then(() => {
|
||||
fetch(`http://localhost:3000/api/files/${row.id}`, {
|
||||
method: 'DELETE',
|
||||
}).then(response => response.json()).then(data => {
|
||||
if (data.success) {
|
||||
ElMessage.success('删除成功')
|
||||
loadData()
|
||||
} else {
|
||||
ElMessage.error(data.message)
|
||||
}
|
||||
)
|
||||
ElMessage.success('删除成功')
|
||||
loadData()
|
||||
} catch {
|
||||
// 用户取消
|
||||
}
|
||||
}).catch(error => {
|
||||
ElMessage.error('删除失败' + error.message)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// 上架
|
||||
const handlePublish = (row) => {
|
||||
ElMessage.success(`文件"${row.title}"已上架`)
|
||||
row.status = 'published'
|
||||
ElMessageBox.confirm(`确定要上架文件"${row.title}"吗?`, '确认上架', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
}).then(() => {
|
||||
handlePublishFiles([row.id])
|
||||
})
|
||||
}
|
||||
|
||||
// 下架
|
||||
const handleOffline = (row) => {
|
||||
ElMessage.warning(`文件"${row.title}"已下架`)
|
||||
row.status = 'offline'
|
||||
ElMessageBox.confirm(`确定要下架文件"${row.title}"吗?`, '确认下架', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
}).then(() => {
|
||||
handleOfflineFiles([row.id])
|
||||
})
|
||||
}
|
||||
|
||||
// 批量上架
|
||||
const handleBatchPublish = () => {
|
||||
ElMessageBox.confirm(`确定要上架 ${selectedRows.value.length} 个文件吗?`, '确认上架', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
}).then(() => {
|
||||
handlePublishFiles(selectedRows.value.map(row => row.id))
|
||||
})
|
||||
}
|
||||
|
||||
// 批量下架
|
||||
const handleBatchOffline = () => {
|
||||
ElMessageBox.confirm(`确定要下架 ${selectedRows.value.length} 个文件吗?`, '确认下架', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
}).then(() => {
|
||||
handleOfflineFiles(selectedRows.value.map(row => row.id))
|
||||
})
|
||||
}
|
||||
|
||||
// 上架请求
|
||||
const handlePublishFiles = (ids) => {
|
||||
fetch('http://localhost:3000/api/files/publish', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(ids),
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
}).then(response => response.json()).then(data => {
|
||||
if (data.success) {
|
||||
ElMessage.success('上架成功')
|
||||
loadData()
|
||||
} else {
|
||||
ElMessage.error(data.message)
|
||||
}
|
||||
}).catch(error => {
|
||||
ElMessage.error('上架失败' + error.message)
|
||||
})
|
||||
}
|
||||
|
||||
// 下架请求
|
||||
const handleOfflineFiles = (ids) => {
|
||||
fetch('http://localhost:3000/api/files/unpublish', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(ids),
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
}).then(response => response.json()).then(data => {
|
||||
if (data.success) {
|
||||
ElMessage.success('下架成功')
|
||||
loadData()
|
||||
} else {
|
||||
ElMessage.error(data.message)
|
||||
}
|
||||
}).catch(error => {
|
||||
ElMessage.error('下架失败' + error.message)
|
||||
})
|
||||
}
|
||||
|
||||
// 导入
|
||||
@@ -344,34 +498,6 @@ const handleExport = () => {
|
||||
ElMessage.info('导出功能开发中...')
|
||||
}
|
||||
|
||||
// 批量上架
|
||||
const handleBatchPublish = () => {
|
||||
if (selectedRows.value.length === 0) {
|
||||
ElMessage.warning('请先选择要上架的文件')
|
||||
return
|
||||
}
|
||||
|
||||
ElMessage.success(`已批量上架 ${selectedRows.value.length} 个文件`)
|
||||
selectedRows.value.forEach(row => {
|
||||
row.status = 'published'
|
||||
})
|
||||
selectedRows.value = []
|
||||
}
|
||||
|
||||
// 批量下架
|
||||
const handleBatchOffline = () => {
|
||||
if (selectedRows.value.length === 0) {
|
||||
ElMessage.warning('请先选择要下架的文件')
|
||||
return
|
||||
}
|
||||
|
||||
ElMessage.warning(`已批量下架 ${selectedRows.value.length} 个文件`)
|
||||
selectedRows.value.forEach(row => {
|
||||
row.status = 'offline'
|
||||
})
|
||||
selectedRows.value = []
|
||||
}
|
||||
|
||||
// 选择变化
|
||||
const handleSelectionChange = (selection) => {
|
||||
selectedRows.value = selection
|
||||
@@ -390,12 +516,16 @@ const handleCurrentChange = (val) => {
|
||||
}
|
||||
|
||||
// 获取文件类型名称
|
||||
const getFileTypeName = (fileType) => {
|
||||
const getFileTypeName = (fileName) => {
|
||||
const fileType = fileName.split('.').pop().toLowerCase()
|
||||
const typeMap = {
|
||||
pdf: 'PDF文档',
|
||||
doc: 'Word文档',
|
||||
docx: 'Word文档',
|
||||
xls: 'Excel表格',
|
||||
xlsx: 'Excel表格',
|
||||
ppt: 'PPT演示',
|
||||
pptx: 'PPT演示',
|
||||
image: '图片文件',
|
||||
zip: '压缩包',
|
||||
other: '其他'
|
||||
@@ -404,12 +534,16 @@ const getFileTypeName = (fileType) => {
|
||||
}
|
||||
|
||||
// 获取文件类型标签类型
|
||||
const getFileTypeTagType = (fileType) => {
|
||||
const getFileTypeTagType = (fileName) => {
|
||||
const fileType = fileName.split('.').pop().toLowerCase()
|
||||
const typeMap = {
|
||||
pdf: 'danger',
|
||||
doc: 'primary',
|
||||
docx: 'primary',
|
||||
xls: 'success',
|
||||
xlsx: 'success',
|
||||
ppt: 'warning',
|
||||
pptx: 'warning',
|
||||
image: 'info',
|
||||
zip: '',
|
||||
other: 'info'
|
||||
@@ -418,12 +552,16 @@ const getFileTypeTagType = (fileType) => {
|
||||
}
|
||||
|
||||
// 获取文件类型图标
|
||||
const getFileTypeIcon = (fileType) => {
|
||||
const getFileTypeIcon = (fileName) => {
|
||||
const fileType = fileName.split('.').pop().toLowerCase()
|
||||
const iconMap = {
|
||||
pdf: 'Document',
|
||||
doc: 'Document',
|
||||
docx: 'Document',
|
||||
xls: 'Document',
|
||||
xlsx: 'Document',
|
||||
ppt: 'Document',
|
||||
pptx: 'Document',
|
||||
image: 'Picture',
|
||||
zip: 'Folder',
|
||||
other: 'Document'
|
||||
@@ -432,12 +570,16 @@ const getFileTypeIcon = (fileType) => {
|
||||
}
|
||||
|
||||
// 获取文件类型颜色
|
||||
const getFileTypeColor = (fileType) => {
|
||||
const getFileTypeColor = (fileName) => {
|
||||
const fileType = fileName.split('.').pop().toLowerCase()
|
||||
const colorMap = {
|
||||
pdf: '#f56c6c',
|
||||
doc: '#409eff',
|
||||
docx: '#409eff',
|
||||
xls: '#67c23a',
|
||||
xlsx: '#67c23a',
|
||||
ppt: '#e6a23c',
|
||||
pptx: '#e6a23c',
|
||||
image: '#909399',
|
||||
zip: '#606266',
|
||||
other: '#909399'
|
||||
@@ -445,25 +587,128 @@ const getFileTypeColor = (fileType) => {
|
||||
return colorMap[fileType] || '#909399'
|
||||
}
|
||||
|
||||
// 获取状态名称
|
||||
const getStatusName = (status) => {
|
||||
const statusMap = {
|
||||
draft: '草稿',
|
||||
published: '已发布',
|
||||
offline: '已下架'
|
||||
// 获取文章状态数组
|
||||
const getArticleStatuses = (row) => {
|
||||
const statuses = []
|
||||
// 基础状态
|
||||
if (row.isDraft === 1) {
|
||||
statuses.push({ key: 'draft', name: '草稿', type: 'info' })
|
||||
} else if (row.isPublished === 1) {
|
||||
statuses.push({ key: 'published', name: '已发布', type: 'success' })
|
||||
} else {
|
||||
statuses.push({ key: 'offline', name: '已下架', type: 'warning' })
|
||||
}
|
||||
return statusMap[status] || status
|
||||
// 特殊状态标签
|
||||
if (row.isTop === 1) {
|
||||
statuses.push({ key: 'top', name: '置顶', type: 'danger' })
|
||||
}
|
||||
if (row.isRecommended === 1) {
|
||||
statuses.push({ key: 'recommended', name: '推荐', type: 'primary' })
|
||||
}
|
||||
if (row.isHot === 1) {
|
||||
statuses.push({ key: 'hot', name: '热点', type: 'warning' })
|
||||
}
|
||||
if (row.isSlideshow === 1) {
|
||||
statuses.push({ key: 'slideshow', name: '幻灯', type: 'success' })
|
||||
}
|
||||
return statuses
|
||||
}
|
||||
|
||||
// 获取状态类型
|
||||
const getStatusType = (status) => {
|
||||
const typeMap = {
|
||||
draft: 'info',
|
||||
published: 'success',
|
||||
offline: 'warning'
|
||||
}
|
||||
return typeMap[status] || ''
|
||||
// 回收站相关
|
||||
const recycleDialogVisible = ref(false)
|
||||
const recycleTableData = ref([])
|
||||
const recycleSearch = ref('')
|
||||
|
||||
const openRecycleBin = () => {
|
||||
recycleDialogVisible.value = true
|
||||
loadRecycleData()
|
||||
}
|
||||
|
||||
// 加载回收站数据
|
||||
const loadRecycleData = () => {
|
||||
recycleTableData.value = []
|
||||
fetch('http://localhost:3000/api/files?isDeleted=1&title=' + recycleSearch.value, {
|
||||
method: 'GET'
|
||||
}).then(response => response.json()).then(data => {
|
||||
recycleTableData.value = data.data
|
||||
})
|
||||
}
|
||||
|
||||
// 恢复回收站文件
|
||||
const handleRecover = (row) => {
|
||||
ElMessageBox.confirm(`确定恢复文件 "${row.title}" 吗?`, '确认恢复', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
fetch(`http://localhost:3000/api/files/recover/${row.id}`, {
|
||||
method: 'POST'
|
||||
}).then(response => response.json()).then(data => {
|
||||
if (data.success) {
|
||||
ElMessage.success('恢复成功')
|
||||
loadRecycleData()
|
||||
if (recycleTableData.value.length === 0) {
|
||||
recycleDialogVisible.value = false
|
||||
}
|
||||
} else {
|
||||
ElMessage.error(data.message)
|
||||
}
|
||||
}).catch(error => {
|
||||
ElMessage.error('恢复失败' + error.message)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// 永久删除回收站文件
|
||||
const handleForceDelete = (row) => {
|
||||
ElMessageBox.confirm(`永久删除文件 "${row.title}"? 此操作不可恢复。`, '永久删除', {
|
||||
confirmButtonText: '删除',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
fetch(`http://localhost:3000/api/files/force/${row.id}`, {
|
||||
method: 'DELETE'
|
||||
}).then(response => response.json()).then(data => {
|
||||
if (data.success) {
|
||||
ElMessage.success('已永久删除')
|
||||
loadRecycleData()
|
||||
if (recycleTableData.value.length === 0) {
|
||||
recycleDialogVisible.value = false
|
||||
}
|
||||
} else {
|
||||
ElMessage.error(data.message)
|
||||
}
|
||||
}).catch(error => {
|
||||
ElMessage.error('删除失败' + error.message)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// 一键恢复全部
|
||||
const handleRecoverAll = () => {
|
||||
ElMessageBox.confirm('确定一键恢复回收站中的全部文件吗?', '确认恢复全部', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }).then(() => {
|
||||
// 实际实现调用 API 批量恢复
|
||||
ElMessage.success('一键恢复(占位)')
|
||||
loadRecycleData()
|
||||
loadData()
|
||||
})
|
||||
}
|
||||
|
||||
// 一键删除全部
|
||||
const handleForceDeleteAll = () => {
|
||||
ElMessageBox.confirm('确定要永久删除回收站中的全部文件吗?此操作不可恢复。', '永久删除全部', { confirmButtonText: '删除', cancelButtonText: '取消', type: 'warning' }).then(() => {
|
||||
// 实际实现调用 API 批量永久删除
|
||||
ElMessage.success('一键删除(占位)')
|
||||
loadRecycleData()
|
||||
})
|
||||
}
|
||||
|
||||
// 监控:当回收站弹窗关闭时,加载数据
|
||||
watch(recycleDialogVisible, (newVal) => {
|
||||
if (!newVal) {
|
||||
loadData()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -52,12 +52,6 @@
|
||||
</el-icon>
|
||||
新增服务对象
|
||||
</el-button>
|
||||
<el-button type="danger" :disabled="!hasSelection" @click="handleBatchDelete">
|
||||
<el-icon>
|
||||
<Delete />
|
||||
</el-icon>
|
||||
批量删除
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="action-right">
|
||||
<el-button @click="handleImport">
|
||||
@@ -66,11 +60,29 @@
|
||||
</el-icon>
|
||||
导入
|
||||
</el-button>
|
||||
<el-button @click="handleExport">
|
||||
<el-dropdown @command="handleExport" trigger="click">
|
||||
<el-button>
|
||||
<el-icon>
|
||||
<Download />
|
||||
</el-icon>
|
||||
导出
|
||||
<el-icon class="el-icon--right">
|
||||
<ArrowDown />
|
||||
</el-icon>
|
||||
</el-button>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item command="json">导出为JSON</el-dropdown-item>
|
||||
<el-dropdown-item command="xlsx">导出为Excel</el-dropdown-item>
|
||||
<el-dropdown-item command="csv">导出为CSV</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
<el-button @click="openRecycleBin" type="danger">
|
||||
<el-icon>
|
||||
<Download />
|
||||
<Delete />
|
||||
</el-icon>
|
||||
导出
|
||||
回收站
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -88,12 +100,25 @@
|
||||
{{ row.gender === 1 ? '男' : '女' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="birthDate" label="出生年月" width="120" />
|
||||
<el-table-column prop="nation" label="民族" width="100" />
|
||||
<el-table-column prop="phone" label="电话号码" width="120" />
|
||||
<el-table-column prop="birthDate" label="出生年月" width="110" >
|
||||
<template #default="{ row }">
|
||||
{{ row.birthDate ? new Date(row.birthDate).toLocaleDateString() : '' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="nation" label="民族" width="100" >
|
||||
<template #default="{ row }">
|
||||
{{ row.nation ? nationOptions.find(item => item.value === row.nation)?.label : '' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="livingCondition" label="居住情况" width="150" show-overflow-tooltip />
|
||||
<el-table-column prop="selfCare" label="自理情况" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getSelfCareType(row.selfCare)">{{ getSelfCareName(row.selfCare) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="address" label="家庭住址" min-width="200" show-overflow-tooltip />
|
||||
<el-table-column prop="contact" label="家属联系人" width="120" />
|
||||
<el-table-column prop="phone" label="电话号码" width="120" />
|
||||
<el-table-column label="操作" width="160" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" size="small" @click="handleEdit(row)">
|
||||
@@ -135,7 +160,12 @@
|
||||
<el-radio-group v-model="form.gender">
|
||||
<el-radio :label="1">男</el-radio>
|
||||
<el-radio :label="2">女</el-radio>
|
||||
</el-radio-group>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="电话号码" prop="phone">
|
||||
<el-input v-model="form.phone" placeholder="请输入电话号码" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
@@ -160,7 +190,7 @@
|
||||
<el-date-picker v-model="form.partyJoinDate" type="date" placeholder="选择入党时间" style="width: 100%" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="24">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="现享受待遇" prop="currentBenefit">
|
||||
<el-input v-model="form.currentBenefit" placeholder="请输入现享受待遇" />
|
||||
</el-form-item>
|
||||
@@ -195,8 +225,8 @@
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="电话号码" prop="phone">
|
||||
<el-input v-model="form.phone" placeholder="请输入电话号码" />
|
||||
<el-form-item label="家属联系电话" prop="contactPhone">
|
||||
<el-input v-model="form.contactPhone" placeholder="请输入家属联系电话" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
@@ -208,13 +238,47 @@
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 回收站弹窗 -->
|
||||
<el-dialog v-model="recycleDialogVisible" title="回收站" width="60%">
|
||||
<div>
|
||||
<el-row :gutter="12" align="middle" style="margin-bottom:12px">
|
||||
<el-col :span="12" style="display: flex; align-items: center;">
|
||||
<span style="font-size: 16px; margin-right: 16px;">姓名:</span>
|
||||
<el-input v-model="recycleSearch" placeholder="按姓名搜索" style="width:200px" clearable />
|
||||
<el-button type="primary" @click="loadRecycleData" style="margin-left: 8px;">搜索</el-button>
|
||||
<el-button @click="recycleSearch = ''; loadRecycleData()" style="margin-left: 4px;">重置</el-button>
|
||||
</el-col>
|
||||
<el-col :span="12" style="text-align: right;">
|
||||
<el-button type="success" :disabled="recycleTableData.length === 0"
|
||||
@click="handleRecoverAll">一键恢复</el-button>
|
||||
<el-button type="danger" :disabled="recycleTableData.length === 0" @click="handleForceDeleteAll"
|
||||
style="margin-left: 8px;">一键删除</el-button>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-table :data="recycleTableData" stripe border style="width:100%">
|
||||
<el-table-column prop="id" label="ID" width="80" />
|
||||
<el-table-column prop="name" label="姓名" min-width="240" show-overflow-tooltip />
|
||||
<el-table-column prop="updateTime" label="删除时间" width="200">
|
||||
<template #default="{ row }">{{ row.updateTime ? new Date(row.updateTime).toLocaleString() : ''
|
||||
}}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="220" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" size="small" @click="handleRecover(row)">恢复</el-button>
|
||||
<el-button type="danger" size="small" @click="handleForceDelete(row)">永久删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, computed, onMounted } from 'vue'
|
||||
import { ref, reactive, computed, onMounted, watch } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Search, Refresh, Plus, Delete, Edit, Upload, Download } from '@element-plus/icons-vue'
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = reactive({
|
||||
@@ -303,6 +367,11 @@ const getSelfCareName = (status) => {
|
||||
return names[status] || '-'
|
||||
}
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
loadData()
|
||||
})
|
||||
|
||||
// 搜索
|
||||
const handleSearch = () => {
|
||||
loadData()
|
||||
@@ -354,24 +423,18 @@ const handleDelete = (row) => {
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
ElMessage.success('删除成功')
|
||||
loadData()
|
||||
})
|
||||
}
|
||||
|
||||
// 批量删除
|
||||
const handleBatchDelete = () => {
|
||||
if (selectedRows.value.length === 0) {
|
||||
ElMessage.warning('请选择要删除的记录')
|
||||
return
|
||||
}
|
||||
ElMessageBox.confirm(`确定要删除选中的${selectedRows.value.length}条记录吗?`, '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
ElMessage.success('删除成功')
|
||||
loadData()
|
||||
fetch(`http://localhost:3000/api/services/${row.id}`, {
|
||||
method: 'DELETE'
|
||||
}).then(response => response.json()).then(data => {
|
||||
if (data.success) {
|
||||
ElMessage.success('删除成功')
|
||||
loadData()
|
||||
} else {
|
||||
ElMessage.error(data.message)
|
||||
}
|
||||
}).catch(error => {
|
||||
ElMessage.error('删除失败' + error.message)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -390,89 +453,55 @@ const handleSubmit = async () => {
|
||||
if (!formRef.value) return
|
||||
await formRef.value.validate((valid) => {
|
||||
if (valid) {
|
||||
ElMessage.success(dialogType.value === 'add' ? '添加成功' : '修改成功')
|
||||
dialogVisible.value = false
|
||||
loadData()
|
||||
fetch("http://localhost:3000/api/services/" + (dialogType.value === 'add' ? 0 : form.id), {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(form),
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
}).then(response => response.json()).then(data => {
|
||||
if (data.success) {
|
||||
ElMessage.success('添加成功')
|
||||
dialogVisible.value = false
|
||||
loadData()
|
||||
} else {
|
||||
ElMessage.error(data.message)
|
||||
}
|
||||
}).catch(error => {
|
||||
ElMessage.error('添加失败' + error.message)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 加载数据(使用静态模拟数据并支持简单过滤与分页)
|
||||
// 加载数据
|
||||
const loadData = () => {
|
||||
loading.value = true
|
||||
setTimeout(() => {
|
||||
// 生成静态模拟数据(总数可调整)
|
||||
const totalMock = 57
|
||||
const all = Array.from({ length: totalMock }).map((_, i) => {
|
||||
const id = i + 1
|
||||
const nationIdx = i % nationOptions.value.length
|
||||
const selfCareVal = String((i % 3) + 1)
|
||||
const gender = i % 2 === 0 ? 1 : 2
|
||||
const birth = new Date(1940 + (i % 60), (i % 12), 1).toISOString().slice(0, 10)
|
||||
const workStart = new Date(1960 + (i % 50), 0, 1).toISOString().slice(0, 10)
|
||||
const partyJoin = new Date(1965 + (i % 50), 0, 1).toISOString().slice(0, 10)
|
||||
return {
|
||||
id,
|
||||
name: `姓名 ${id}`,
|
||||
gender,
|
||||
birthDate: birth,
|
||||
nationLabel: nationOptions.value[nationIdx].label,
|
||||
nationValue: nationOptions.value[nationIdx].value,
|
||||
workStartDate: workStart,
|
||||
partyJoinDate: partyJoin,
|
||||
currentBenefit: '退休金及补贴',
|
||||
retirementInfo: `单位${id};职务${id}`,
|
||||
selfCare: selfCareVal,
|
||||
livingCondition: i % 2 === 0 ? '独居' : '与家人同住',
|
||||
address: `示例地址 ${id}`,
|
||||
contact: `家属${id}`,
|
||||
phone: `1380000${(1000 + id).toString().slice(-4)}`
|
||||
}
|
||||
})
|
||||
|
||||
// 简单过滤:姓名、民族、自理情况、入党时间范围
|
||||
let filtered = all.filter((row) => {
|
||||
if (searchForm.name && !row.name.includes(searchForm.name)) return false
|
||||
if (searchForm.nation && String(row.nationValue) !== String(searchForm.nation)) return false
|
||||
if (searchForm.selfCare && String(row.selfCare) !== String(searchForm.selfCare)) return false
|
||||
if (searchForm.partyJoinDateRange && searchForm.partyJoinDateRange.length === 2) {
|
||||
const start = new Date(searchForm.partyJoinDateRange[0])
|
||||
const end = new Date(searchForm.partyJoinDateRange[1])
|
||||
const pj = new Date(row.partyJoinDate)
|
||||
if (pj < start || pj > end) return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
// 分页
|
||||
pagination.total = filtered.length
|
||||
const start = (pagination.currentPage - 1) * pagination.pageSize
|
||||
tableData.value = filtered.slice(start, start + pagination.pageSize).map(r => ({
|
||||
id: r.id,
|
||||
name: r.name,
|
||||
gender: r.gender,
|
||||
birthDate: r.birthDate,
|
||||
nation: r.nationLabel,
|
||||
nationValue: r.nationValue,
|
||||
workStartDate: r.workStartDate,
|
||||
partyJoinDate: r.partyJoinDate,
|
||||
currentBenefit: r.currentBenefit,
|
||||
retirementInfo: r.retirementInfo,
|
||||
selfCare: r.selfCare,
|
||||
livingCondition: r.livingCondition,
|
||||
address: r.address,
|
||||
contact: r.contact,
|
||||
phone: r.phone
|
||||
}))
|
||||
|
||||
fetch("http://localhost:3000/api/services?page=" + pagination.currentPage + "&pageSize=" + pagination.pageSize + "&" + buildSearchParams(), {
|
||||
method: 'GET',
|
||||
}).then(response => response.json()).then(data => {
|
||||
tableData.value = data.data
|
||||
pagination.total = data.pagination.total
|
||||
loading.value = false
|
||||
}, 600)
|
||||
}).catch(error => {
|
||||
ElMessage.error('加载失败' + error.message)
|
||||
loading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
// 组件挂载时加载一次数据
|
||||
onMounted(() => {
|
||||
loadData()
|
||||
})
|
||||
// 构建搜索参数
|
||||
const buildSearchParams = () => {
|
||||
const params = new URLSearchParams()
|
||||
params.append('isDeleted', 0)
|
||||
params.append('name', searchForm.name)
|
||||
params.append('nation', searchForm.nation)
|
||||
params.append('selfCare', searchForm.selfCare)
|
||||
if (searchForm.partyJoinDateRange && searchForm.partyJoinDateRange.length === 2) {
|
||||
params.append('startDate', searchForm.partyJoinDateRange[0])
|
||||
params.append('endDate', searchForm.partyJoinDateRange[1])
|
||||
}
|
||||
return params.toString()
|
||||
}
|
||||
|
||||
// 选择变化
|
||||
const handleSelectionChange = (selection) => {
|
||||
@@ -490,6 +519,102 @@ const handleCurrentChange = (val) => {
|
||||
pagination.currentPage = val
|
||||
loadData()
|
||||
}
|
||||
|
||||
// 回收站相关
|
||||
const recycleDialogVisible = ref(false)
|
||||
const recycleTableData = ref([])
|
||||
const recycleSearch = ref('')
|
||||
|
||||
const openRecycleBin = () => {
|
||||
recycleDialogVisible.value = true
|
||||
loadRecycleData()
|
||||
}
|
||||
|
||||
// 加载回收站数据
|
||||
const loadRecycleData = () => {
|
||||
recycleTableData.value = []
|
||||
fetch('http://localhost:3000/api/services?isDeleted=1&name=' + recycleSearch.value, {
|
||||
method: 'GET'
|
||||
}).then(response => response.json()).then(data => {
|
||||
recycleTableData.value = data.data
|
||||
})
|
||||
}
|
||||
|
||||
// 恢复回收站服务对象
|
||||
const handleRecover = (row) => {
|
||||
ElMessageBox.confirm(`确定恢复服务对象 "${row.name}" 吗?`, '确认恢复', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
fetch(`http://localhost:3000/api/services/recover/${row.id}`, {
|
||||
method: 'POST'
|
||||
}).then(response => response.json()).then(data => {
|
||||
if (data.success) {
|
||||
ElMessage.success('恢复成功')
|
||||
loadRecycleData()
|
||||
if (recycleTableData.value.length === 0) {
|
||||
recycleDialogVisible.value = false
|
||||
}
|
||||
} else {
|
||||
ElMessage.error(data.message)
|
||||
}
|
||||
}).catch(error => {
|
||||
ElMessage.error('恢复失败' + error.message)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// 永久删除回收站服务对象
|
||||
const handleForceDelete = (row) => {
|
||||
ElMessageBox.confirm(`永久删除服务对象 "${row.name}"? 此操作不可恢复。`, '永久删除', {
|
||||
confirmButtonText: '删除',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
fetch(`http://localhost:3000/api/services/force/${row.id}`, {
|
||||
method: 'DELETE'
|
||||
}).then(response => response.json()).then(data => {
|
||||
if (data.success) {
|
||||
ElMessage.success('已永久删除')
|
||||
loadRecycleData()
|
||||
if (recycleTableData.value.length === 0) {
|
||||
recycleDialogVisible.value = false
|
||||
}
|
||||
} else {
|
||||
ElMessage.error(data.message)
|
||||
}
|
||||
}).catch(error => {
|
||||
ElMessage.error('删除失败' + error.message)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// 一键恢复全部
|
||||
const handleRecoverAll = () => {
|
||||
ElMessageBox.confirm('确定一键恢复回收站中的全部服务对象吗?', '确认恢复全部', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }).then(() => {
|
||||
// 实际实现调用 API 批量恢复
|
||||
ElMessage.success('一键恢复(占位)')
|
||||
loadRecycleData()
|
||||
loadData()
|
||||
})
|
||||
}
|
||||
|
||||
// 一键删除全部
|
||||
const handleForceDeleteAll = () => {
|
||||
ElMessageBox.confirm('确定要永久删除回收站中的全部服务对象吗?此操作不可恢复。', '永久删除全部', { confirmButtonText: '删除', cancelButtonText: '取消', type: 'warning' }).then(() => {
|
||||
// 实际实现调用 API 批量永久删除
|
||||
ElMessage.success('一键删除(占位)')
|
||||
loadRecycleData()
|
||||
})
|
||||
}
|
||||
|
||||
// 监控:当回收站弹窗关闭时,加载数据
|
||||
watch(recycleDialogVisible, (newVal) => {
|
||||
if (!newVal) {
|
||||
loadData()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -181,46 +181,49 @@
|
||||
@size-change="handleSizeChange" @current-change="handleCurrentChange" />
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<el-dialog v-model="showVideoDialog" title="视频播放" width="80%">
|
||||
<h2 :style="{ color: videoDialogData.titleColor, textAlign: 'center' }">{{ videoDialogData.title }}</h2>
|
||||
<p style="font-size: 12px; color: #666; margin-bottom: 10px;">发布时间:{{ new
|
||||
Date(videoDialogData.publishTime).toLocaleString() }}</p>
|
||||
<div v-html="videoDialogData.content"></div>
|
||||
<video :src="videoDialogData.videoUrl" controls style="width: 200px" />
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-model="recycleDialogVisible" title="回收站" width="60%">
|
||||
<div>
|
||||
<el-row :gutter="12" align="middle" style="margin-bottom:12px">
|
||||
<el-col :span="12" style="display: flex; align-items: center;">
|
||||
<span style="font-size: 16px; margin-right: 16px;">标题:</span>
|
||||
<el-input v-model="recycleSearch" placeholder="按文章标题搜索" style="width:200px" clearable />
|
||||
<el-button type="primary" @click="loadRecycleData" style="margin-left: 8px;">搜索</el-button>
|
||||
<el-button @click="recycleSearch = ''; loadRecycleData()" style="margin-left: 4px;">重置</el-button>
|
||||
</el-col>
|
||||
<el-col :span="12" style="text-align: right;">
|
||||
<el-button type="success" :disabled="recycleTableData.length === 0"
|
||||
@click="handleRecoverAll">一键恢复</el-button>
|
||||
<el-button type="danger" :disabled="recycleTableData.length === 0" @click="handleForceDeleteAll"
|
||||
style="margin-left: 8px;">一键删除</el-button>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-table :data="recycleTableData" stripe border style="width:100%">
|
||||
<el-table-column prop="id" label="ID" width="80" />
|
||||
<el-table-column prop="title" label="文章名称" min-width="240" show-overflow-tooltip />
|
||||
<el-table-column prop="updatedTime" label="删除时间" width="200">
|
||||
<template #default="{ row }">{{ row.updatedTime ? new Date(row.updatedTime).toLocaleString() : ''
|
||||
}}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="220" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" size="small" @click="handleRecover(row)">恢复</el-button>
|
||||
<el-button type="danger" size="small" @click="handleForceDelete(row)">永久删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
|
||||
<el-dialog v-model="showVideoDialog" title="视频播放" width="80%">
|
||||
<h3>{{ videoDialogData.title }}</h3>
|
||||
<p>{{ videoDialogData.content }}</p>
|
||||
<video :src="videoDialogData.videoUrl" controls style="width: 200px" />
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-model="recycleDialogVisible" title="回收站" width="60%">
|
||||
<div>
|
||||
<el-row :gutter="12" align="middle" style="margin-bottom:12px">
|
||||
<el-col :span="12" style="display: flex; align-items: center;">
|
||||
<span style="font-size: 16px; margin-right: 16px;">标题:</span>
|
||||
<el-input v-model="recycleSearch" placeholder="按文章标题搜索" style="width:200px" clearable />
|
||||
<el-button type="primary" @click="loadRecycleData" style="margin-left: 8px;">搜索</el-button>
|
||||
<el-button @click="recycleSearch = ''; loadRecycleData()" style="margin-left: 4px;">重置</el-button>
|
||||
</el-col>
|
||||
<el-col :span="12" style="text-align: right;">
|
||||
<el-button type="success" :disabled="recycleTableData.length === 0" @click="handleRecoverAll">一键恢复</el-button>
|
||||
<el-button type="danger" :disabled="recycleTableData.length === 0" @click="handleForceDeleteAll"
|
||||
style="margin-left: 8px;">一键删除</el-button>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-table :data="recycleTableData" stripe border style="width:100%">
|
||||
<el-table-column prop="id" label="ID" width="80" />
|
||||
<el-table-column prop="title" label="文章名称" min-width="240" show-overflow-tooltip />
|
||||
<el-table-column prop="updatedTime" label="删除时间" width="200">
|
||||
<template #default="{ row }">{{ row.updatedTime ? new Date(row.updatedTime).toLocaleString() : ''
|
||||
}}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="220" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" size="small" @click="handleRecover(row)">恢复</el-button>
|
||||
<el-button type="danger" size="small" @click="handleForceDelete(row)">永久删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
|
||||
Reference in New Issue
Block a user