448 lines
9.3 KiB
Plaintext
448 lines
9.3 KiB
Plaintext
|
|
<template>
|
|||
|
|
<view class="cl-upload-list" :class="[pt.className]">
|
|||
|
|
<view
|
|||
|
|
v-for="(item, index) in list"
|
|||
|
|
:key="item.uid"
|
|||
|
|
class="cl-upload"
|
|||
|
|
:class="[
|
|||
|
|
{
|
|||
|
|
'is-dark': isDark,
|
|||
|
|
'is-disabled': isDisabled
|
|||
|
|
},
|
|||
|
|
pt.item?.className
|
|||
|
|
]"
|
|||
|
|
:style="uploadStyle"
|
|||
|
|
@tap="choose(index)"
|
|||
|
|
>
|
|||
|
|
<image
|
|||
|
|
class="cl-upload__image"
|
|||
|
|
:class="[
|
|||
|
|
{
|
|||
|
|
'is-uploading': item.progress < 100
|
|||
|
|
},
|
|||
|
|
pt.image?.className
|
|||
|
|
]"
|
|||
|
|
:src="item.preview"
|
|||
|
|
mode="aspectFill"
|
|||
|
|
></image>
|
|||
|
|
|
|||
|
|
<cl-icon
|
|||
|
|
name="close-line"
|
|||
|
|
color="white"
|
|||
|
|
:pt="{
|
|||
|
|
className: 'cl-upload__close'
|
|||
|
|
}"
|
|||
|
|
@tap.stop="remove(item.uid)"
|
|||
|
|
v-if="!isDisabled"
|
|||
|
|
></cl-icon>
|
|||
|
|
|
|||
|
|
<view class="cl-upload__progress" v-if="item.progress < 100">
|
|||
|
|
<cl-progress :value="item.progress" :show-text="false"></cl-progress>
|
|||
|
|
</view>
|
|||
|
|
</view>
|
|||
|
|
|
|||
|
|
<view
|
|||
|
|
v-if="isAdd"
|
|||
|
|
class="cl-upload is-add"
|
|||
|
|
:class="[
|
|||
|
|
{
|
|||
|
|
'is-dark': isDark,
|
|||
|
|
'is-disabled': isDisabled
|
|||
|
|
},
|
|||
|
|
pt.add?.className
|
|||
|
|
]"
|
|||
|
|
:style="uploadStyle"
|
|||
|
|
@tap="choose(-1)"
|
|||
|
|
>
|
|||
|
|
<cl-icon
|
|||
|
|
:name="icon"
|
|||
|
|
:pt="{
|
|||
|
|
className: parseClass([
|
|||
|
|
[isDark, 'text-white', 'text-surface-400'],
|
|||
|
|
pt.icon?.className
|
|||
|
|
])
|
|||
|
|
}"
|
|||
|
|
:size="50"
|
|||
|
|
></cl-icon>
|
|||
|
|
|
|||
|
|
<cl-text
|
|||
|
|
:pt="{
|
|||
|
|
className: parseClass([
|
|||
|
|
[isDark, 'text-white', 'text-surface-500'],
|
|||
|
|
'text-xs mt-1 text-center',
|
|||
|
|
pt.text?.className
|
|||
|
|
])
|
|||
|
|
}"
|
|||
|
|
>{{ text }}</cl-text
|
|||
|
|
>
|
|||
|
|
</view>
|
|||
|
|
</view>
|
|||
|
|
</template>
|
|||
|
|
|
|||
|
|
<script lang="ts" setup>
|
|||
|
|
import { forInObject, isDark, parseClass, parsePt, parseRpx, uploadFile, uuid } from "@/cool";
|
|||
|
|
import { t } from "@/locale";
|
|||
|
|
import { computed, reactive, ref, watch, type PropType } from "vue";
|
|||
|
|
import type { ClUploadItem, PassThroughProps } from "../../types";
|
|||
|
|
import { useForm } from "../../hooks";
|
|||
|
|
|
|||
|
|
defineOptions({
|
|||
|
|
name: "cl-upload"
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
const props = defineProps({
|
|||
|
|
// 透传属性,用于自定义样式类名
|
|||
|
|
pt: {
|
|||
|
|
type: Object,
|
|||
|
|
default: () => ({})
|
|||
|
|
},
|
|||
|
|
// 双向绑定的值,支持字符串或字符串数组
|
|||
|
|
modelValue: {
|
|||
|
|
type: [Array, String] as PropType<string[] | string>,
|
|||
|
|
default: () => []
|
|||
|
|
},
|
|||
|
|
// 上传按钮的图标
|
|||
|
|
icon: {
|
|||
|
|
type: String,
|
|||
|
|
default: "camera-fill"
|
|||
|
|
},
|
|||
|
|
// 上传按钮显示的文本
|
|||
|
|
text: {
|
|||
|
|
type: String,
|
|||
|
|
default: () => t("上传 / 拍摄")
|
|||
|
|
},
|
|||
|
|
// 图片压缩方式:original原图,compressed压缩图
|
|||
|
|
sizeType: {
|
|||
|
|
type: [String, Array] as PropType<string[] | string>,
|
|||
|
|
default: () => ["original", "compressed"]
|
|||
|
|
},
|
|||
|
|
// 图片选择来源:album相册,camera拍照
|
|||
|
|
sourceType: {
|
|||
|
|
type: Array as PropType<string[]>,
|
|||
|
|
default: () => ["album", "camera"]
|
|||
|
|
},
|
|||
|
|
// 上传区域高度
|
|||
|
|
height: {
|
|||
|
|
type: [Number, String],
|
|||
|
|
default: 150
|
|||
|
|
},
|
|||
|
|
// 上传区域宽度
|
|||
|
|
width: {
|
|||
|
|
type: [Number, String],
|
|||
|
|
default: 150
|
|||
|
|
},
|
|||
|
|
// 是否支持多选
|
|||
|
|
multiple: {
|
|||
|
|
type: Boolean,
|
|||
|
|
default: false
|
|||
|
|
},
|
|||
|
|
// 最大上传数量限制
|
|||
|
|
limit: {
|
|||
|
|
type: Number,
|
|||
|
|
default: 9
|
|||
|
|
},
|
|||
|
|
// 是否禁用组件
|
|||
|
|
disabled: {
|
|||
|
|
type: Boolean,
|
|||
|
|
default: false
|
|||
|
|
},
|
|||
|
|
// 演示用,本地预览
|
|||
|
|
test: {
|
|||
|
|
type: Boolean,
|
|||
|
|
default: false
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
const emit = defineEmits([
|
|||
|
|
"update:modelValue", // 更新modelValue值
|
|||
|
|
"change", // 值发生变化时触发
|
|||
|
|
"exceed", // 超出数量限制时触发
|
|||
|
|
"success", // 上传成功时触发
|
|||
|
|
"error", // 上传失败时触发
|
|||
|
|
"progress" // 上传进度更新时触发
|
|||
|
|
]);
|
|||
|
|
|
|||
|
|
// cl-form 上下文
|
|||
|
|
const { disabled } = useForm();
|
|||
|
|
|
|||
|
|
// 是否禁用
|
|||
|
|
const isDisabled = computed(() => {
|
|||
|
|
return disabled.value || props.disabled;
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 透传属性的类型定义
|
|||
|
|
type PassThrough = {
|
|||
|
|
className?: string;
|
|||
|
|
item?: PassThroughProps;
|
|||
|
|
add?: PassThroughProps;
|
|||
|
|
image?: PassThroughProps;
|
|||
|
|
icon?: PassThroughProps;
|
|||
|
|
text?: PassThroughProps;
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 解析透传属性
|
|||
|
|
const pt = computed(() => parsePt<PassThrough>(props.pt));
|
|||
|
|
|
|||
|
|
// 上传文件列表
|
|||
|
|
const list = ref<ClUploadItem[]>([]);
|
|||
|
|
|
|||
|
|
// 当前操作的文件索引,-1表示新增,其他表示替换指定索引的文件
|
|||
|
|
const activeIndex = ref(0);
|
|||
|
|
|
|||
|
|
// 计算是否显示添加按钮
|
|||
|
|
const isAdd = computed(() => {
|
|||
|
|
const n = list.value.length;
|
|||
|
|
|
|||
|
|
if (isDisabled.value) {
|
|||
|
|
// 禁用状态下,只有没有文件时才显示添加按钮
|
|||
|
|
return n == 0;
|
|||
|
|
} else {
|
|||
|
|
// 根据multiple模式判断是否可以继续添加
|
|||
|
|
return n < (props.multiple ? props.limit : 1);
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 计算上传区域的样式
|
|||
|
|
const uploadStyle = computed(() => {
|
|||
|
|
return {
|
|||
|
|
height: parseRpx(props.height),
|
|||
|
|
width: parseRpx(props.width)
|
|||
|
|
};
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 获取已成功上传的文件URL列表
|
|||
|
|
*/
|
|||
|
|
function getUrls() {
|
|||
|
|
return list.value.filter((e) => e.url != "" && e.progress == 100).map((e) => e.url);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 获取当前的值,根据multiple模式返回不同格式
|
|||
|
|
*/
|
|||
|
|
function getValue() {
|
|||
|
|
const urls = getUrls();
|
|||
|
|
|
|||
|
|
if (props.multiple) {
|
|||
|
|
return urls;
|
|||
|
|
} else {
|
|||
|
|
return urls[0];
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 添加新的上传项或更新已有项
|
|||
|
|
* @param {string} url - 预览图片的本地路径
|
|||
|
|
*/
|
|||
|
|
function append(url: string) {
|
|||
|
|
// 创建新的上传项
|
|||
|
|
const item =
|
|||
|
|
activeIndex.value == -1
|
|||
|
|
? reactive<ClUploadItem>({
|
|||
|
|
uid: uuid(), // 生成唯一ID
|
|||
|
|
preview: url, // 预览图片路径
|
|||
|
|
url: "", // 最终上传后的URL
|
|||
|
|
progress: 0 // 上传进度
|
|||
|
|
})
|
|||
|
|
: list.value[activeIndex.value];
|
|||
|
|
|
|||
|
|
// 更新已有项或添加新项
|
|||
|
|
if (activeIndex.value == -1) {
|
|||
|
|
// 添加新项到列表末尾
|
|||
|
|
list.value.push(item);
|
|||
|
|
} else {
|
|||
|
|
// 替换已有项的内容
|
|||
|
|
item.progress = 0;
|
|||
|
|
item.preview = url;
|
|||
|
|
item.url = "";
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return item.uid;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 触发值变化事件
|
|||
|
|
*/
|
|||
|
|
function change() {
|
|||
|
|
const value = getValue();
|
|||
|
|
|
|||
|
|
emit("update:modelValue", value);
|
|||
|
|
emit("change", value);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 更新指定上传项的数据
|
|||
|
|
* @param {string} uid - 上传项的唯一ID
|
|||
|
|
* @param {any} data - 要更新的数据对象
|
|||
|
|
*/
|
|||
|
|
function update(uid: string, data: any) {
|
|||
|
|
const item = list.value.find((e) => e.uid == uid);
|
|||
|
|
|
|||
|
|
if (item != null) {
|
|||
|
|
// 遍历更新数据对象的所有属性
|
|||
|
|
forInObject(data, (value, key) => {
|
|||
|
|
item[key] = value;
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 当上传完成且有URL时,触发change事件
|
|||
|
|
if (item.progress == 100 && item.url != "") {
|
|||
|
|
change();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 删除指定的上传项
|
|||
|
|
* @param {string} uid - 要删除的上传项唯一ID
|
|||
|
|
*/
|
|||
|
|
function remove(uid: string) {
|
|||
|
|
list.value.splice(
|
|||
|
|
list.value.findIndex((e) => e.uid == uid),
|
|||
|
|
1
|
|||
|
|
);
|
|||
|
|
change();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 选择图片文件
|
|||
|
|
* @param {number} index - 操作的索引,-1表示新增,其他表示替换
|
|||
|
|
*/
|
|||
|
|
function choose(index: number) {
|
|||
|
|
if (isDisabled.value) {
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
activeIndex.value = index;
|
|||
|
|
|
|||
|
|
// 计算可选择的图片数量
|
|||
|
|
const count = activeIndex.value == -1 ? props.limit - list.value.length : 1;
|
|||
|
|
|
|||
|
|
if (count <= 0) {
|
|||
|
|
// 超出数量限制
|
|||
|
|
emit("exceed", list.value);
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 调用uni-app的选择图片API
|
|||
|
|
uni.chooseImage({
|
|||
|
|
count: count, // 最多可以选择的图片张数
|
|||
|
|
sizeType: props.sizeType as string[], // 压缩方式
|
|||
|
|
sourceType: props.sourceType as string[], // 图片来源
|
|||
|
|
success(res) {
|
|||
|
|
// 选择成功后处理每个文件
|
|||
|
|
if (Array.isArray(res.tempFiles)) {
|
|||
|
|
(res.tempFiles as ChooseImageTempFile[]).forEach((file) => {
|
|||
|
|
// 添加到列表并获取唯一ID
|
|||
|
|
const uid = append(file.path);
|
|||
|
|
|
|||
|
|
// 测试用,本地预览
|
|||
|
|
if (props.test) {
|
|||
|
|
update(uid, { url: file.path, progress: 100 });
|
|||
|
|
emit("success", file.path, uid);
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 开始上传文件
|
|||
|
|
uploadFile(file, {
|
|||
|
|
// 上传进度回调
|
|||
|
|
onProgressUpdate: ({ progress }) => {
|
|||
|
|
update(uid, { progress });
|
|||
|
|
emit("progress", progress);
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
.then((url) => {
|
|||
|
|
// 上传成功,更新URL和进度
|
|||
|
|
update(uid, { url, progress: 100 });
|
|||
|
|
emit("success", url, uid);
|
|||
|
|
})
|
|||
|
|
.catch((err) => {
|
|||
|
|
// 上传失败,触发错误事件并删除该项
|
|||
|
|
emit("error", err as string);
|
|||
|
|
remove(uid);
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
fail(err) {
|
|||
|
|
// 选择图片失败
|
|||
|
|
console.error(err);
|
|||
|
|
emit("error", err.errMsg);
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 监听modelValue的变化,同步更新内部列表
|
|||
|
|
watch(
|
|||
|
|
computed(() => props.modelValue!),
|
|||
|
|
(val: string | string[]) => {
|
|||
|
|
// 将当前列表的URL转为字符串用于比较
|
|||
|
|
const currentUrls = getUrls().join(",");
|
|||
|
|
|
|||
|
|
// 将传入的值标准化为字符串进行比较
|
|||
|
|
const newUrls = Array.isArray(val) ? val.join(",") : val;
|
|||
|
|
|
|||
|
|
// 如果值发生变化,更新内部列表
|
|||
|
|
if (currentUrls != newUrls) {
|
|||
|
|
// 标准化为数组格式
|
|||
|
|
const urls = Array.isArray(val) ? val : [val];
|
|||
|
|
|
|||
|
|
// 过滤空值并转换为Item对象
|
|||
|
|
list.value = urls
|
|||
|
|
.filter((url) => url != "")
|
|||
|
|
.map((url) => {
|
|||
|
|
return {
|
|||
|
|
uid: uuid(),
|
|||
|
|
preview: url,
|
|||
|
|
url,
|
|||
|
|
progress: 100 // 外部传入的URL认为已上传完成
|
|||
|
|
} as ClUploadItem;
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
immediate: true // 立即执行一次
|
|||
|
|
}
|
|||
|
|
);
|
|||
|
|
</script>
|
|||
|
|
|
|||
|
|
<style lang="scss" scoped>
|
|||
|
|
.cl-upload-list {
|
|||
|
|
@apply flex flex-row flex-wrap;
|
|||
|
|
|
|||
|
|
.cl-upload {
|
|||
|
|
@apply relative bg-surface-100 rounded-xl flex flex-col items-center justify-center;
|
|||
|
|
@apply mr-2 mb-2;
|
|||
|
|
|
|||
|
|
&.is-dark {
|
|||
|
|
@apply bg-surface-700;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
&.is-disabled {
|
|||
|
|
@apply opacity-50;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
&.is-add {
|
|||
|
|
@apply p-1;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
&__image {
|
|||
|
|
@apply w-full h-full absolute top-0 left-0;
|
|||
|
|
transition-property: opacity;
|
|||
|
|
transition-duration: 0.2s;
|
|||
|
|
|
|||
|
|
&.is-uploading {
|
|||
|
|
@apply opacity-70;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
&__close {
|
|||
|
|
@apply absolute top-1 right-1;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
&__progress {
|
|||
|
|
@apply absolute bottom-2 left-0 w-full z-10 px-2;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
</style>
|