|
|
@@ -1,24 +1,63 @@
|
|
|
<template>
|
|
|
<view class="file-picker-wrapper">
|
|
|
- <!-- 只读模式:仅展示文件列表 -->
|
|
|
+ <!-- 只读模式:仅展示 -->
|
|
|
<view v-if="readonly" class="file-list readonly-list">
|
|
|
- <view v-for="(file, index) in value" :key="index" class="file-item readonly-item" @click="previewFile(file)">
|
|
|
+ <view v-for="(file, index) in innerFileList" :key="index" class="file-item readonly-item"
|
|
|
+ @click="previewFile(file, index)">
|
|
|
<image v-if="isImage(file)" :src="file.url || file.path" mode="aspectFill" class="file-thumb" />
|
|
|
- <view v-else class="file-icon">
|
|
|
- <text class="iconfont icon-file">📄</text>
|
|
|
+ <view v-else class="file-icon" :class="getFileClass(file.name)">
|
|
|
+ <text class="file-icon-text">{{ getFileIcon(file.name) }}</text>
|
|
|
</view>
|
|
|
<text class="file-name">{{ file.name }}</text>
|
|
|
</view>
|
|
|
- <view v-if="!value.length" class="empty-text">暂无文件</view>
|
|
|
+ <view v-if="!innerFileList.length" class="empty-text">暂无文件</view>
|
|
|
</view>
|
|
|
|
|
|
- <!-- 非只读模式:完整功能 -->
|
|
|
- <view v-else>
|
|
|
- <view class="upload-area">
|
|
|
- <uni-file-picker ref="filePicker" v-bind="$attrs" :limit="999" :show-file-list="false" :auto-upload="false"
|
|
|
- mode="grid" @select="handleSelect" @fail="handleFail" @error="handleError">
|
|
|
+ <!-- 正常模式:宫格上传(完全对齐官方 upload-image 样式) -->
|
|
|
+ <view v-else class="uni-file-picker__container">
|
|
|
+ <view class="file-picker__box" v-for="(item, index) in innerFileList" :key="index" :style="boxStyle">
|
|
|
+ <view class="file-picker__box-content" :style="borderStyle">
|
|
|
+ <!-- 图片 -->
|
|
|
+ <image v-if="isImage(item)" class="file-image" :src="item.url || item.path" mode="aspectFill"
|
|
|
+ @click.stop="previewFile(item, index)" />
|
|
|
+
|
|
|
+ <!-- 视频 → 显示第一帧 + 播放图标 -->
|
|
|
+ <view v-else-if="isVideo(item)" class="file-cover video-box" @click.stop="previewFile(item, index)">
|
|
|
+ <image :src="item.url || item.path" class="file-image" mode="aspectFill" />
|
|
|
+ <view class="video-play-icon">▶</view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 其他文件(PDF/Word/Excel) -->
|
|
|
+ <view v-else class="file-cover file-box" :class="getFileClass(item.name)"
|
|
|
+ @click.stop="previewFile(item, index)">
|
|
|
+ <text class="file-big-icon">{{ getFileIcon(item.name) }}</text>
|
|
|
+ <text class="file-ext-name">{{ getFileExt(item.name) }}</text>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 删除 -->
|
|
|
+ <view v-if="delIcon" class="icon-del-box" @click.stop="deleteFile(index)">
|
|
|
+ <view class="icon-del"></view>
|
|
|
+ <view class="icon-del rotate"></view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 进度 -->
|
|
|
+ <view v-if="item.status === 'uploading'" class="file-picker__progress">
|
|
|
+ <progress class="file-picker__progress-item" :percent="item.progress" stroke-width="4"
|
|
|
+ backgroundColor="#EBEBEB" />
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 失败重试 -->
|
|
|
+ <view v-if="item.status === 'error'" class="file-picker__mask" @click.stop="reUploadFile(index)">
|
|
|
+ 点击重试
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 添加按钮 -->
|
|
|
+ <view v-if="innerFileList.length < limit" class="file-picker__box" :style="boxStyle">
|
|
|
+ <view class="file-picker__box-content is-add" :style="borderStyle" @click="handleChoose">
|
|
|
<cwg-icon name="icon_add" class="upload-icon" :size="24" />
|
|
|
- </uni-file-picker>
|
|
|
+ </view>
|
|
|
</view>
|
|
|
</view>
|
|
|
</view>
|
|
|
@@ -29,54 +68,61 @@ import { computed, ref, watch } from 'vue'
|
|
|
import config from '@/config'
|
|
|
import { userToken } from '@/composables/config'
|
|
|
|
|
|
-// 定义 props
|
|
|
+// === Props 完全对齐官方 uni-file-picker ===
|
|
|
const props = defineProps({
|
|
|
- // 支持 v-model 的文件列表
|
|
|
value: {
|
|
|
type: Array,
|
|
|
default: () => []
|
|
|
},
|
|
|
- // 只读模式
|
|
|
readonly: {
|
|
|
type: Boolean,
|
|
|
default: false
|
|
|
},
|
|
|
- // 最大文件数量
|
|
|
+ disabled: {
|
|
|
+ type: Boolean,
|
|
|
+ default: false
|
|
|
+ },
|
|
|
limit: {
|
|
|
type: Number,
|
|
|
- default: 5
|
|
|
+ default: 9
|
|
|
},
|
|
|
- // 文件类型限制(image/video/all)
|
|
|
fileMediatype: {
|
|
|
type: String,
|
|
|
default: 'all'
|
|
|
},
|
|
|
- // 上传地址(必填)
|
|
|
+ delIcon: {
|
|
|
+ type: Boolean,
|
|
|
+ default: true
|
|
|
+ },
|
|
|
+ disablePreview: {
|
|
|
+ type: Boolean,
|
|
|
+ default: false
|
|
|
+ },
|
|
|
+ autoUpload: {
|
|
|
+ type: Boolean,
|
|
|
+ default: true
|
|
|
+ },
|
|
|
+ // 上传配置
|
|
|
action: {
|
|
|
type: String,
|
|
|
default: ''
|
|
|
},
|
|
|
- // 上传URL(优先使用)
|
|
|
uploadUrl: {
|
|
|
type: String,
|
|
|
default: '/custom/bank/upload'
|
|
|
},
|
|
|
- // 上传请求头
|
|
|
uploadHeaders: {
|
|
|
type: Object,
|
|
|
default: () => ({})
|
|
|
},
|
|
|
- // 上传字段名
|
|
|
uploadName: {
|
|
|
type: String,
|
|
|
default: 'file'
|
|
|
},
|
|
|
- // 上传额外数据
|
|
|
uploadData: {
|
|
|
type: Object,
|
|
|
default: () => ({})
|
|
|
},
|
|
|
- // 响应数据处理函数
|
|
|
responseHandler: {
|
|
|
type: Function,
|
|
|
default: (res) => {
|
|
|
@@ -85,22 +131,15 @@ const props = defineProps({
|
|
|
return {
|
|
|
success: data.code === 200,
|
|
|
path: data.data?.path || data.data,
|
|
|
- message: data.msg || '上传成功',
|
|
|
- data: data
|
|
|
+ message: data.msg || '上传成功'
|
|
|
}
|
|
|
} catch (e) {
|
|
|
- return {
|
|
|
- success: false,
|
|
|
- path: null,
|
|
|
- message: '解析响应失败',
|
|
|
- data: null
|
|
|
- }
|
|
|
+ return { success: false, message: '解析失败' }
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
})
|
|
|
|
|
|
-// 定义 emits
|
|
|
const emit = defineEmits([
|
|
|
'input',
|
|
|
'update:value',
|
|
|
@@ -108,118 +147,168 @@ const emit = defineEmits([
|
|
|
'delete',
|
|
|
'success',
|
|
|
'fail',
|
|
|
- 'error',
|
|
|
- 'progress'
|
|
|
+ 'progress',
|
|
|
+ 'select'
|
|
|
])
|
|
|
|
|
|
-// 响应式数据
|
|
|
-const innerFileList = ref([]) // 内部文件列表
|
|
|
-const uploadProgress = ref({}) // 存储每个文件的上传进度 { fileIndex: progress }
|
|
|
-const isUploading = ref(false) // 是否正在上传
|
|
|
+// 内部数据
|
|
|
+const innerFileList = ref([])
|
|
|
+const tempFileQueue = ref([])
|
|
|
|
|
|
-// 计算属性
|
|
|
-const remainingLimit = computed(() => {
|
|
|
- return Math.max(0, props.limit - innerFileList.value.length)
|
|
|
-})
|
|
|
+// 完全对齐官方 upload-image 样式
|
|
|
+const boxStyle = 'width:33.3%;height:0;padding-top:33.3%;'
|
|
|
+const borderStyle = 'border:1px #eee solid;border-radius:5px;'
|
|
|
|
|
|
-// 监听外部 value 变化
|
|
|
+// 同步外部 value
|
|
|
watch(
|
|
|
() => props.value,
|
|
|
- (newVal) => {
|
|
|
- innerFileList.value = [...newVal]
|
|
|
+ (val) => {
|
|
|
+ innerFileList.value = val ? [...val] : []
|
|
|
},
|
|
|
{ immediate: true, deep: true }
|
|
|
)
|
|
|
|
|
|
-// 监听内部文件列表变化,同步到外部
|
|
|
+// 内部变化同步外部
|
|
|
watch(
|
|
|
innerFileList,
|
|
|
- (newVal) => {
|
|
|
- emit('input', newVal)
|
|
|
- emit('update:value', newVal)
|
|
|
- emit('change', newVal)
|
|
|
+ (val) => {
|
|
|
+ emit('input', val)
|
|
|
+ emit('update:value', val)
|
|
|
+ emit('change', val)
|
|
|
+ console.log(val, 1212);
|
|
|
+
|
|
|
},
|
|
|
{ deep: true }
|
|
|
)
|
|
|
|
|
|
-// 方法
|
|
|
-// 判断是否为图片
|
|
|
+// ==============================================
|
|
|
+// 文件类型判断
|
|
|
+// ==============================================
|
|
|
const isImage = (file) => {
|
|
|
- const type = file.type || file.fileType
|
|
|
- if (type) return type.startsWith('image/')
|
|
|
- const ext = (file.name || '').split('.').pop().toLowerCase()
|
|
|
- return ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp'].includes(ext)
|
|
|
+ const ext = (file.path || file.name || '').split('.').pop()?.toLowerCase()
|
|
|
+ return ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp', 'svg'].includes(ext)
|
|
|
+}
|
|
|
+
|
|
|
+const isVideo = (file) => {
|
|
|
+ const ext = (file.path || file.name || '').split('.').pop()?.toLowerCase()
|
|
|
+ return ['mp4', 'mov', 'avi', 'flv', 'wmv', 'rmvb'].includes(ext)
|
|
|
+}
|
|
|
+
|
|
|
+const getFileExt = (name) => {
|
|
|
+ if (!name) return ''
|
|
|
+ return name.split('.').pop()?.toUpperCase()
|
|
|
+}
|
|
|
+
|
|
|
+// 文件图标
|
|
|
+const getFileIcon = (name) => {
|
|
|
+ const ext = getFileExt(name)
|
|
|
+ if (ext === 'PDF') return 'PDF'
|
|
|
+ if (ext === 'PDF') return 'PDF'
|
|
|
+ if (['DOC', 'DOCX'].includes(ext)) return 'WORD'
|
|
|
+ if (['XLS', 'XLSX'].includes(ext)) return 'EXCEL'
|
|
|
+ if (isVideo({ name })) return '▶'
|
|
|
+ if (isImage({ name })) return 'IMAGE'
|
|
|
+ // return 'FILE'
|
|
|
}
|
|
|
|
|
|
-// 预览文件
|
|
|
-const previewFile = (file) => {
|
|
|
+// 文件样式类
|
|
|
+const getFileClass = (name) => {
|
|
|
+ const ext = getFileExt(name)
|
|
|
+ if (ext === 'PDF') return 'file-pdf'
|
|
|
+ if (['DOC', 'DOCX'].includes(ext)) return 'file-word'
|
|
|
+ if (['XLS', 'XLSX'].includes(ext)) return 'file-excel'
|
|
|
+ if (isVideo({ name })) return 'file-video'
|
|
|
+ return 'file-other'
|
|
|
+}
|
|
|
+
|
|
|
+// ==============================================
|
|
|
+// 预览:全部跳转到新页面使用 web-view 打开
|
|
|
+// ==============================================
|
|
|
+const previewFile = (file, index) => {
|
|
|
+ if (
|
|
|
+ props.disablePreview ||
|
|
|
+ file.status === 'uploading' ||
|
|
|
+ file.status === 'error'
|
|
|
+ ) {
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ const url = file.url || file.path
|
|
|
+ const name = file.name || '文件'
|
|
|
+
|
|
|
+ // 图片预览(保留原生预览,体验更好)
|
|
|
if (isImage(file)) {
|
|
|
- const urls = innerFileList.value
|
|
|
- .filter(f => isImage(f))
|
|
|
- .map(f => f.url || f.path)
|
|
|
- const current = file.url || file.path
|
|
|
- uni.previewImage({ urls, current })
|
|
|
- } else {
|
|
|
- if (file.url) {
|
|
|
- // #ifdef H5
|
|
|
- window.open(file.url)
|
|
|
- // #endif
|
|
|
- // #ifdef APP-PLUS
|
|
|
- plus.runtime.openURL(file.url)
|
|
|
- // #endif
|
|
|
- // #ifdef MP-WEIXIN
|
|
|
- uni.downloadFile({
|
|
|
- url: file.url,
|
|
|
- success: (res) => uni.openDocument({ filePath: res.tempFilePath })
|
|
|
- })
|
|
|
- // #endif
|
|
|
- } else {
|
|
|
- uni.showToast({ title: '暂无法预览此文件', icon: 'none' })
|
|
|
- }
|
|
|
+ const successImages = innerFileList.value.filter(item =>
|
|
|
+ isImage(item) && item.status === 'success'
|
|
|
+ )
|
|
|
+ const realIndex = successImages.findIndex(
|
|
|
+ item => item.url === file.url && item.path === file.path
|
|
|
+ )
|
|
|
+ const urls = successImages.map(item => item.url || item.path)
|
|
|
+
|
|
|
+ uni.previewImage({
|
|
|
+ current: realIndex,
|
|
|
+ urls: urls,
|
|
|
+ loop: true
|
|
|
+ })
|
|
|
+ return
|
|
|
}
|
|
|
-}
|
|
|
|
|
|
-// 删除文件
|
|
|
+ // ==============================================
|
|
|
+ // 视频 / PDF / Word / Excel → 跳转到 web-view 页面
|
|
|
+ // ==============================================
|
|
|
+ uni.navigateTo({
|
|
|
+ url: `/pages/common/webview?url=${encodeURIComponent(url)}&title=${encodeURIComponent(name)}`
|
|
|
+ })
|
|
|
+}
|
|
|
+// ==============================================
|
|
|
+// 上传 / 删除 / 重试 逻辑(已修复进度)
|
|
|
+// ==============================================
|
|
|
const deleteFile = (index) => {
|
|
|
- const deleted = innerFileList.value[index]
|
|
|
+ const item = innerFileList.value[index]
|
|
|
innerFileList.value.splice(index, 1)
|
|
|
- emit('delete', deleted, index)
|
|
|
-}
|
|
|
-
|
|
|
-// 处理文件选择
|
|
|
-const handleSelect = async (res) => {
|
|
|
- const { tempFiles } = res
|
|
|
- // 正常新增模式:检查数量限制
|
|
|
- const availableSlots = props.limit - innerFileList.value.length
|
|
|
- const toAddFiles = tempFiles.slice(0, availableSlots)
|
|
|
- if (toAddFiles.length < tempFiles.length) {
|
|
|
- uni.showToast({
|
|
|
- title: `最多选择${props.limit}个文件,超出部分已忽略`,
|
|
|
- icon: 'none'
|
|
|
- })
|
|
|
- }
|
|
|
+ emit('delete', item, index)
|
|
|
+}
|
|
|
|
|
|
- isUploading.value = true
|
|
|
- try {
|
|
|
- for (let i = 0; i < toAddFiles.length; i++) {
|
|
|
- const file = toAddFiles[i]
|
|
|
- const fileIndex = innerFileList.value.length + i
|
|
|
- await uploadFile(file, fileIndex)
|
|
|
+const handleChoose = () => {
|
|
|
+ if (props.disabled || props.readonly) return
|
|
|
+ const count = props.limit - innerFileList.value.length
|
|
|
+ uni.chooseFile({
|
|
|
+ type: props.fileMediatype,
|
|
|
+ count,
|
|
|
+ success: (res) => {
|
|
|
+ const files = res.tempFiles || res.tempFilePaths.map((p, i) => ({
|
|
|
+ path: p,
|
|
|
+ name: `file_${i}`
|
|
|
+ }))
|
|
|
+ emit('select', { tempFiles: files })
|
|
|
+ tempFileQueue.value = files
|
|
|
+ if (props.autoUpload) startUpload()
|
|
|
}
|
|
|
- } finally {
|
|
|
- isUploading.value = false
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+const startUpload = async () => {
|
|
|
+ const files = tempFileQueue.value
|
|
|
+ tempFileQueue.value = []
|
|
|
+ for (const file of files) {
|
|
|
+ await uploadFile(file)
|
|
|
}
|
|
|
}
|
|
|
-const filePicker = ref(null)
|
|
|
-// 上传文件
|
|
|
-const uploadFile = (fileItem, fileIndex) => {
|
|
|
- return new Promise((resolve, reject) => {
|
|
|
- // 构建上传URL
|
|
|
- const uploadUrl = props.action || (config.Host80 + props.uploadUrl)
|
|
|
|
|
|
- // 构建上传参数
|
|
|
+const uploadFile = (fileItem) => {
|
|
|
+ return new Promise((resolve) => {
|
|
|
+ innerFileList.value.push({
|
|
|
+ ...fileItem,
|
|
|
+ status: 'uploading',
|
|
|
+ progress: 0
|
|
|
+ })
|
|
|
+
|
|
|
+ const index = innerFileList.value.length - 1
|
|
|
+ const url = props.action || config.Host80 + props.uploadUrl
|
|
|
+
|
|
|
const task = uni.uploadFile({
|
|
|
- url: uploadUrl,
|
|
|
+ url,
|
|
|
filePath: fileItem.path,
|
|
|
name: props.uploadName,
|
|
|
header: {
|
|
|
@@ -227,201 +316,247 @@ const uploadFile = (fileItem, fileIndex) => {
|
|
|
...props.uploadHeaders
|
|
|
},
|
|
|
formData: props.uploadData,
|
|
|
- success: (res) => {
|
|
|
|
|
|
+ success: (res) => {
|
|
|
const result = props.responseHandler(res.data)
|
|
|
if (result.success) {
|
|
|
- // 构造文件对象
|
|
|
- const uploadedFile = {
|
|
|
- url: config.Host80 + result.path,
|
|
|
- path: result.path,
|
|
|
- name: fileItem.name,
|
|
|
- size: fileItem.size,
|
|
|
- type: fileItem.type,
|
|
|
- response: result.data
|
|
|
- }
|
|
|
- innerFileList.value.push(uploadedFile)
|
|
|
- emit('success', uploadedFile, res)
|
|
|
- resolve(uploadedFile)
|
|
|
+ innerFileList.value[index].progress = 100
|
|
|
+ innerFileList.value[index].status = 'success'
|
|
|
+ innerFileList.value[index].url = config.Host80 + result.path
|
|
|
+ innerFileList.value[index].path = result.path
|
|
|
+ emit('success', innerFileList.value[index])
|
|
|
} else {
|
|
|
- uni.showToast({ title: result.message || '上传失败', icon: 'error' })
|
|
|
- emit('fail', new Error(result.message), fileItem)
|
|
|
- reject(new Error(result.message))
|
|
|
+ innerFileList.value[index].status = 'error'
|
|
|
+ uni.showToast({ title: result.message, icon: 'error' })
|
|
|
+ emit('fail', result.message)
|
|
|
}
|
|
|
+ resolve(result)
|
|
|
},
|
|
|
- fail: (err) => {
|
|
|
- // 清除进度
|
|
|
- delete uploadProgress.value[fileIndex]
|
|
|
-
|
|
|
- console.error('上传失败:', err)
|
|
|
- uni.showToast({ title: '网络错误,请重试', icon: 'error' })
|
|
|
- emit('fail', err, fileItem)
|
|
|
- reject(err)
|
|
|
+
|
|
|
+ fail: () => {
|
|
|
+ innerFileList.value[index].status = 'error'
|
|
|
+ uni.showToast({ title: '上传失败', icon: 'error' })
|
|
|
+ emit('fail', '网络异常')
|
|
|
+ resolve(null)
|
|
|
}
|
|
|
})
|
|
|
|
|
|
- // 监听上传进度
|
|
|
- task.onProgressUpdate((progress) => {
|
|
|
- uploadProgress.value[fileIndex] = progress.progress
|
|
|
- emit('progress', progress, fileItem, fileIndex)
|
|
|
+ task.onProgressUpdate((p) => {
|
|
|
+ innerFileList.value[index].progress = p.progress
|
|
|
+ emit('progress', p, index)
|
|
|
})
|
|
|
})
|
|
|
}
|
|
|
|
|
|
+const reUploadFile = (index) => {
|
|
|
+ const file = innerFileList.value[index]
|
|
|
+ uploadFile(file)
|
|
|
+}
|
|
|
+</script>
|
|
|
|
|
|
-
|
|
|
-// 处理失败事件(来自 uni-file-picker)
|
|
|
-const handleFail = (err) => {
|
|
|
- emit('fail', err)
|
|
|
+<style scoped>
|
|
|
+/* 布局 */
|
|
|
+.uni-file-picker__container {
|
|
|
+ display: flex;
|
|
|
+ flex-wrap: wrap;
|
|
|
+ margin: -5px;
|
|
|
+ width: 100%;
|
|
|
}
|
|
|
|
|
|
-// 处理错误事件(来自 uni-file-picker)
|
|
|
-const handleError = (err) => {
|
|
|
- emit('error', err)
|
|
|
+.file-picker__box {
|
|
|
+ position: relative;
|
|
|
+ width: 33.3%;
|
|
|
+ height: 0;
|
|
|
+ padding-top: 33.3%;
|
|
|
+ box-sizing: border-box;
|
|
|
}
|
|
|
|
|
|
-// 格式化文件大小
|
|
|
-const formatFileSize = (bytes) => {
|
|
|
- if (!bytes) return ''
|
|
|
- const sizes = ['B', 'KB', 'MB', 'GB']
|
|
|
- const i = Math.floor(Math.log(bytes) / Math.log(1024))
|
|
|
- return (bytes / Math.pow(1024, i)).toFixed(2) + sizes[i]
|
|
|
+.file-picker__box-content {
|
|
|
+ position: absolute;
|
|
|
+ top: 0;
|
|
|
+ right: 0;
|
|
|
+ bottom: 0;
|
|
|
+ left: 0;
|
|
|
+ margin: 5px;
|
|
|
+ border-radius: 5px;
|
|
|
+ overflow: hidden;
|
|
|
+ background: #f7f7f7;
|
|
|
}
|
|
|
|
|
|
-// 暴露方法(可选,与原组件保持一致,未暴露任何方法)
|
|
|
-// defineExpose({})
|
|
|
-</script>
|
|
|
+/* 图片 */
|
|
|
+.file-image {
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+}
|
|
|
|
|
|
-<style scoped>
|
|
|
-.file-picker-wrapper {
|
|
|
+/* 文件封面 */
|
|
|
+.file-cover {
|
|
|
width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ flex-direction: column;
|
|
|
+ color: #fff;
|
|
|
}
|
|
|
|
|
|
-.file-list {
|
|
|
- margin-bottom: 20rpx;
|
|
|
+/* 视频 */
|
|
|
+.video-box {
|
|
|
+ position: relative;
|
|
|
}
|
|
|
|
|
|
-.file-item {
|
|
|
+.video-play-icon {
|
|
|
+ position: absolute;
|
|
|
+ font-size: 30px;
|
|
|
+ color: #fff;
|
|
|
+ background: rgba(0, 0, 0, 0.5);
|
|
|
+ width: 50px;
|
|
|
+ height: 50px;
|
|
|
+ border-radius: 50%;
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
- justify-content: space-between;
|
|
|
- background: #f8f8f8;
|
|
|
- border-radius: 12rpx;
|
|
|
- padding: 16rpx 20rpx;
|
|
|
- margin-bottom: 16rpx;
|
|
|
+ justify-content: center;
|
|
|
+}
|
|
|
+
|
|
|
+/* 文件类型颜色 */
|
|
|
+.file-pdf {
|
|
|
+ background: #ff4f4f;
|
|
|
+}
|
|
|
+
|
|
|
+.file-word {
|
|
|
+ background: #3a7ff5;
|
|
|
+}
|
|
|
+
|
|
|
+.file-excel {
|
|
|
+ background: #24a148;
|
|
|
+}
|
|
|
+
|
|
|
+.file-video {
|
|
|
+ background: #000;
|
|
|
+}
|
|
|
+
|
|
|
+.file-other {
|
|
|
+ background: #f7f7f7;
|
|
|
}
|
|
|
|
|
|
-.file-info {
|
|
|
+.file-big-icon {
|
|
|
+ font-size: 16px;
|
|
|
+ font-weight: bold;
|
|
|
+ margin-bottom: 4px;
|
|
|
+}
|
|
|
+
|
|
|
+.file-ext-name {
|
|
|
+ font-size: 12px;
|
|
|
+ opacity: 0.9;
|
|
|
+}
|
|
|
+
|
|
|
+/* 添加按钮 */
|
|
|
+.is-add {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
- flex: 1;
|
|
|
- gap: 20rpx;
|
|
|
+ justify-content: center;
|
|
|
+ background: #f7f7f7;
|
|
|
}
|
|
|
|
|
|
-.file-thumb {
|
|
|
- width: 80rpx;
|
|
|
- height: 80rpx;
|
|
|
- border-radius: 8rpx;
|
|
|
- background: #e0e0e0;
|
|
|
+.upload-icon {
|
|
|
+ color: #999;
|
|
|
}
|
|
|
|
|
|
-.file-icon {
|
|
|
- width: 80rpx;
|
|
|
- height: 80rpx;
|
|
|
- background: #e0e0e0;
|
|
|
- border-radius: 8rpx;
|
|
|
+/* 删除按钮 */
|
|
|
+.icon-del-box {
|
|
|
+ position: absolute;
|
|
|
+ top: 3px;
|
|
|
+ right: 3px;
|
|
|
+ width: 26px;
|
|
|
+ height: 26px;
|
|
|
+ border-radius: 50%;
|
|
|
+ background: rgba(0, 0, 0, 0.5);
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
justify-content: center;
|
|
|
+ z-index: 10;
|
|
|
+ transform: rotate(-45deg);
|
|
|
}
|
|
|
|
|
|
-.file-detail {
|
|
|
- flex: 1;
|
|
|
- overflow: hidden;
|
|
|
+.icon-del {
|
|
|
+ width: 15px;
|
|
|
+ height: 2px;
|
|
|
+ background: #fff;
|
|
|
+ border-radius: 2px;
|
|
|
}
|
|
|
|
|
|
-.file-name {
|
|
|
- font-size: 28rpx;
|
|
|
- color: #333;
|
|
|
- display: block;
|
|
|
- white-space: nowrap;
|
|
|
- overflow: hidden;
|
|
|
- text-overflow: ellipsis;
|
|
|
+.rotate {
|
|
|
+ position: absolute;
|
|
|
+ transform: rotate(90deg);
|
|
|
}
|
|
|
|
|
|
-.file-size {
|
|
|
- font-size: 24rpx;
|
|
|
- color: #999;
|
|
|
- margin-top: 8rpx;
|
|
|
- display: block;
|
|
|
+/* 进度 */
|
|
|
+.file-picker__progress {
|
|
|
+ position: absolute;
|
|
|
+ bottom: 0;
|
|
|
+ left: 0;
|
|
|
+ right: 0;
|
|
|
+ z-index: 2;
|
|
|
}
|
|
|
|
|
|
-.file-actions {
|
|
|
+/* 失败遮罩 */
|
|
|
+.file-picker__mask {
|
|
|
+ position: absolute;
|
|
|
+ top: 0;
|
|
|
+ left: 0;
|
|
|
+ right: 0;
|
|
|
+ bottom: 0;
|
|
|
+ background: rgba(0, 0, 0, 0.4);
|
|
|
+ color: #fff;
|
|
|
display: flex;
|
|
|
- gap: 16rpx;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ font-size: 12px;
|
|
|
}
|
|
|
|
|
|
-.action-btn {
|
|
|
- padding: 0 20rpx;
|
|
|
- height: 56rpx;
|
|
|
- line-height: 56rpx;
|
|
|
- font-size: 24rpx;
|
|
|
+/* 只读模式 */
|
|
|
+.file-list {
|
|
|
+ padding: 5px;
|
|
|
}
|
|
|
|
|
|
-.readonly-item {
|
|
|
- cursor: pointer;
|
|
|
- transition: opacity 0.2s;
|
|
|
+.file-item {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 10px;
|
|
|
+ padding: 10px;
|
|
|
+ background: #f8f8f8;
|
|
|
+ border-radius: 8px;
|
|
|
+ margin-bottom: 10px;
|
|
|
}
|
|
|
|
|
|
-.readonly-item:active {
|
|
|
- opacity: 0.7;
|
|
|
+.file-thumb,
|
|
|
+.file-icon {
|
|
|
+ width: 60px;
|
|
|
+ height: 60px;
|
|
|
+ border-radius: 6px;
|
|
|
}
|
|
|
|
|
|
-.empty-text {
|
|
|
- text-align: center;
|
|
|
- color: #999;
|
|
|
- font-size: 28rpx;
|
|
|
- padding: 40rpx 0;
|
|
|
+.file-icon {
|
|
|
+ background: #eee;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
}
|
|
|
|
|
|
-.upload-area {
|
|
|
- margin-top: 20rpx;
|
|
|
+.file-icon-text {
|
|
|
+ font-size: 12px;
|
|
|
+ font-weight: bold;
|
|
|
}
|
|
|
|
|
|
-.upload-btn {
|
|
|
- width: 100%;
|
|
|
- background: #007aff;
|
|
|
- color: #fff;
|
|
|
- border-radius: 8rpx;
|
|
|
+.file-name {
|
|
|
+ flex: 1;
|
|
|
+ font-size: 14px;
|
|
|
+ color: #333;
|
|
|
}
|
|
|
|
|
|
-/* 上传进度条样式 */
|
|
|
-.upload-progress {
|
|
|
- position: relative;
|
|
|
- width: 100%;
|
|
|
- height: 8rpx;
|
|
|
- background-color: #f0f0f0;
|
|
|
- border-radius: 4rpx;
|
|
|
- margin-top: 8rpx;
|
|
|
- overflow: hidden;
|
|
|
-
|
|
|
- .progress-bar {
|
|
|
- position: absolute;
|
|
|
- top: 0;
|
|
|
- left: 0;
|
|
|
- height: 100%;
|
|
|
- background-color: #007aff;
|
|
|
- border-radius: 4rpx;
|
|
|
- transition: width 0.3s ease;
|
|
|
- }
|
|
|
-
|
|
|
- .progress-text {
|
|
|
- position: absolute;
|
|
|
- top: 0;
|
|
|
- right: 0;
|
|
|
- font-size: 20rpx;
|
|
|
- color: #666;
|
|
|
- line-height: 8rpx;
|
|
|
- }
|
|
|
+.empty-text {
|
|
|
+ text-align: center;
|
|
|
+ color: #999;
|
|
|
+ padding: 20px 0;
|
|
|
}
|
|
|
</style>
|