upload-image.vue 4.66 KB
<template>
  <view class="upload-images-container">
    <!-- 已上传图片预览(uView Album组件) -->
    <up-album
        v-if="imageList.length > 0"
        :list="imageList"
        :max-count="maxCount"
        :show-delete="showDelete"
        @delete="handleDelete"
        @preview="handlePreview"
        :border-radius="8"
        :column="column"
        :gap="15"
    ></up-album>

    <!-- 上传按钮 -->
    <up-upload
        class="upload-btn"
        :disabled="imageList.length >= maxCount"
        :multiple="true"
        :max-count="maxCount - imageList.length"
        @after-read="handleAfterRead"
        :previewFullImage="true"
    >
      <view class="upload-btn-inner">
        <up-icon name="plus" color="#999" size="24"></up-icon>
        <text class="upload-tips" v-if="imageList.length < maxCount">上传图片</text>
        <text class="upload-tips" v-else>已达上限</text>
      </view>
    </up-upload>
  </view>
</template>

<script setup lang="ts">
import { ref, watch, defineProps, defineEmits, withDefaults } from 'vue';
import { uploadImages } from '@/common/utils/upload.js';


// 定义Props
const props = withDefaults(
    defineProps<{
      // 已上传图片列表(双向绑定)
      value: string[];
      // 最大上传数量
      maxCount?: number;
      // 是否显示删除按钮
      showDelete?: boolean;
      // 相册列数
      column?: number;
      // 上传文件key名
      fileKey?: string;
      // 自定义请求头(覆盖默认)
      header?: Record<string, string>;
      // 上传失败是否继续(多图时)
      ignoreError?: boolean;
    }>(),
    {
      maxCount: 9,
      showDelete: true,
      column: 3,
      fileKey: 'file',
      ignoreError: true
    }
);

// 定义Emits
const emit = defineEmits<{
  // 双向绑定更新图片列表
  (e: 'update:value', val: string[]): void;
  // 单张/多张上传成功
  (e: 'upload-success', urls: string[]): void;
  // 上传失败
  (e: 'upload-error', err: any): void;
  // 删除图片
  (e: 'delete-image', index: number): void;
  // 预览图片
  (e: 'preview-image', index: number): void;
}>();

// 内部维护的图片列表
const imageList = ref([...props.value]);

// 监听父组件传入的value变化
watch(
    () => props.value,
    (newVal) => {
      imageList.value = [...newVal];
    },
    { immediate: true }
);

// 监听内部列表变化,同步给父组件
watch(
    () => imageList.value,
    (newVal) => {
      emit('update:value', [...newVal]);
    },
    { deep: true }
);

/**
 * 选择图片后触发(上传核心逻辑)
 */
const handleAfterRead = async (event: any) => {
  // 获取选择的图片临时路径
  const filePaths = event.file.map((item: any) => item.url);

  try {
    uni.showLoading({ title: '上传中...' });
    // 调用公共上传方法
    const uploadUrls = await uploadImages({
      filePaths,
      fileKey: props.fileKey,
      header: props.header,
      ignoreError: props.ignoreError
    });

    if (uploadUrls.length > 0) {
      // 合并到已上传列表
      imageList.value = [...imageList.value, ...uploadUrls];
      // 通知父组件上传成功
      emit('upload-success', uploadUrls);
      uni.showToast({ title: `成功上传${uploadUrls.length}张图片`, icon: 'success' });
    }
  } catch (err) {
    emit('upload-error', err);
    console.error('图片上传失败:', err);
  } finally {
    uni.hideLoading();
  }
};

/**
 * 删除图片
 */
const handleDelete = (index: number) => {
  imageList.value.splice(index, 1);
  emit('delete-image', index);
  uni.showToast({ title: '删除成功', icon: 'success' });
};

/**
 * 预览图片
 */
const handlePreview = (index: number) => {
  emit('preview-image', index);
  // 原生预览(可选)
  uni.previewImage({
    urls: imageList.value,
    current: imageList.value[index]
  });
};
</script>

<style lang="scss" scoped>
.upload-images-container {
  padding: 10rpx 0;
}

// 上传按钮样式
.upload-btn {
  margin-top: 15rpx;
  width: 200rpx;
  height: 200rpx;
  border: 1px dashed #e5e5e5;
  border-radius: 8rpx;
  display: flex;
  align-items: center;
  justify-content: center;
  background-color: #f9f9f9;

  .upload-btn-inner {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
  }

  .upload-tips {
    font-size: 24rpx;
    color: #999;
    margin-top: 10rpx;
  }
}

// 适配Album组件样式
:deep(.u-album__item) {
  border-radius: 8rpx !important;
  overflow: hidden;
}

:deep(.u-album__delete) {
  background-color: rgba(0, 0, 0, 0.5) !important;
  border-radius: 50% !important;
  width: 40rpx !important;
  height: 40rpx !important;
  top: 0 !important;
  right: 0 !important;
}
</style>