实现下载专区、精准服务前后端增删改查交互

This commit is contained in:
2025-10-27 17:52:36 +08:00
parent ce4b9e7c71
commit 52dd775f29
7 changed files with 867 additions and 355 deletions

72
package-lock.json generated
View File

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

View File

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

View File

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

View File

@@ -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('设置保存成功')

View File

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

View File

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

View File

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