|
@@ -64,14 +64,14 @@
|
|
|
</template>
|
|
</template>
|
|
|
|
|
|
|
|
<script setup>
|
|
<script setup>
|
|
|
-import { computed, ref, watch } from 'vue'
|
|
|
|
|
|
|
+import { ref, watch, nextTick } from 'vue'
|
|
|
import config from '@/config'
|
|
import config from '@/config'
|
|
|
import { userToken } from '@/composables/config'
|
|
import { userToken } from '@/composables/config'
|
|
|
|
|
|
|
|
-// === Props 完全对齐官方 uni-file-picker ===
|
|
|
|
|
|
|
+// === Vue3 v-model 标准写法 + 多类型兼容 ===
|
|
|
const props = defineProps({
|
|
const props = defineProps({
|
|
|
- value: {
|
|
|
|
|
- type: Array,
|
|
|
|
|
|
|
+ modelValue: {
|
|
|
|
|
+ type: [Array, String, Object],
|
|
|
default: () => []
|
|
default: () => []
|
|
|
},
|
|
},
|
|
|
readonly: {
|
|
readonly: {
|
|
@@ -102,7 +102,6 @@ const props = defineProps({
|
|
|
type: Boolean,
|
|
type: Boolean,
|
|
|
default: true
|
|
default: true
|
|
|
},
|
|
},
|
|
|
- // 上传配置
|
|
|
|
|
action: {
|
|
action: {
|
|
|
type: String,
|
|
type: String,
|
|
|
default: ''
|
|
default: ''
|
|
@@ -141,8 +140,7 @@ const props = defineProps({
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
const emit = defineEmits([
|
|
const emit = defineEmits([
|
|
|
- 'input',
|
|
|
|
|
- 'update:value',
|
|
|
|
|
|
|
+ 'update:modelValue',
|
|
|
'change',
|
|
'change',
|
|
|
'delete',
|
|
'delete',
|
|
|
'success',
|
|
'success',
|
|
@@ -154,33 +152,129 @@ const emit = defineEmits([
|
|
|
// 内部数据
|
|
// 内部数据
|
|
|
const innerFileList = ref([])
|
|
const innerFileList = ref([])
|
|
|
const tempFileQueue = ref([])
|
|
const tempFileQueue = ref([])
|
|
|
|
|
+const originalType = ref('array') // 'string' | 'object' | 'array'
|
|
|
|
|
|
|
|
-// 完全对齐官方 upload-image 样式
|
|
|
|
|
const boxStyle = 'width:33.3%;height:0;padding-top:33.3%;'
|
|
const boxStyle = 'width:33.3%;height:0;padding-top:33.3%;'
|
|
|
const borderStyle = 'border:1px #eee solid;border-radius:5px;'
|
|
const borderStyle = 'border:1px #eee solid;border-radius:5px;'
|
|
|
|
|
|
|
|
-// 同步外部 value
|
|
|
|
|
|
|
+// ==============================================
|
|
|
|
|
+// 统一格式化函数(修复递归核心)
|
|
|
|
|
+// ==============================================
|
|
|
|
|
+function formatValue(val) {
|
|
|
|
|
+ if (!val) return []
|
|
|
|
|
+
|
|
|
|
|
+ // 字符串
|
|
|
|
|
+ if (typeof val === 'string') {
|
|
|
|
|
+ return [{
|
|
|
|
|
+ path: val,
|
|
|
|
|
+ url: val.startsWith('http') ? val : config.Host80 + val,
|
|
|
|
|
+ name: val.split('/').pop(),
|
|
|
|
|
+ status: 'success'
|
|
|
|
|
+ }]
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 对象
|
|
|
|
|
+ if (typeof val === 'object' && !Array.isArray(val)) {
|
|
|
|
|
+ const path = val.path || val.url || ''
|
|
|
|
|
+ return [{
|
|
|
|
|
+ ...val,
|
|
|
|
|
+ path,
|
|
|
|
|
+ url: val.url || (path.startsWith('http') ? path : config.Host80 + path),
|
|
|
|
|
+ name: val.name || path.split('/').pop(),
|
|
|
|
|
+ status: 'success'
|
|
|
|
|
+ }]
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 数组
|
|
|
|
|
+ if (Array.isArray(val)) {
|
|
|
|
|
+ return val.map(item => {
|
|
|
|
|
+ if (typeof item === 'string') {
|
|
|
|
|
+ return {
|
|
|
|
|
+ path: item,
|
|
|
|
|
+ url: item.startsWith('http') ? item : config.Host80 + item,
|
|
|
|
|
+ name: item.split('/').pop(),
|
|
|
|
|
+ status: 'success'
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ const path = item?.path || item?.url || ''
|
|
|
|
|
+ return {
|
|
|
|
|
+ ...item,
|
|
|
|
|
+ path,
|
|
|
|
|
+ url: item?.url || (path.startsWith('http') ? path : config.Host80 + path),
|
|
|
|
|
+ name: item?.name || path.split('/').pop(),
|
|
|
|
|
+ status: 'success'
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+ return []
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// ==============================================
|
|
|
|
|
+// 监听外部传入:防重复赋值 → 无递归
|
|
|
|
|
+// ==============================================
|
|
|
watch(
|
|
watch(
|
|
|
- () => props.value,
|
|
|
|
|
|
|
+ () => props.modelValue,
|
|
|
(val) => {
|
|
(val) => {
|
|
|
- innerFileList.value = val ? [...val] : []
|
|
|
|
|
|
|
+ const formatted = formatValue(val)
|
|
|
|
|
+ // 值相同不更新,防止死循环
|
|
|
|
|
+ if (JSON.stringify(innerFileList.value) === JSON.stringify(formatted)) return
|
|
|
|
|
+
|
|
|
|
|
+ if (!val) {
|
|
|
|
|
+ innerFileList.value = []
|
|
|
|
|
+ originalType.value = 'array'
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 记录原始类型
|
|
|
|
|
+ if (typeof val === 'string') originalType.value = 'string'
|
|
|
|
|
+ else if (typeof val === 'object' && !Array.isArray(val)) originalType.value = 'object'
|
|
|
|
|
+ else originalType.value = 'array'
|
|
|
|
|
+
|
|
|
|
|
+ innerFileList.value = formatted
|
|
|
},
|
|
},
|
|
|
{ immediate: true, deep: true }
|
|
{ immediate: true, deep: true }
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
-// 内部变化同步外部
|
|
|
|
|
|
|
+// ==============================================
|
|
|
|
|
+// 内部变化同步外部:nextTick → 无递归
|
|
|
|
|
+// ==============================================
|
|
|
watch(
|
|
watch(
|
|
|
innerFileList,
|
|
innerFileList,
|
|
|
- (val) => {
|
|
|
|
|
- emit('input', val)
|
|
|
|
|
- emit('update:value', val)
|
|
|
|
|
- emit('change', val)
|
|
|
|
|
- console.log(val, 1212);
|
|
|
|
|
|
|
+ (list) => {
|
|
|
|
|
+ nextTick(() => {
|
|
|
|
|
+ let returnValue = []
|
|
|
|
|
+
|
|
|
|
|
+ if (!list || list.length === 0) {
|
|
|
|
|
+ returnValue = originalType.value === 'string' ? '' :
|
|
|
|
|
+ originalType.value === 'object' ? {} : []
|
|
|
|
|
+ emitUpdate(returnValue)
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 只返回干净的 path 数据
|
|
|
|
|
+ const cleanList = list.map(item => item.path || item.url || '')
|
|
|
|
|
+ console.log(originalType);
|
|
|
|
|
|
|
|
|
|
+ // 按原始类型返回
|
|
|
|
|
+ if (props.limit === 1) {
|
|
|
|
|
+ returnValue = cleanList[0] || ''
|
|
|
|
|
+ } else if (originalType.value === 'string') returnValue = cleanList[0] || ''
|
|
|
|
|
+ else if (originalType.value === 'object') returnValue = { path: cleanList[0] || '' }
|
|
|
|
|
+ else returnValue = cleanList
|
|
|
|
|
+
|
|
|
|
|
+ emitUpdate(returnValue)
|
|
|
|
|
+ })
|
|
|
},
|
|
},
|
|
|
{ deep: true }
|
|
{ deep: true }
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
|
|
+// 统一触发更新
|
|
|
|
|
+function emitUpdate(val) {
|
|
|
|
|
+ emit('update:modelValue', val)
|
|
|
|
|
+ emit('change', val)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
// ==============================================
|
|
// ==============================================
|
|
|
// 文件类型判断
|
|
// 文件类型判断
|
|
|
// ==============================================
|
|
// ==============================================
|
|
@@ -199,19 +293,16 @@ const getFileExt = (name) => {
|
|
|
return name.split('.').pop()?.toUpperCase()
|
|
return name.split('.').pop()?.toUpperCase()
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-// 文件图标
|
|
|
|
|
const getFileIcon = (name) => {
|
|
const getFileIcon = (name) => {
|
|
|
const ext = getFileExt(name)
|
|
const ext = getFileExt(name)
|
|
|
if (ext === 'PDF') return 'PDF'
|
|
if (ext === 'PDF') return 'PDF'
|
|
|
- if (ext === 'PDF') return 'PDF'
|
|
|
|
|
if (['DOC', 'DOCX'].includes(ext)) return 'WORD'
|
|
if (['DOC', 'DOCX'].includes(ext)) return 'WORD'
|
|
|
if (['XLS', 'XLSX'].includes(ext)) return 'EXCEL'
|
|
if (['XLS', 'XLSX'].includes(ext)) return 'EXCEL'
|
|
|
if (isVideo({ name })) return '▶'
|
|
if (isVideo({ name })) return '▶'
|
|
|
- if (isImage({ name })) return 'IMAGE'
|
|
|
|
|
- // return 'FILE'
|
|
|
|
|
|
|
+ if (isImage({ name })) return 'IMG'
|
|
|
|
|
+ return 'FILE'
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-// 文件样式类
|
|
|
|
|
const getFileClass = (name) => {
|
|
const getFileClass = (name) => {
|
|
|
const ext = getFileExt(name)
|
|
const ext = getFileExt(name)
|
|
|
if (ext === 'PDF') return 'file-pdf'
|
|
if (ext === 'PDF') return 'file-pdf'
|
|
@@ -222,47 +313,28 @@ const getFileClass = (name) => {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// ==============================================
|
|
// ==============================================
|
|
|
-// 预览:全部跳转到新页面使用 web-view 打开
|
|
|
|
|
|
|
+// 预览
|
|
|
// ==============================================
|
|
// ==============================================
|
|
|
const previewFile = (file, index) => {
|
|
const previewFile = (file, index) => {
|
|
|
- if (
|
|
|
|
|
- props.disablePreview ||
|
|
|
|
|
- file.status === 'uploading' ||
|
|
|
|
|
- file.status === 'error'
|
|
|
|
|
- ) {
|
|
|
|
|
- return
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
|
|
+ if (props.disablePreview || file.status === 'uploading' || file.status === 'error') return
|
|
|
const url = file.url || file.path
|
|
const url = file.url || file.path
|
|
|
const name = file.name || '文件'
|
|
const name = file.name || '文件'
|
|
|
|
|
|
|
|
- // 图片预览(保留原生预览,体验更好)
|
|
|
|
|
if (isImage(file)) {
|
|
if (isImage(file)) {
|
|
|
- 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 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)
|
|
const urls = successImages.map(item => item.url || item.path)
|
|
|
-
|
|
|
|
|
- uni.previewImage({
|
|
|
|
|
- current: realIndex,
|
|
|
|
|
- urls: urls,
|
|
|
|
|
- loop: true
|
|
|
|
|
- })
|
|
|
|
|
|
|
+ uni.previewImage({ current: realIndex, urls, loop: true })
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // ==============================================
|
|
|
|
|
- // 视频 / PDF / Word / Excel → 跳转到 web-view 页面
|
|
|
|
|
- // ==============================================
|
|
|
|
|
uni.navigateTo({
|
|
uni.navigateTo({
|
|
|
url: `/pages/common/webview?url=${encodeURIComponent(url)}&title=${encodeURIComponent(name)}`
|
|
url: `/pages/common/webview?url=${encodeURIComponent(url)}&title=${encodeURIComponent(name)}`
|
|
|
})
|
|
})
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
// ==============================================
|
|
// ==============================================
|
|
|
-// 上传 / 删除 / 重试 逻辑(已修复进度)
|
|
|
|
|
|
|
+// 上传 / 删除 / 重试
|
|
|
// ==============================================
|
|
// ==============================================
|
|
|
const deleteFile = (index) => {
|
|
const deleteFile = (index) => {
|
|
|
const item = innerFileList.value[index]
|
|
const item = innerFileList.value[index]
|
|
@@ -278,8 +350,7 @@ const handleChoose = () => {
|
|
|
count,
|
|
count,
|
|
|
success: (res) => {
|
|
success: (res) => {
|
|
|
const files = res.tempFiles || res.tempFilePaths.map((p, i) => ({
|
|
const files = res.tempFiles || res.tempFilePaths.map((p, i) => ({
|
|
|
- path: p,
|
|
|
|
|
- name: `file_${i}`
|
|
|
|
|
|
|
+ path: p, name: `file_${i}`
|
|
|
}))
|
|
}))
|
|
|
emit('select', { tempFiles: files })
|
|
emit('select', { tempFiles: files })
|
|
|
tempFileQueue.value = files
|
|
tempFileQueue.value = files
|
|
@@ -291,19 +362,12 @@ const handleChoose = () => {
|
|
|
const startUpload = async () => {
|
|
const startUpload = async () => {
|
|
|
const files = tempFileQueue.value
|
|
const files = tempFileQueue.value
|
|
|
tempFileQueue.value = []
|
|
tempFileQueue.value = []
|
|
|
- for (const file of files) {
|
|
|
|
|
- await uploadFile(file)
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ for (const file of files) await uploadFile(file)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
const uploadFile = (fileItem) => {
|
|
const uploadFile = (fileItem) => {
|
|
|
return new Promise((resolve) => {
|
|
return new Promise((resolve) => {
|
|
|
- innerFileList.value.push({
|
|
|
|
|
- ...fileItem,
|
|
|
|
|
- status: 'uploading',
|
|
|
|
|
- progress: 0
|
|
|
|
|
- })
|
|
|
|
|
-
|
|
|
|
|
|
|
+ innerFileList.value.push({ ...fileItem, status: 'uploading', progress: 0 })
|
|
|
const index = innerFileList.value.length - 1
|
|
const index = innerFileList.value.length - 1
|
|
|
const url = props.action || config.Host80 + props.uploadUrl
|
|
const url = props.action || config.Host80 + props.uploadUrl
|
|
|
|
|
|
|
@@ -311,10 +375,7 @@ const uploadFile = (fileItem) => {
|
|
|
url,
|
|
url,
|
|
|
filePath: fileItem.path,
|
|
filePath: fileItem.path,
|
|
|
name: props.uploadName,
|
|
name: props.uploadName,
|
|
|
- header: {
|
|
|
|
|
- 'Access-Token': userToken.value,
|
|
|
|
|
- ...props.uploadHeaders
|
|
|
|
|
- },
|
|
|
|
|
|
|
+ header: { 'Access-Token': userToken.value, ...props.uploadHeaders },
|
|
|
formData: props.uploadData,
|
|
formData: props.uploadData,
|
|
|
|
|
|
|
|
success: (res) => {
|
|
success: (res) => {
|