123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488 |
- <template>
- <div class="sa-upload-image">
- <div class="upload-container sa-flex sa-flex-wrap">
- <!-- 已上传的图片列表 -->
- <sa-draggable
- v-if="imageList.length > 0"
- v-model="imageList"
- :animation="300"
- handle=".sortable-drag"
- item-key="url"
- @end="handleSort"
- class="sa-flex sa-flex-wrap"
- >
- <template #item="{ element, index }">
- <div
- class="image-item"
- :class="{ 'compact-mode': compact }"
- :style="{ width: size + 'px', height: size + 'px' }"
- >
- <el-image
- :src="element"
- fit="cover"
- :preview-src-list="compact ? [] : imageList"
- :initial-index="compact ? 0 : index"
- :preview-teleported="true"
- @click="compact ? previewImage(element, index) : null"
- :style="{ cursor: compact ? 'pointer' : 'default' }"
- >
- <template #error>
- <div class="image-error">
- <el-icon><Picture /></el-icon>
- </div>
- </template>
- </el-image>
- <div class="image-mask" :class="{ 'compact-mask': compact }">
- <template v-if="compact">
- <el-icon @click.stop="removeImage(index)" title="删除" size="12"
- ><Delete
- /></el-icon>
- </template>
- <template v-else>
- <el-icon class="sortable-drag" title="拖拽排序" size="24"><Rank /></el-icon>
- <el-icon @click="previewImage(element, index)" title="预览" size="24"
- ><ZoomIn
- /></el-icon>
- <el-icon @click="removeImage(index)" title="删除" size="24"><Delete /></el-icon>
- </template>
- </div>
- </div>
- </template>
- </sa-draggable>
- <!-- 上传按钮 -->
- <div
- v-if="!maxCount || imageList.length < maxCount"
- class="upload-trigger"
- :class="{ 'compact-trigger': compact }"
- :style="{ width: size + 'px', height: size + 'px' }"
- @click="handleUpload"
- >
- <el-icon class="upload-icon" :size="compact ? 16 : 24"><Plus /></el-icon>
- <div v-if="!compact" class="upload-text">{{ placeholder || '上传图片' }}</div>
- </div>
- <!-- 隐藏的文件输入框 -->
- <input
- ref="fileInputRef"
- type="file"
- :accept="acceptString"
- :multiple="multiple"
- style="display: none"
- @change="handleFileSelect"
- />
- </div>
- <!-- 提示信息 -->
- <div v-if="showTip" class="upload-tip">
- <span v-if="maxCount">最多上传{{ maxCount }}张,</span>
- <span v-if="accept && accept.length">支持{{ accept.join('、') }}格式,</span>
- <span v-if="maxSize">单张图片不超过{{ maxSize }}MB</span>
- </div>
- <!-- 图片预览弹窗 -->
- <el-image-viewer
- v-if="previewVisible"
- :url-list="previewImageList"
- :initial-index="previewInitialIndex"
- :infinite="false"
- :hide-on-click-modal="true"
- :teleported="true"
- :z-index="3000"
- @close="closePreview"
- />
- </div>
- </template>
- <script>
- import { ref, computed, watch } from 'vue';
- import { ElMessage, ElImageViewer } from 'element-plus';
- import { Picture, Plus, Rank, ZoomIn, Delete } from '@element-plus/icons-vue';
- import SaDraggable from 'vuedraggable';
- import adminApi from '@/app/admin/api';
- export default {
- name: 'SaUploadImage',
- components: {
- Picture,
- Plus,
- Rank,
- ZoomIn,
- Delete,
- SaDraggable,
- ElImageViewer,
- },
- };
- </script>
- <script setup>
- const props = defineProps({
- modelValue: {
- type: Array,
- default: () => [],
- },
- // 是否直传,默认为true
- directUpload: {
- type: Boolean,
- default: true,
- },
- // 最大上传数量
- maxCount: {
- type: Number,
- default: 0,
- },
- // 支持的文件格式
- accept: {
- type: Array,
- default: () => ['jpg', 'jpeg', 'png'],
- },
- // 最大文件大小(MB)
- maxSize: {
- type: Number,
- default: 2,
- },
- // 是否支持多选
- multiple: {
- type: Boolean,
- default: true,
- },
- // 图片尺寸
- size: {
- type: Number,
- default: 100,
- },
- // 占位符文本
- placeholder: {
- type: String,
- default: '',
- },
- // 是否显示提示信息
- showTip: {
- type: Boolean,
- default: true,
- },
- // 精简模式
- compact: {
- type: Boolean,
- default: false,
- },
- });
- const emit = defineEmits(['update:modelValue', 'change']);
- // 文件输入框引用
- const fileInputRef = ref();
- // 图片列表
- const imageList = ref([...props.modelValue]);
- // 计算接受的文件类型字符串
- const acceptString = computed(() => {
- return props.accept.map((type) => `.${type}`).join(',');
- });
- // 监听外部数据变化
- watch(
- () => props.modelValue,
- (newValue) => {
- imageList.value = [...newValue];
- },
- { deep: true },
- );
- // 监听内部数据变化
- watch(
- imageList,
- (newValue) => {
- emit('update:modelValue', newValue);
- emit('change', newValue);
- },
- { deep: true },
- );
- // 处理上传点击
- const handleUpload = () => {
- if (props.directUpload) {
- // 直传模式,直接打开文件选择
- fileInputRef.value?.click();
- } else {
- // 非直传模式,打开文件管理弹窗
- openFileManager();
- }
- };
- // 处理文件选择
- const handleFileSelect = async (event) => {
- const files = Array.from(event.target.files);
- if (!files.length) return;
- // 检查数量限制
- if (props.maxCount && imageList.value.length + files.length > props.maxCount) {
- ElMessage.warning(`最多只能上传${props.maxCount}张图片`);
- return;
- }
- // 验证文件
- const validFiles = [];
- for (const file of files) {
- if (validateFile(file)) {
- validFiles.push(file);
- }
- }
- if (validFiles.length === 0) return;
- // 上传文件
- try {
- const uploadPromises = validFiles.map((file) => uploadFile(file));
- const results = await Promise.all(uploadPromises);
- // 添加成功上传的图片
- results.forEach((result) => {
- if (result.success) {
- imageList.value.push(result.url);
- }
- });
- ElMessage.success(`成功上传${results.filter((r) => r.success).length}张图片`);
- } catch (error) {
- ElMessage.error('上传失败:' + error.message);
- }
- // 清空文件输入框
- event.target.value = '';
- };
- // 验证文件
- const validateFile = (file) => {
- // 检查文件类型
- const fileExtension = file.name.split('.').pop().toLowerCase();
- if (!props.accept.includes(fileExtension)) {
- ElMessage.warning(`不支持${fileExtension}格式,请选择${props.accept.join('、')}格式的图片`);
- return false;
- }
- // 检查文件大小
- const fileSizeMB = file.size / 1024 / 1024;
- if (props.maxSize && fileSizeMB > props.maxSize) {
- ElMessage.warning(`图片大小不能超过${props.maxSize}MB`);
- return false;
- }
- return true;
- };
- // 上传文件
- const uploadFile = async (file) => {
- try {
- const response = await adminApi.file.upload({}, file);
- if (response.code == '200') {
- return {
- success: true,
- url: response.data.url,
- };
- } else {
- throw new Error(response.msg || '上传失败');
- }
- } catch (error) {
- console.error('上传文件失败:', error);
- return {
- success: false,
- error: error.message,
- };
- }
- };
- // 打开文件管理器(非直传模式)
- const openFileManager = () => {
- // 这里可以集成现有的文件管理器组件
- console.log('打开文件管理器');
- ElMessage.info('文件管理器功能待实现');
- };
- // 图片预览状态
- const previewVisible = ref(false);
- const previewImageUrl = ref('');
- const previewImageList = ref([]);
- const previewInitialIndex = ref(0);
- // 预览图片
- const previewImage = (url, index = 0) => {
- previewImageUrl.value = url;
- previewImageList.value = [...imageList.value];
- previewInitialIndex.value = index;
- previewVisible.value = true;
- };
- // 关闭预览
- const closePreview = () => {
- previewVisible.value = false;
- previewImageUrl.value = '';
- previewImageList.value = [];
- previewInitialIndex.value = 0;
- };
- // 删除图片
- const removeImage = (index) => {
- imageList.value.splice(index, 1);
- };
- // 处理拖拽排序
- const handleSort = () => {
- // 拖拽排序后自动触发 watch 更新
- console.log('图片排序已更新');
- };
- </script>
- <style lang="scss" scoped>
- .sa-upload-image {
- .upload-container {
- gap: 8px;
- }
- .image-item {
- position: relative;
- border-radius: 6px;
- overflow: hidden;
- border: 1px solid #dcdfe6;
- .el-image {
- width: 100%;
- height: 100%;
- }
- .image-error {
- display: flex;
- align-items: center;
- justify-content: center;
- width: 100%;
- height: 100%;
- background: #f5f7fa;
- color: #909399;
- }
- .image-mask {
- position: absolute;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
- background: rgba(0, 0, 0, 0.6);
- display: flex;
- align-items: center;
- justify-content: center;
- gap: 8px;
- opacity: 0;
- transition: opacity 0.3s;
- .el-icon {
- color: white;
- font-size: 16px;
- cursor: pointer;
- padding: 4px;
- border-radius: 2px;
- transition: background-color 0.3s;
- &:hover {
- background: rgba(255, 255, 255, 0.2);
- }
- &.sortable-drag {
- cursor: move;
- }
- }
- }
- &:hover .image-mask {
- opacity: 1;
- }
- }
- .upload-trigger {
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- border: 2px dashed #dcdfe6;
- border-radius: 6px;
- cursor: pointer;
- transition: border-color 0.3s;
- background: #fafafa;
- &:hover {
- // 使用系统主色调
- border-color: var(--el-color-primary);
- }
- .upload-icon {
- font-size: 24px;
- color: #8c939d;
- margin-bottom: 4px;
- }
- .upload-text {
- font-size: 12px;
- color: #8c939d;
- }
- }
- .upload-tip {
- margin-top: 8px;
- font-size: 12px;
- color: #909399;
- line-height: 1.4;
- }
- /* 精简模式样式 */
- .compact-mode {
- border: 1px solid #dcdfe6;
- border-radius: 4px;
- &:hover {
- border-color: var(--el-color-primary);
- }
- }
- .compact-mask {
- background: transparent;
- pointer-events: none;
- .el-icon {
- position: absolute;
- top: 2px;
- right: 2px;
- background: rgba(0, 0, 0, 0.8);
- border-radius: 50%;
- padding: 1px;
- pointer-events: auto;
- width: 16px;
- height: 16px;
- display: flex;
- align-items: center;
- justify-content: center;
- font-size: 12px;
- }
- }
- .compact-trigger {
- border: 1px dashed #dcdfe6;
- border-radius: 4px;
- background: #fafafa;
- transition: all 0.3s;
- &:hover {
- border-color: var(--el-color-primary);
- background: var(--el-color-primary-light-9);
- }
- .upload-icon {
- color: #8c939d;
- }
- &:hover .upload-icon {
- color: var(--el-color-primary);
- }
- }
- }
- </style>
|