<template>
|
<div class="cos-upload-test">
|
<div class="page-card">
|
<h3 class="card-title">腾讯云COS上传测试</h3>
|
|
<!-- 配置信息展示 -->
|
<el-card class="config-card" shadow="never">
|
<template #header>
|
<div class="card-header">
|
<span>COS配置信息</span>
|
<el-button type="primary" size="small" @click="testConnection">
|
测试连接
|
</el-button>
|
</div>
|
</template>
|
<div class="config-info">
|
<div class="config-item">
|
<label>存储桶:</label>
|
<span>{{ cosConfig.bucket }}</span>
|
</div>
|
<div class="config-item">
|
<label>地域:</label>
|
<span>{{ cosConfig.region }}</span>
|
</div>
|
<div class="config-item">
|
<label>状态:</label>
|
<el-tag :type="connectionStatus === 'connected' ? 'success' : connectionStatus === 'error' ? 'danger' : 'info'">
|
{{ connectionStatusText }}
|
</el-tag>
|
</div>
|
</div>
|
</el-card>
|
|
<!-- 文件上传区域 -->
|
<el-card class="upload-card" shadow="never">
|
<template #header>
|
<span>文件上传测试</span>
|
</template>
|
|
<!-- 拖拽上传区域 -->
|
<div class="upload-area">
|
<el-upload
|
ref="uploadRef"
|
class="upload-dragger"
|
drag
|
:auto-upload="false"
|
:show-file-list="false"
|
:on-change="handleFileSelect"
|
accept="image/*,video/*,audio/*,.pdf,.doc,.docx,.txt"
|
multiple
|
>
|
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
|
<div class="el-upload__text">
|
将文件拖到此处,或<em>点击上传</em>
|
</div>
|
<template #tip>
|
<div class="el-upload__tip">
|
支持图片、视频、音频、文档等格式,单个文件不超过100MB
|
</div>
|
</template>
|
</el-upload>
|
</div>
|
|
<!-- 选中的文件列表 -->
|
<div v-if="selectedFiles.length > 0" class="selected-files">
|
<h4>待上传文件 ({{ selectedFiles.length }})</h4>
|
<div class="file-list">
|
<div
|
v-for="(file, index) in selectedFiles"
|
:key="index"
|
class="file-item"
|
>
|
<div class="file-info">
|
<el-icon class="file-icon">
|
<Document v-if="isDocument(file)" />
|
<Picture v-else-if="isImage(file)" />
|
<VideoPlay v-else-if="isVideo(file)" />
|
<Headset v-else-if="isAudio(file)" />
|
<Document v-else />
|
</el-icon>
|
<div class="file-details">
|
<div class="file-name">{{ file.name }}</div>
|
<div class="file-size">{{ formatFileSize(file.size) }}</div>
|
</div>
|
</div>
|
<div class="file-actions">
|
<el-select
|
v-model="file.folder"
|
placeholder="选择存储目录"
|
size="small"
|
style="width: 120px; margin-right: 8px;"
|
>
|
<el-option label="头像" value="avatars/" />
|
<el-option label="文档" value="documents/" />
|
<el-option label="图片" value="images/" />
|
<el-option label="视频" value="videos/" />
|
<el-option label="音频" value="audios/" />
|
<el-option label="其他" value="others/" />
|
</el-select>
|
<el-button
|
type="danger"
|
size="small"
|
@click="removeFile(index)"
|
>
|
移除
|
</el-button>
|
</div>
|
</div>
|
</div>
|
|
<!-- 批量操作 -->
|
<div class="batch-actions">
|
<el-button type="success" @click="uploadAllFiles" :loading="uploading">
|
<el-icon><Upload /></el-icon>
|
上传所有文件
|
</el-button>
|
<el-button @click="clearAllFiles">
|
清空列表
|
</el-button>
|
</div>
|
</div>
|
</el-card>
|
|
<!-- 上传进度 -->
|
<el-card v-if="uploadProgress.length > 0" class="progress-card" shadow="never">
|
<template #header>
|
<span>上传进度</span>
|
</template>
|
<div class="progress-list">
|
<div
|
v-for="(progress, index) in uploadProgress"
|
:key="index"
|
class="progress-item"
|
>
|
<div class="progress-info">
|
<span class="progress-name">{{ progress.fileName }}</span>
|
<span class="progress-status" :class="progress.status">
|
{{ getStatusText(progress.status) }}
|
</span>
|
</div>
|
<el-progress
|
:percentage="progress.percent"
|
:status="progress.status === 'success' ? 'success' : progress.status === 'error' ? 'exception' : undefined"
|
/>
|
<div v-if="progress.url" class="progress-result">
|
<el-link :href="progress.url" target="_blank" type="primary">
|
查看文件
|
</el-link>
|
<el-button
|
type="text"
|
size="small"
|
@click="copyUrl(progress.url)"
|
>
|
复制链接
|
</el-button>
|
</div>
|
</div>
|
</div>
|
</el-card>
|
|
<!-- 上传历史 -->
|
<el-card v-if="uploadHistory.length > 0" class="history-card" shadow="never">
|
<template #header>
|
<div class="card-header">
|
<span>上传历史</span>
|
<el-button type="danger" size="small" @click="clearHistory">
|
清空历史
|
</el-button>
|
</div>
|
</template>
|
<el-table :data="uploadHistory" style="width: 100%">
|
<el-table-column prop="fileName" label="文件名" min-width="200" />
|
<el-table-column prop="folder" label="存储目录" width="120" />
|
<el-table-column prop="size" label="文件大小" width="100">
|
<template #default="{ row }">
|
{{ formatFileSize(row.size) }}
|
</template>
|
</el-table-column>
|
<el-table-column prop="uploadTime" label="上传时间" width="180">
|
<template #default="{ row }">
|
{{ formatTime(row.uploadTime) }}
|
</template>
|
</el-table-column>
|
<el-table-column label="操作" width="200">
|
<template #default="{ row }">
|
<el-button type="primary" size="small" @click="openFile(row.url)">
|
查看
|
</el-button>
|
<el-button type="text" size="small" @click="copyUrl(row.url)">
|
复制链接
|
</el-button>
|
<el-button type="danger" size="small" @click="deleteFile(row)">
|
删除
|
</el-button>
|
</template>
|
</el-table-column>
|
</el-table>
|
</el-card>
|
</div>
|
</div>
|
</template>
|
|
<script setup lang="ts">
|
import { ref, reactive, onMounted, computed } from 'vue'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { uploadToCOS, deleteFromCOS } from '@/utils/cos'
|
import dayjs from 'dayjs'
|
|
// 组件状态
|
const uploading = ref(false)
|
const connectionStatus = ref<'unknown' | 'connected' | 'error'>('unknown')
|
|
// COS配置信息
|
const cosConfig = reactive({
|
bucket: 'ryc-media-1234567890',
|
region: 'ap-chengdu'
|
})
|
|
// 选中的文件列表
|
interface SelectedFile extends File {
|
folder?: string
|
}
|
|
const selectedFiles = ref<SelectedFile[]>([])
|
|
// 上传进度
|
interface UploadProgress {
|
fileName: string
|
percent: number
|
status: 'uploading' | 'success' | 'error'
|
url?: string
|
error?: string
|
}
|
|
const uploadProgress = ref<UploadProgress[]>([])
|
|
// 上传历史
|
interface UploadHistory {
|
fileName: string
|
folder: string
|
size: number
|
url: string
|
uploadTime: Date
|
key: string
|
}
|
|
const uploadHistory = ref<UploadHistory[]>([])
|
|
// 计算属性
|
const connectionStatusText = computed(() => {
|
switch (connectionStatus.value) {
|
case 'connected':
|
return '连接正常'
|
case 'error':
|
return '连接失败'
|
default:
|
return '未测试'
|
}
|
})
|
|
// 测试COS连接
|
const testConnection = async () => {
|
try {
|
connectionStatus.value = 'unknown'
|
// 这里可以通过上传一个小文件来测试连接
|
// 或者调用COS的getBucket等API来测试
|
ElMessage.info('正在测试连接...')
|
|
// 模拟测试(实际项目中应该调用真实的COS API)
|
setTimeout(() => {
|
connectionStatus.value = 'connected'
|
ElMessage.success('COS连接测试成功')
|
}, 1000)
|
|
} catch (error) {
|
connectionStatus.value = 'error'
|
ElMessage.error('COS连接测试失败')
|
console.error('连接测试失败:', error)
|
}
|
}
|
|
// 文件选择处理
|
const handleFileSelect = (file: any) => {
|
const newFile = file.raw as SelectedFile
|
newFile.folder = getDefaultFolder(newFile)
|
selectedFiles.value.push(newFile)
|
}
|
|
// 根据文件类型获取默认存储目录
|
const getDefaultFolder = (file: File): string => {
|
const type = file.type.toLowerCase()
|
if (type.startsWith('image/')) {
|
return 'images/'
|
} else if (type.startsWith('video/')) {
|
return 'videos/'
|
} else if (type.startsWith('audio/')) {
|
return 'audios/'
|
} else if (type.includes('pdf') || type.includes('document') || type.includes('text')) {
|
return 'documents/'
|
} else {
|
return 'others/'
|
}
|
}
|
|
// 文件类型判断
|
const isImage = (file: File) => file.type.startsWith('image/')
|
const isVideo = (file: File) => file.type.startsWith('video/')
|
const isAudio = (file: File) => file.type.startsWith('audio/')
|
const isDocument = (file: File) => {
|
const type = file.type.toLowerCase()
|
return type.includes('pdf') || type.includes('document') || type.includes('text')
|
}
|
|
// 移除文件
|
const removeFile = (index: number) => {
|
selectedFiles.value.splice(index, 1)
|
}
|
|
// 清空文件列表
|
const clearAllFiles = () => {
|
selectedFiles.value = []
|
}
|
|
// 上传所有文件
|
const uploadAllFiles = async () => {
|
if (selectedFiles.value.length === 0) {
|
ElMessage.warning('请先选择要上传的文件')
|
return
|
}
|
|
uploading.value = true
|
uploadProgress.value = []
|
|
// 初始化进度
|
selectedFiles.value.forEach(file => {
|
uploadProgress.value.push({
|
fileName: file.name,
|
percent: 0,
|
status: 'uploading'
|
})
|
})
|
|
// 并发上传所有文件
|
const uploadPromises = selectedFiles.value.map(async (file, index) => {
|
try {
|
const url = await uploadToCOS(file, file.folder || '')
|
|
// 更新进度
|
uploadProgress.value[index].percent = 100
|
uploadProgress.value[index].status = 'success'
|
uploadProgress.value[index].url = url
|
|
// 添加到历史记录
|
uploadHistory.value.unshift({
|
fileName: file.name,
|
folder: file.folder || '',
|
size: file.size,
|
url: url,
|
uploadTime: new Date(),
|
key: `${file.folder || ''}${file.name}`
|
})
|
|
return { success: true, file: file.name, url }
|
} catch (error) {
|
uploadProgress.value[index].status = 'error'
|
uploadProgress.value[index].error = error.message
|
console.error(`上传失败 ${file.name}:`, error)
|
return { success: false, file: file.name, error }
|
}
|
})
|
|
try {
|
const results = await Promise.all(uploadPromises)
|
const successCount = results.filter(r => r.success).length
|
const failCount = results.length - successCount
|
|
if (failCount === 0) {
|
ElMessage.success(`所有文件上传成功!共 ${successCount} 个文件`)
|
} else {
|
ElMessage.warning(`上传完成:成功 ${successCount} 个,失败 ${failCount} 个`)
|
}
|
|
// 清空选中的文件
|
selectedFiles.value = []
|
} catch (error) {
|
ElMessage.error('批量上传失败')
|
console.error('批量上传失败:', error)
|
} finally {
|
uploading.value = false
|
}
|
}
|
|
// 获取状态文本
|
const getStatusText = (status: string) => {
|
switch (status) {
|
case 'uploading':
|
return '上传中'
|
case 'success':
|
return '成功'
|
case 'error':
|
return '失败'
|
default:
|
return '未知'
|
}
|
}
|
|
// 复制URL
|
const copyUrl = async (url: string) => {
|
try {
|
await navigator.clipboard.writeText(url)
|
ElMessage.success('链接已复制到剪贴板')
|
} catch (error) {
|
ElMessage.error('复制失败')
|
}
|
}
|
|
// 打开文件
|
const openFile = (url: string) => {
|
window.open(url, '_blank')
|
}
|
|
// 删除文件
|
const deleteFile = async (item: UploadHistory) => {
|
try {
|
await ElMessageBox.confirm(`确定要删除文件"${item.fileName}"吗?`, '提示', {
|
confirmButtonText: '确定',
|
cancelButtonText: '取消',
|
type: 'warning'
|
})
|
|
await deleteFromCOS(item.key)
|
|
// 从历史记录中移除
|
const index = uploadHistory.value.findIndex(h => h.key === item.key)
|
if (index > -1) {
|
uploadHistory.value.splice(index, 1)
|
}
|
|
ElMessage.success('文件删除成功')
|
} catch (error: any) {
|
if (error !== 'cancel') {
|
console.error('删除失败:', error)
|
ElMessage.error('删除失败')
|
}
|
}
|
}
|
|
// 清空历史
|
const clearHistory = () => {
|
uploadHistory.value = []
|
uploadProgress.value = []
|
}
|
|
// 格式化文件大小
|
const formatFileSize = (bytes: number): string => {
|
if (bytes === 0) return '0 B'
|
const k = 1024
|
const sizes = ['B', 'KB', 'MB', 'GB']
|
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
|
}
|
|
// 格式化时间
|
const formatTime = (date: Date): string => {
|
return dayjs(date).format('YYYY-MM-DD HH:mm:ss')
|
}
|
|
onMounted(() => {
|
// 页面加载时可以测试连接
|
// testConnection()
|
})
|
</script>
|
|
<style lang="scss" scoped>
|
.cos-upload-test {
|
.card-title {
|
margin-bottom: 20px;
|
color: #303133;
|
font-size: 18px;
|
font-weight: 600;
|
}
|
|
.config-card,
|
.upload-card,
|
.progress-card,
|
.history-card {
|
margin-bottom: 20px;
|
}
|
|
.card-header {
|
display: flex;
|
justify-content: space-between;
|
align-items: center;
|
}
|
|
.config-info {
|
.config-item {
|
display: flex;
|
margin-bottom: 8px;
|
|
label {
|
width: 80px;
|
font-weight: 500;
|
color: #606266;
|
}
|
|
span {
|
color: #303133;
|
}
|
}
|
}
|
|
.upload-area {
|
margin-bottom: 20px;
|
|
.upload-dragger {
|
width: 100%;
|
}
|
}
|
|
.selected-files {
|
h4 {
|
margin-bottom: 16px;
|
color: #303133;
|
}
|
|
.file-list {
|
margin-bottom: 16px;
|
}
|
|
.file-item {
|
display: flex;
|
justify-content: space-between;
|
align-items: center;
|
padding: 12px;
|
border: 1px solid #e4e7ed;
|
border-radius: 4px;
|
margin-bottom: 8px;
|
|
.file-info {
|
display: flex;
|
align-items: center;
|
flex: 1;
|
|
.file-icon {
|
font-size: 24px;
|
margin-right: 12px;
|
color: #409eff;
|
}
|
|
.file-details {
|
.file-name {
|
font-weight: 500;
|
color: #303133;
|
margin-bottom: 4px;
|
}
|
|
.file-size {
|
font-size: 12px;
|
color: #909399;
|
}
|
}
|
}
|
|
.file-actions {
|
display: flex;
|
align-items: center;
|
}
|
}
|
|
.batch-actions {
|
display: flex;
|
gap: 12px;
|
}
|
}
|
|
.progress-list {
|
.progress-item {
|
margin-bottom: 16px;
|
padding: 12px;
|
border: 1px solid #e4e7ed;
|
border-radius: 4px;
|
|
.progress-info {
|
display: flex;
|
justify-content: space-between;
|
align-items: center;
|
margin-bottom: 8px;
|
|
.progress-name {
|
font-weight: 500;
|
color: #303133;
|
}
|
|
.progress-status {
|
font-size: 12px;
|
|
&.success {
|
color: #67c23a;
|
}
|
|
&.error {
|
color: #f56c6c;
|
}
|
|
&.uploading {
|
color: #409eff;
|
}
|
}
|
}
|
|
.progress-result {
|
margin-top: 8px;
|
display: flex;
|
gap: 8px;
|
}
|
}
|
}
|
}
|
</style>
|