Files
martial-mini/pages/attachment-view/attachment-view.vue
2025-12-26 10:20:46 +08:00

881 lines
21 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>
<view class="attachment-page">
<!-- 赛事信息卡片 -->
<view class="event-info-card">
<view class="event-title">{{ competitionName || '赛事名称' }}</view>
<view class="event-time-row">
<view class="time-item">
<text class="time-label">开始时间</text>
<text class="time-value">{{ startTime || '待定' }}</text>
</view>
<view class="time-divider"></view>
<view class="time-item">
<text class="time-label">结束时间</text>
<text class="time-value">{{ endTime || '待定' }}</text>
</view>
</view>
</view>
<!-- 加载状态 -->
<view class="loading-state" v-if="loading">
<view class="loading-spinner"></view>
<text class="loading-text">加载中...</text>
</view>
<!-- 附件列表 -->
<view class="attachments-section" v-if="!loading && attachments.length > 0">
<view class="section-header">
<text class="section-title">{{ pageTitle }}文件</text>
<text class="section-count">{{ attachments.length }}个文件</text>
</view>
<view class="attachments-list">
<view
class="attachment-item"
v-for="(file, index) in attachments"
:key="index"
>
<!-- 文件图标 -->
<view class="file-icon" :class="'icon-' + file.fileType">
<text class="icon-text">{{ getFileIconText(file.fileType) }}</text>
</view>
<!-- 文件信息 -->
<view class="file-content">
<text class="file-name">{{ file.fileName }}</text>
<view class="file-meta">
<text class="meta-item">{{ file.fileSize }}</text>
<text class="meta-dot" v-if="file.uploadTime">·</text>
<text class="meta-item" v-if="file.uploadTime">{{ file.uploadTime }}</text>
</view>
</view>
<!-- 操作按钮 -->
<view class="file-actions">
<view class="action-btn preview-btn" @click="previewFile(file)">
<text class="action-text">预览</text>
</view>
<view class="action-btn download-btn" @click="downloadFile(file)">
<text class="action-text">下载</text>
</view>
</view>
</view>
</view>
</view>
<!-- 空状态 -->
<view class="empty-state" v-if="!loading && attachments.length === 0">
<image class="empty-image" src="/static/images/empty.png" mode="aspectFit" />
<text class="empty-title">暂无{{ pageTitle }}文件</text>
<text class="empty-desc">相关文件正在整理中请稍后查看</text>
</view>
<!-- PDF预览弹窗 (仅H5) -->
<!-- #ifdef H5 -->
<view class="preview-modal" v-if="showPreview" @click="closePreview">
<view class="preview-container" @click.stop>
<view class="preview-header">
<text class="preview-title">{{ previewFileName }}</text>
<view class="preview-close" @click="closePreview">
<text class="close-icon">×</text>
</view>
</view>
<view class="preview-body">
<iframe
:src="previewUrl"
class="preview-iframe"
frameborder="0"
></iframe>
</view>
</view>
</view>
<!-- #endif -->
</view>
</template>
<script>
import competitionAPI from '@/api/competition.js'
// 页面类型配置
const PAGE_CONFIG = {
'info': { title: '信息发布', type: 'info' },
'rules': { title: '赛事规程', type: 'rules' },
'schedule': { title: '活动日程', type: 'schedule' },
'score': { title: '成绩公告', type: 'results' },
'results': { title: '成绩公告', type: 'results' },
'awards': { title: '奖牌榜', type: 'medals' },
'medals': { title: '奖牌榜', type: 'medals' },
'photos': { title: '图片直播', type: 'photos' }
}
export default {
data() {
return {
loading: true,
pageType: 'rules',
pageTitle: '赛事规程',
competitionId: '',
competitionName: '',
startTime: '',
endTime: '',
attachments: [],
// H5预览相关
showPreview: false,
previewUrl: '',
previewFileName: ''
}
},
onLoad(options) {
// 获取页面类型
if (options.type && PAGE_CONFIG[options.type]) {
this.pageType = options.type
this.pageTitle = PAGE_CONFIG[options.type].title
}
// 获取赛事ID
if (options.competitionId || options.eventId) {
this.competitionId = options.competitionId || options.eventId
}
// 获取赛事名称
if (options.name) {
this.competitionName = decodeURIComponent(options.name)
}
// 获取比赛时间
if (options.startTime) {
this.startTime = decodeURIComponent(options.startTime)
}
if (options.endTime) {
this.endTime = decodeURIComponent(options.endTime)
}
// 设置导航栏标题
uni.setNavigationBarTitle({
title: this.pageTitle
})
// 加载数据
this.loadData()
},
methods: {
/**
* 加载数据
*/
async loadData() {
this.loading = true
try {
// 如果没有赛事信息,先获取赛事详情
if (!this.startTime || !this.endTime) {
await this.loadCompetitionInfo()
}
// 加载附件列表
await this.loadAttachments()
} finally {
this.loading = false
}
},
/**
* 加载赛事信息
*/
async loadCompetitionInfo() {
try {
const res = await competitionAPI.getCompetitionDetail(this.competitionId)
if (res) {
this.competitionName = res.name || res.title || this.competitionName
this.startTime = this.formatDate(res.startTime || res.competitionStartTime)
this.endTime = this.formatDate(res.endTime || res.competitionEndTime)
}
} catch (err) {
console.error('加载赛事信息失败:', err)
// 使用模拟数据
this.startTime = '2025.12.12'
this.endTime = '2025.12.14'
}
},
/**
* 加载附件列表
*/
async loadAttachments() {
try {
// 使用 PAGE_CONFIG 映射的 type 值
const attachmentType = PAGE_CONFIG[this.pageType]?.type || this.pageType
console.log('=== 加载附件 ===')
console.log('competitionId:', this.competitionId)
console.log('pageType:', this.pageType)
console.log('attachmentType (发送给后端):', attachmentType)
const res = await competitionAPI.getAttachments({
competitionId: this.competitionId,
type: attachmentType
})
console.log('API返回结果:', res)
console.log('API返回结果类型:', typeof res)
console.log('是否为数组:', Array.isArray(res))
// 兼容不同的返回格式
let attachmentList = []
if (Array.isArray(res)) {
attachmentList = res
} else if (res && res.records && Array.isArray(res.records)) {
// 分页格式
attachmentList = res.records
} else if (res && typeof res === 'object') {
// 可能是单个对象,转为数组
attachmentList = [res]
}
console.log('处理后的附件列表:', attachmentList)
if (attachmentList.length > 0) {
this.attachments = attachmentList.map(file => ({
id: file.id,
fileName: file.fileName || file.name,
fileUrl: file.fileUrl || file.url,
fileSize: this.formatFileSize(file.fileSize || file.size),
fileType: this.getFileType(file.fileName || file.name),
uploadTime: this.formatDate(file.uploadTime || file.createTime)
}))
console.log('附件加载成功,共', this.attachments.length, '个文件')
} else {
console.log('没有附件数据,显示空状态')
this.attachments = []
}
} catch (err) {
console.error('=== 加载附件失败 ===')
console.error('错误详情:', err)
console.error('错误消息:', err.message)
console.error('错误代码:', err.code)
// 显示空状态,不使用模拟数据,方便调试
this.attachments = []
// 如果需要模拟数据,取消下面的注释
// this.loadMockData()
}
},
/**
* 加载模拟数据
*/
loadMockData() {
const mockDataMap = {
'info': [
{ id: '1', fileName: '2025年郑州武术大赛通知.pdf', fileUrl: '', fileSize: '1.2 MB', fileType: 'pdf', uploadTime: '2025-12-20' }
],
'rules': [
{ id: '1', fileName: '2025年郑州武术大赛竞赛规程.pdf', fileUrl: '', fileSize: '2.5 MB', fileType: 'pdf', uploadTime: '2025-12-18' },
{ id: '2', fileName: '参赛报名表.pdf', fileUrl: '', fileSize: '156 KB', fileType: 'pdf', uploadTime: '2025-12-18' }
],
'schedule': [
{ id: '1', fileName: '比赛日程安排表.pdf', fileUrl: '', fileSize: '890 KB', fileType: 'pdf', uploadTime: '2025-12-19' }
],
'score': [
{ id: '1', fileName: '比赛成绩公告.pdf', fileUrl: '', fileSize: '1.8 MB', fileType: 'pdf', uploadTime: '2025-12-25' }
],
'results': [
{ id: '1', fileName: '比赛成绩公告.pdf', fileUrl: '', fileSize: '1.8 MB', fileType: 'pdf', uploadTime: '2025-12-25' }
],
'awards': [
{ id: '1', fileName: '奖牌榜统计.pdf', fileUrl: '', fileSize: '520 KB', fileType: 'pdf', uploadTime: '2025-12-25' }
],
'medals': [
{ id: '1', fileName: '奖牌榜统计.pdf', fileUrl: '', fileSize: '520 KB', fileType: 'pdf', uploadTime: '2025-12-25' }
],
'photos': [
{ id: '1', fileName: '比赛精彩瞬间.pdf', fileUrl: '', fileSize: '15.6 MB', fileType: 'pdf', uploadTime: '2025-12-25' }
]
}
this.attachments = mockDataMap[this.pageType] || []
},
/**
* 预览文件
*/
previewFile(file) {
if (!file.fileUrl) {
uni.showToast({
title: '文件暂不可用',
icon: 'none'
})
return
}
// #ifdef H5
// H5端根据文件类型选择预览方式
if (file.fileType === 'pdf') {
// 方案1: 直接在新标签页打开PDF浏览器内置PDF阅读器
window.open(file.fileUrl, '_blank')
// 方案2: 使用微软 Office Online Viewer备选需要公网可访问
// const msViewerUrl = `https://view.officeapps.live.com/op/view.aspx?src=${encodeURIComponent(file.fileUrl)}`
// window.open(msViewerUrl, '_blank')
// 方案3: 使用内嵌弹窗(如果服务器支持)
// this.previewFileName = file.fileName
// this.previewUrl = file.fileUrl
// this.showPreview = true
} else if (['jpg', 'jpeg', 'png', 'gif', 'webp'].includes(file.fileType)) {
// 图片使用弹窗显示
this.previewFileName = file.fileName
this.previewUrl = file.fileUrl
this.showPreview = true
} else if (['doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx'].includes(file.fileType)) {
// Office 文档使用微软在线预览
const msViewerUrl = `https://view.officeapps.live.com/op/view.aspx?src=${encodeURIComponent(file.fileUrl)}`
window.open(msViewerUrl, '_blank')
} else {
// 其他文件类型,在新窗口打开
window.open(file.fileUrl, '_blank')
}
return
// #endif
// #ifndef H5
// 非H5端使用下载+打开文档的方式
uni.showLoading({
title: '加载中...'
})
uni.downloadFile({
url: file.fileUrl,
success: (res) => {
uni.hideLoading()
if (res.statusCode === 200) {
uni.openDocument({
filePath: res.tempFilePath,
fileType: file.fileType,
showMenu: true,
success: () => {
console.log('打开文档成功')
},
fail: (err) => {
console.error('打开文档失败:', err)
uni.showToast({
title: '无法预览此文件',
icon: 'none'
})
}
})
} else {
uni.showToast({
title: '文件加载失败',
icon: 'none'
})
}
},
fail: (err) => {
uni.hideLoading()
console.error('下载失败:', err)
uni.showToast({
title: '下载失败,请重试',
icon: 'none'
})
}
})
// #endif
},
/**
* 关闭预览弹窗
*/
closePreview() {
this.showPreview = false
this.previewUrl = ''
this.previewFileName = ''
},
/**
* 下载文件
*/
downloadFile(file) {
if (!file.fileUrl) {
uni.showToast({
title: '文件暂不可用',
icon: 'none'
})
return
}
uni.showLoading({
title: '下载中...'
})
uni.downloadFile({
url: file.fileUrl,
success: (res) => {
uni.hideLoading()
if (res.statusCode === 200) {
// #ifdef MP-WEIXIN
// 微信小程序保存文件
uni.saveFile({
tempFilePath: res.tempFilePath,
success: (saveRes) => {
uni.showToast({
title: '下载成功',
icon: 'success'
})
},
fail: () => {
// 保存失败则打开文档
uni.openDocument({
filePath: res.tempFilePath,
fileType: file.fileType,
showMenu: true
})
}
})
// #endif
// #ifdef H5
// H5端打开新窗口下载
window.open(file.fileUrl)
uni.showToast({
title: '开始下载',
icon: 'success'
})
// #endif
// #ifdef APP-PLUS
// APP端保存到相册或文件
uni.saveFile({
tempFilePath: res.tempFilePath,
success: (saveRes) => {
uni.showToast({
title: '下载成功',
icon: 'success'
})
},
fail: () => {
uni.openDocument({
filePath: res.tempFilePath,
fileType: file.fileType,
showMenu: true
})
}
})
// #endif
} else {
uni.showToast({
title: '下载失败',
icon: 'none'
})
}
},
fail: (err) => {
uni.hideLoading()
console.error('下载失败:', err)
uni.showToast({
title: '下载失败,请重试',
icon: 'none'
})
}
})
},
/**
* 获取文件类型
*/
getFileType(fileName) {
if (!fileName) return 'pdf'
const ext = fileName.split('.').pop().toLowerCase()
return ext
},
/**
* 获取文件图标文字
*/
getFileIconText(fileType) {
const iconMap = {
'pdf': 'PDF',
'doc': 'DOC',
'docx': 'DOC',
'xls': 'XLS',
'xlsx': 'XLS',
'ppt': 'PPT',
'pptx': 'PPT',
'txt': 'TXT',
'jpg': 'IMG',
'jpeg': 'IMG',
'png': 'IMG',
'zip': 'ZIP',
'rar': 'RAR'
}
return iconMap[fileType] || 'FILE'
},
/**
* 格式化文件大小
*/
formatFileSize(bytes) {
if (!bytes || bytes === 0) return '0 B'
if (typeof bytes === 'string') return bytes
const k = 1024
const sizes = ['B', 'KB', 'MB', 'GB']
const i = Math.floor(Math.log(bytes) / Math.log(k))
return (bytes / Math.pow(k, i)).toFixed(1) + ' ' + sizes[i]
},
/**
* 格式化日期
*/
formatDate(dateStr) {
if (!dateStr) return ''
const date = new Date(dateStr)
if (isNaN(date.getTime())) return dateStr
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
return `${year}.${month}.${day}`
}
}
}
</script>
<style lang="scss" scoped>
.attachment-page {
min-height: 100vh;
background: #f5f5f5;
}
// 赛事信息卡片
.event-info-card {
background: #fff;
margin: 20rpx;
border-radius: 16rpx;
padding: 30rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.06);
}
.event-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
margin-bottom: 24rpx;
line-height: 1.4;
}
.event-time-row {
display: flex;
align-items: center;
background: #f8f8f8;
border-radius: 12rpx;
padding: 20rpx;
}
.time-item {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
gap: 8rpx;
}
.time-label {
font-size: 24rpx;
color: #999;
}
.time-value {
font-size: 28rpx;
color: #333;
font-weight: 500;
}
.time-divider {
width: 1rpx;
height: 60rpx;
background: #e0e0e0;
margin: 0 20rpx;
}
// 加载状态
.loading-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 120rpx 0;
}
.loading-spinner {
width: 60rpx;
height: 60rpx;
border: 4rpx solid #f0f0f0;
border-top-color: #C93639;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.loading-text {
margin-top: 20rpx;
font-size: 28rpx;
color: #999;
}
// 附件区域
.attachments-section {
margin: 20rpx;
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20rpx;
padding: 0 10rpx;
}
.section-title {
font-size: 30rpx;
font-weight: bold;
color: #333;
}
.section-count {
font-size: 24rpx;
color: #999;
}
.attachments-list {
display: flex;
flex-direction: column;
gap: 16rpx;
}
// 附件项
.attachment-item {
background: #fff;
border-radius: 16rpx;
padding: 24rpx;
display: flex;
align-items: center;
gap: 20rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
}
// 文件图标
.file-icon {
width: 80rpx;
height: 80rpx;
border-radius: 12rpx;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
background: #C93639;
}
.file-icon.icon-pdf {
background: #E74C3C;
}
.file-icon.icon-doc,
.file-icon.icon-docx {
background: #3498DB;
}
.file-icon.icon-xls,
.file-icon.icon-xlsx {
background: #27AE60;
}
.file-icon.icon-ppt,
.file-icon.icon-pptx {
background: #E67E22;
}
.file-icon.icon-jpg,
.file-icon.icon-jpeg,
.file-icon.icon-png {
background: #9B59B6;
}
.icon-text {
font-size: 22rpx;
font-weight: bold;
color: #fff;
}
// 文件内容
.file-content {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 8rpx;
}
.file-name {
font-size: 28rpx;
color: #333;
font-weight: 500;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.file-meta {
display: flex;
align-items: center;
gap: 8rpx;
}
.meta-item {
font-size: 24rpx;
color: #999;
}
.meta-dot {
font-size: 24rpx;
color: #ccc;
}
// 操作按钮
.file-actions {
flex-shrink: 0;
display: flex;
gap: 12rpx;
}
.action-btn {
padding: 12rpx 24rpx;
border-radius: 30rpx;
transition: all 0.3s;
&:active {
opacity: 0.8;
transform: scale(0.95);
}
}
.preview-btn {
background: #C93639;
.action-text {
color: #fff;
}
}
.download-btn {
background: #fff;
border: 1rpx solid #C93639;
.action-text {
color: #C93639;
}
}
.action-text {
font-size: 24rpx;
font-weight: 500;
}
// 空状态
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 120rpx 60rpx;
}
.empty-image {
width: 200rpx;
height: 200rpx;
margin-bottom: 30rpx;
opacity: 0.6;
}
.empty-title {
font-size: 32rpx;
font-weight: 600;
color: #333;
margin-bottom: 16rpx;
}
.empty-desc {
font-size: 26rpx;
color: #999;
text-align: center;
}
// PDF预览弹窗样式
.preview-modal {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.7);
z-index: 9999;
display: flex;
align-items: center;
justify-content: center;
}
.preview-container {
width: 95%;
height: 90%;
background: #fff;
border-radius: 16rpx;
display: flex;
flex-direction: column;
overflow: hidden;
}
.preview-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 24rpx 30rpx;
background: #C93639;
color: #fff;
}
.preview-title {
font-size: 32rpx;
font-weight: 500;
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
padding-right: 20rpx;
}
.preview-close {
width: 60rpx;
height: 60rpx;
display: flex;
align-items: center;
justify-content: center;
background: rgba(255, 255, 255, 0.2);
border-radius: 50%;
cursor: pointer;
}
.close-icon {
font-size: 40rpx;
color: #fff;
line-height: 1;
}
.preview-body {
flex: 1;
overflow: hidden;
}
.preview-iframe {
width: 100%;
height: 100%;
border: none;
}
</style>