Files
ApiServer-Web-admin_dashboa…/src/components/admin/AvatarSelector.vue
T
shiran a827fc5c41
Build and Deploy Vue3 / build (push) Successful in 1m27s
Build and Deploy Vue3 / deploy (push) Successful in 34s
feat(system): 通知管理与文件选择器来源筛选
- 新增通知管理(渠道卡片化、模板 CRUD、参数按钮插入)

- ImageSelector/AvatarSelector 增加上传来源 is_admin 筛选

- 宿主机详情页实时指标与硬件/网卡 IPv6 展示优化

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-04 16:38:47 +08:00

458 lines
12 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<el-dialog
v-model="visible"
:title="title"
width="800px"
append-to-body
@close="handleClose"
>
<div class="avatar-selector">
<el-tabs v-model="activeTab" @tab-click="handleTabClick">
<!-- 文件列表 -->
<el-tab-pane label="文件" name="userFiles">
<div class="file-list-container">
<div class="file-list-header">
<h4>文件列表</h4>
<div class="header-actions">
<el-select v-model="sourceFilter" placeholder="上传来源" clearable size="default" style="width: 120px" @change="handleSourceChange">
<el-option label="管理员" :value="true" />
<el-option label="用户" :value="false" />
</el-select>
<el-button type="primary" @click="switchToUpload" :icon="Upload">
上传新文件
</el-button>
</div>
</div>
<div class="file-grid" v-loading="loading">
<div
v-for="file in fileList"
:key="file.cover_id"
class="file-item"
:class="{ 'selected': selectedId === file.cover_id }"
@click="selectFile(file)"
>
<div class="file-preview">
<img
v-if="isImageFile(file)"
:src="file.url"
:alt="file.realName"
@error="handleImageError"
/>
<el-icon v-else class="file-icon"><Document /></el-icon>
</div>
<div class="file-info">
<p class="file-name">{{ file.realName }}</p>
<p class="file-size">{{ formatFileSize(file.size) }}</p>
</div>
</div>
</div>
<el-empty v-if="fileList.length === 0 && !loading" description="暂无文件" />
<!-- 分页 -->
<div class="pagination-container" v-if="total > 0">
<el-pagination
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:page-sizes="[10, 20, 30, 50]"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
background
@size-change="handleSizeChange"
@current-change="handlePageChange"
/>
</div>
</div>
</el-tab-pane>
<!-- 上传文件 -->
<el-tab-pane label="上传文件" name="upload">
<div class="upload-section">
<el-upload
:http-request="handleUpload"
:before-upload="beforeUpload"
:show-file-list="false"
accept="image/*"
drag
>
<el-icon class="el-icon--upload"><UploadFilled /></el-icon>
<div class="el-upload__text">
将文件拖到此处<em>点击上传</em>
</div>
<template #tip>
<div class="el-upload__tip">
只能上传jpg/png文件且不超过2MB
</div>
</template>
</el-upload>
</div>
</el-tab-pane>
</el-tabs>
</div>
<template #footer>
<div class="dialog-footer">
<el-button @click="handleClose">取消</el-button>
<el-button
type="primary"
@click="handleConfirm"
:disabled="!selectedId"
>
确定选择
</el-button>
</div>
</template>
</el-dialog>
</template>
<script setup>
import { ref, watch } from 'vue'
import { ElMessage } from 'element-plus'
import { Upload, UploadFilled, Document } from '@element-plus/icons-vue'
import { getFileList, getFileDetail, uploadFile } from '@/api/admin/file'
import { closeAllMessage } from '../../utils/message'
// Props
const props = defineProps({
modelValue: {
type: Boolean,
default: false
},
userId: {
type: [String, Number],
required: true
},
currentCoverId: {
type: [String, Number],
default: ''
},
title: {
type: String,
default: '选择文件'
}
})
// Emits
const emit = defineEmits(['update:modelValue', 'confirm'])
// 响应式数据
const visible = ref(false)
const activeTab = ref('userFiles')
const fileList = ref([])
const loading = ref(false)
const selectedId = ref('')
const currentPage = ref(1)
const pageSize = ref(10)
const total = ref(0)
const sourceFilter = ref(undefined)
// 监听 modelValue 变化
watch(() => props.modelValue, (newVal) => {
visible.value = newVal
if (newVal) {
selectedId.value = props.currentCoverId
currentPage.value = 1
sourceFilter.value = undefined
fetchFileList()
}
})
// 监听 visible 变化
watch(visible, (newVal) => {
emit('update:modelValue', newVal)
})
// 获取文件列表
const fetchFileList = async () => {
if (!props.userId) return
loading.value = true
fileList.value = [] // 清空列表
try {
const params = { page: currentPage.value, count: pageSize.value }
if (sourceFilter.value !== undefined && sourceFilter.value !== null && sourceFilter.value !== '') {
params.is_admin = sourceFilter.value
}
const res = await getFileList(params)
console.log("获取文件列表:", res)
if (res.data.code === 200) {
const list = res.data.data.list || []
total.value = res.data.data.all_count || 0
// 获取每个文件的详情
for (let i = 0; i < list.length; i++) {
try {
console.log("获取文件详情:", list[i].id)
const res2 = await getFileDetail({ file_id: list[i].id })
if (res2.data.code === 200) {
fileList.value.push({
url: res2.data.data.url,
cover_id: res2.data.data.data.id,
size: res2.data.data.data.size,
realName: res2.data.data.data.realName
})
}
} catch (error) {
console.error('获取文件详情失败:', error)
}
}
console.log("文件列表1237", fileList.value)
}
} catch (error) {
console.error('获取文件列表失败:', error)
ElMessage.error('获取文件列表失败')
} finally {
loading.value = false
}
}
// 处理标签页切换
const handleTabClick = (tab) => {
if (tab.name === 'userFiles') {
currentPage.value = 1
fetchFileList()
}
}
// 分页处理
const handleSizeChange = (size) => {
pageSize.value = size
currentPage.value = 1
fetchFileList()
}
const handlePageChange = (page) => {
currentPage.value = page
fetchFileList()
}
// 来源筛选变化
const handleSourceChange = () => {
currentPage.value = 1
fetchFileList()
}
// 切换到上传标签页
const switchToUpload = () => {
activeTab.value = 'upload'
}
// 判断是否为图片文件
const isImageFile = (file) => {
const imageTypes = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp']
const extension = file.realName?.split('.').pop()?.toLowerCase()
return imageTypes.includes(extension)
}
// 格式化文件大小
const formatFileSize = (size) => {
if (!size) return '0 B'
const units = ['B', 'KB', 'MB', 'GB']
let unitIndex = 0
let fileSize = size
while (fileSize >= 1024 && unitIndex < units.length - 1) {
fileSize /= 1024
unitIndex++
}
return `${fileSize.toFixed(1)} ${units[unitIndex]}`
}
// 选择文件
const selectFile = (file) => {
selectedId.value = file.cover_id
}
// 上传前验证
const beforeUpload = (file) => {
const isImage = file.type.startsWith('image/')
const isLt2M = file.size / 1024 / 1024 < 2
if (!isImage) {
ElMessage.error('只能上传图片文件!')
return false
}
if (!isLt2M) {
ElMessage.error('图片大小不能超过 2MB!')
return false
}
return true
}
// 自定义上传
const handleUpload = async (options) => {
const { file } = options
const formData = new FormData()
formData.append('files', file)
formData.append('file_names', file.name)
formData.append('update_type', 'cover')
formData.append('open_down', 'true')
try {
const res = await uploadFile(formData)
console.log("上传文件:", res)
if (res.data.code === 200) {
ElMessage.success("上传成功")
// 重置到第一页并刷新文件列表
currentPage.value = 1
await fetchFileList()
// 切换到文件列表标签页
activeTab.value = 'userFiles'
// 自动选择新上传的文件
if (res.data.data?.id) {
selectedId.value = res.data.data.id
}
} else {
ElMessage.error(res.data.msg || '上传失败')
}
} catch (error) {
console.error('上传失败:', error)
ElMessage.error('上传失败')
}
}
// 图片加载错误处理
const handleImageError = (event) => {
event.target.style.display = 'none'
}
// 关闭对话框
const handleClose = () => {
visible.value = false
selectedId.value = ''
fileList.value = []
currentPage.value = 1
total.value = 0
sourceFilter.value = undefined
}
// 确认选择
const handleConfirm = () => {
if (selectedId.value) {
const selectedFile = fileList.value.find(file => file.cover_id === selectedId.value)
emit('confirm', {
cover_id: selectedId.value,
url: selectedFile?.url || ''
})
handleClose()
}
}
</script>
<style scoped>
.avatar-selector {
min-height: 400px;
}
.file-list-container {
padding: 20px 0;
}
.file-list-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.file-list-header h4 {
margin: 0;
color: #303133;
}
.header-actions {
display: flex;
align-items: center;
gap: 10px;
}
.file-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
gap: 16px;
max-height: 400px;
overflow-y: auto;
}
.file-item {
border: 2px solid #e4e7ed;
border-radius: 8px;
padding: 12px;
cursor: pointer;
transition: all 0.3s ease;
text-align: center;
}
.file-item:hover {
border-color: #409EFF;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(64, 158, 255, 0.2);
}
.file-item.selected {
border-color: #409EFF;
background-color: #f0f9ff;
}
.file-preview {
width: 80px;
height: 80px;
margin: 0 auto 8px;
border-radius: 4px;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
background-color: #f5f7fa;
}
.file-preview img {
width: 100%;
height: 100%;
object-fit: cover;
}
.file-preview .file-icon {
font-size: 32px;
color: #909399;
}
.file-info {
text-align: center;
}
.file-name {
font-size: 12px;
color: #303133;
margin: 0 0 4px 0;
word-break: break-all;
line-height: 1.3;
}
.file-size {
font-size: 11px;
color: #909399;
margin: 0;
}
.upload-section {
padding: 40px 20px;
text-align: center;
}
.pagination-container {
margin-top: 20px;
display: flex;
justify-content: center;
}
.dialog-footer {
display: flex;
justify-content: flex-end;
gap: 10px;
}
</style>