| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443 |
- <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"
- :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;
- }
- &__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>
|