|
@@ -2,55 +2,39 @@
|
|
|
<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"
|
|
|
- >
|
|
|
+ <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 mr-2px"
|
|
|
- :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"
|
|
|
+ <div class="image-item mr-2px" :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' }"
|
|
|
- >
|
|
|
+ :style="{ cursor: compact ? 'pointer' : 'default' }">
|
|
|
<template #error>
|
|
|
<div class="image-error">
|
|
|
- <el-icon><Picture /></el-icon>
|
|
|
+ <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="t('common.delete')" size="12"
|
|
|
- ><Delete
|
|
|
- /></el-icon>
|
|
|
+ <el-icon @click.stop="removeImage(index)" :title="t('common.delete')" size="12">
|
|
|
+ <Delete />
|
|
|
+ </el-icon>
|
|
|
</template>
|
|
|
<template v-else>
|
|
|
- <el-icon class="sortable-drag" :title="t('common.dragSort')" size="24"
|
|
|
- ><Rank
|
|
|
- /></el-icon>
|
|
|
- <el-icon
|
|
|
- @click="previewImage(element, index)"
|
|
|
- :title="t('common.preview')"
|
|
|
- size="24"
|
|
|
- ><ZoomIn
|
|
|
- /></el-icon>
|
|
|
- <el-icon @click="removeImage(index)" :title="t('common.delete')" size="24"
|
|
|
- ><Delete
|
|
|
- /></el-icon>
|
|
|
+ <el-icon class="sortable-drag" :title="t('common.dragSort')" size="24">
|
|
|
+ <Rank />
|
|
|
+ </el-icon>
|
|
|
+ <el-icon @click="previewImage(element, index)" :title="t('common.preview')" size="24">
|
|
|
+ <ZoomIn />
|
|
|
+ </el-icon>
|
|
|
+ <el-icon @click="removeImage(index)" :title="t('common.delete')" size="24">
|
|
|
+ <Delete />
|
|
|
+ </el-icon>
|
|
|
</template>
|
|
|
</div>
|
|
|
</div>
|
|
@@ -58,17 +42,10 @@
|
|
|
</sa-draggable>
|
|
|
|
|
|
<!-- 上传按钮 -->
|
|
|
- <div
|
|
|
- v-if="!maxCount || imageList.length < maxCount"
|
|
|
- class="upload-wrapper"
|
|
|
- :style="{ width: size + 'px', height: size + 'px' }"
|
|
|
- >
|
|
|
- <div
|
|
|
- class="upload-trigger"
|
|
|
- :class="{ 'compact-trigger': compact, 'is-uploading': uploading }"
|
|
|
- :style="{ width: size + 'px', height: size + 'px' }"
|
|
|
- @click="handleUploadClick"
|
|
|
- >
|
|
|
+ <div v-if="!maxCount || imageList.length < maxCount" class="upload-wrapper"
|
|
|
+ :style="{ width: size + 'px', height: size + 'px' }">
|
|
|
+ <div class="upload-trigger" :class="{ 'compact-trigger': compact, 'is-uploading': uploading }"
|
|
|
+ :style="{ width: size + 'px', height: size + 'px' }" @click="handleUploadClick">
|
|
|
<el-icon class="upload-icon" :size="compact ? 16 : 24">
|
|
|
<Plus />
|
|
|
</el-icon>
|
|
@@ -85,561 +62,591 @@
|
|
|
</div>
|
|
|
|
|
|
<!-- 隐藏的文件输入框 -->
|
|
|
- <input
|
|
|
- ref="fileInputRef"
|
|
|
- type="file"
|
|
|
- :accept="acceptString"
|
|
|
- :multiple="multiple"
|
|
|
- style="display: none"
|
|
|
- @change="handleFileSelect"
|
|
|
- />
|
|
|
+ <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">{{ t('common.maxUpload', { count: maxCount }) }},</span>
|
|
|
- <span v-if="accept && accept.length"
|
|
|
- >{{ t('common.supportFormats', { formats: accept.join('、') }) }},</span
|
|
|
- >
|
|
|
+ <span v-if="accept && accept.length">{{ t('common.supportFormats', { formats: accept.join('、') }) }},</span>
|
|
|
<span v-if="maxSize">{{ t('common.maxFileSize', { size: maxSize }) }}</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"
|
|
|
- />
|
|
|
+ <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,
|
|
|
- },
|
|
|
- };
|
|
|
+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>
|
|
|
- import { useI18n } from 'vue-i18n';
|
|
|
-
|
|
|
- const { t } = useI18n();
|
|
|
-
|
|
|
- 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]);
|
|
|
-
|
|
|
- // 上传loading状态
|
|
|
- const uploading = ref(false);
|
|
|
-
|
|
|
- // 计算接受的文件类型字符串
|
|
|
- const acceptString = computed(() => {
|
|
|
- return props.accept.map((type) => `.${type}`).join(',');
|
|
|
- });
|
|
|
-
|
|
|
- // 添加一个标志来防止循环更新
|
|
|
- let isUpdatingFromProps = false;
|
|
|
- let isUpdatingFromInternal = false;
|
|
|
-
|
|
|
- // 数组比较函数
|
|
|
- const arraysEqual = (a, b) => {
|
|
|
- if (a.length !== b.length) return false;
|
|
|
- return a.every((val, index) => val === b[index]);
|
|
|
- };
|
|
|
-
|
|
|
- // 监听外部数据变化
|
|
|
- watch(
|
|
|
- () => props.modelValue,
|
|
|
- (newValue) => {
|
|
|
- if (isUpdatingFromInternal) return;
|
|
|
-
|
|
|
- const currentValue = imageList.value;
|
|
|
- if (!arraysEqual(newValue, currentValue)) {
|
|
|
- isUpdatingFromProps = true;
|
|
|
- imageList.value = [...newValue];
|
|
|
- setTimeout(() => {
|
|
|
- isUpdatingFromProps = false;
|
|
|
- }, 0);
|
|
|
- }
|
|
|
- },
|
|
|
- { deep: true },
|
|
|
- );
|
|
|
-
|
|
|
- // 监听内部数据变化
|
|
|
- watch(
|
|
|
- imageList,
|
|
|
- (newValue) => {
|
|
|
- if (isUpdatingFromProps) return;
|
|
|
-
|
|
|
- if (!arraysEqual(newValue, props.modelValue)) {
|
|
|
- isUpdatingFromInternal = true;
|
|
|
- emit('update:modelValue', [...newValue]);
|
|
|
- emit('change', [...newValue]);
|
|
|
- setTimeout(() => {
|
|
|
- isUpdatingFromInternal = false;
|
|
|
- }, 0);
|
|
|
- }
|
|
|
- },
|
|
|
- { deep: true },
|
|
|
- );
|
|
|
-
|
|
|
- // 处理上传点击
|
|
|
- const handleUpload = () => {
|
|
|
- if (props.directUpload) {
|
|
|
- // 直传模式,直接打开文件选择
|
|
|
- fileInputRef.value?.click();
|
|
|
- } else {
|
|
|
- // 非直传模式,打开文件管理弹窗
|
|
|
- openFileManager();
|
|
|
+import { useI18n } from 'vue-i18n';
|
|
|
+
|
|
|
+const { t } = useI18n();
|
|
|
+
|
|
|
+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,
|
|
|
+ },
|
|
|
+ // 图片宽高比例要求 (宽/高)
|
|
|
+ aspectRatio: {
|
|
|
+ type: Number,
|
|
|
+ default: null,
|
|
|
+ },
|
|
|
+ // 是否强制执行宽高比例
|
|
|
+ enforceAspectRatio: {
|
|
|
+ type: Boolean,
|
|
|
+ default: false,
|
|
|
+ },
|
|
|
+});
|
|
|
+
|
|
|
+const emit = defineEmits(['update:modelValue', 'change']);
|
|
|
+
|
|
|
+// 文件输入框引用
|
|
|
+const fileInputRef = ref();
|
|
|
+
|
|
|
+// 图片列表
|
|
|
+const imageList = ref([...props.modelValue]);
|
|
|
+
|
|
|
+// 上传loading状态
|
|
|
+const uploading = ref(false);
|
|
|
+
|
|
|
+// 计算接受的文件类型字符串
|
|
|
+const acceptString = computed(() => {
|
|
|
+ return props.accept.map((type) => `.${type}`).join(',');
|
|
|
+});
|
|
|
+
|
|
|
+// 添加一个标志来防止循环更新
|
|
|
+let isUpdatingFromProps = false;
|
|
|
+let isUpdatingFromInternal = false;
|
|
|
+
|
|
|
+// 数组比较函数
|
|
|
+const arraysEqual = (a, b) => {
|
|
|
+ if (a.length !== b.length) return false;
|
|
|
+ return a.every((val, index) => val === b[index]);
|
|
|
+};
|
|
|
+
|
|
|
+// 监听外部数据变化
|
|
|
+watch(
|
|
|
+ () => props.modelValue,
|
|
|
+ (newValue) => {
|
|
|
+ if (isUpdatingFromInternal) return;
|
|
|
+
|
|
|
+ const currentValue = imageList.value;
|
|
|
+ if (!arraysEqual(newValue, currentValue)) {
|
|
|
+ isUpdatingFromProps = true;
|
|
|
+ imageList.value = [...newValue];
|
|
|
+ setTimeout(() => {
|
|
|
+ isUpdatingFromProps = false;
|
|
|
+ }, 0);
|
|
|
}
|
|
|
- };
|
|
|
-
|
|
|
- // 处理点击事件(带上传状态检查)
|
|
|
- const handleUploadClick = () => {
|
|
|
- if (!uploading.value) {
|
|
|
- handleUpload();
|
|
|
+ },
|
|
|
+ { deep: true },
|
|
|
+);
|
|
|
+
|
|
|
+// 监听内部数据变化
|
|
|
+watch(
|
|
|
+ imageList,
|
|
|
+ (newValue) => {
|
|
|
+ if (isUpdatingFromProps) return;
|
|
|
+
|
|
|
+ if (!arraysEqual(newValue, props.modelValue)) {
|
|
|
+ isUpdatingFromInternal = true;
|
|
|
+ emit('update:modelValue', [...newValue]);
|
|
|
+ emit('change', [...newValue]);
|
|
|
+ setTimeout(() => {
|
|
|
+ isUpdatingFromInternal = false;
|
|
|
+ }, 0);
|
|
|
}
|
|
|
- };
|
|
|
+ },
|
|
|
+ { deep: true },
|
|
|
+);
|
|
|
+
|
|
|
+// 处理上传点击
|
|
|
+const handleUpload = () => {
|
|
|
+ if (props.directUpload) {
|
|
|
+ // 直传模式,直接打开文件选择
|
|
|
+ fileInputRef.value?.click();
|
|
|
+ } else {
|
|
|
+ // 非直传模式,打开文件管理弹窗
|
|
|
+ openFileManager();
|
|
|
+ }
|
|
|
+};
|
|
|
|
|
|
- // 处理文件选择
|
|
|
- const handleFileSelect = async (event) => {
|
|
|
- const files = Array.from(event.target.files);
|
|
|
+// 处理点击事件(带上传状态检查)
|
|
|
+const handleUploadClick = () => {
|
|
|
+ if (!uploading.value) {
|
|
|
+ handleUpload();
|
|
|
+ }
|
|
|
+};
|
|
|
|
|
|
- if (!files.length) return;
|
|
|
+// 处理文件选择
|
|
|
+const handleFileSelect = async (event) => {
|
|
|
+ const files = Array.from(event.target.files);
|
|
|
|
|
|
- // 检查数量限制
|
|
|
- if (props.maxCount && imageList.value.length + files.length > props.maxCount) {
|
|
|
- ElMessage.warning(t('message.maxUploadLimit', { count: props.maxCount }));
|
|
|
- return;
|
|
|
- }
|
|
|
+ if (!files.length) return;
|
|
|
|
|
|
- // 验证文件
|
|
|
- const validFiles = [];
|
|
|
- for (const file of files) {
|
|
|
- if (validateFile(file)) {
|
|
|
- validFiles.push(file);
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- if (validFiles.length === 0) return;
|
|
|
+ // 检查数量限制
|
|
|
+ if (props.maxCount && imageList.value.length + files.length > props.maxCount) {
|
|
|
+ ElMessage.warning(t('message.maxUploadLimit', { count: props.maxCount }));
|
|
|
+ return;
|
|
|
+ }
|
|
|
|
|
|
- // 开始上传,显示loading
|
|
|
- uploading.value = true;
|
|
|
+ // 验证文件
|
|
|
+ const validFiles = [];
|
|
|
+ for (const file of files) {
|
|
|
+ if (await validateFile(file)) {
|
|
|
+ validFiles.push(file);
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
- // 上传文件
|
|
|
- try {
|
|
|
- const uploadPromises = validFiles.map((file) => uploadFile(file));
|
|
|
- const results = await Promise.all(uploadPromises);
|
|
|
+ if (validFiles.length === 0) return;
|
|
|
|
|
|
- // 添加成功上传的图片
|
|
|
- results.forEach((result) => {
|
|
|
- if (result.success) {
|
|
|
- imageList.value.push(result.url);
|
|
|
- }
|
|
|
- });
|
|
|
+ // 开始上传,显示loading
|
|
|
+ uploading.value = true;
|
|
|
|
|
|
- const successCount = results.filter((r) => r.success).length;
|
|
|
- if (successCount > 0) {
|
|
|
- ElMessage.success(t('message.uploadSuccess', { count: successCount }));
|
|
|
+ // 上传文件
|
|
|
+ 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);
|
|
|
}
|
|
|
+ });
|
|
|
|
|
|
- // 如果有失败的,显示失败信息
|
|
|
- const failedCount = results.length - successCount;
|
|
|
- if (failedCount > 0) {
|
|
|
- ElMessage.error(t('message.uploadFailed', { count: failedCount }));
|
|
|
- }
|
|
|
- } catch (error) {
|
|
|
- ElMessage.error(t('message.uploadError') + ': ' + error.message);
|
|
|
- } finally {
|
|
|
- // 无论成功还是失败,都关闭loading
|
|
|
- uploading.value = false;
|
|
|
+ const successCount = results.filter((r) => r.success).length;
|
|
|
+ if (successCount > 0) {
|
|
|
+ ElMessage.success(t('message.uploadSuccess', { count: successCount }));
|
|
|
}
|
|
|
|
|
|
- // 清空文件输入框
|
|
|
- event.target.value = '';
|
|
|
- };
|
|
|
-
|
|
|
- // 验证文件
|
|
|
- const validateFile = (file) => {
|
|
|
- // 检查文件类型
|
|
|
- const fileExtension = file.name.split('.').pop().toLowerCase();
|
|
|
- if (!props.accept.includes(fileExtension)) {
|
|
|
- ElMessage.warning(
|
|
|
- t('message.unsupportedFormat', {
|
|
|
- extension: fileExtension,
|
|
|
- formats: props.accept.join('、'),
|
|
|
- }),
|
|
|
- );
|
|
|
- return false;
|
|
|
+ // 如果有失败的,显示失败信息
|
|
|
+ const failedCount = results.length - successCount;
|
|
|
+ if (failedCount > 0) {
|
|
|
+ ElMessage.error(t('message.uploadFailed', { count: failedCount }));
|
|
|
}
|
|
|
+ } catch (error) {
|
|
|
+ ElMessage.error(t('message.uploadError') + ': ' + error.message);
|
|
|
+ } finally {
|
|
|
+ // 无论成功还是失败,都关闭loading
|
|
|
+ uploading.value = false;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 清空文件输入框
|
|
|
+ event.target.value = '';
|
|
|
+};
|
|
|
+
|
|
|
+// 验证文件
|
|
|
+const validateFile = async (file) => {
|
|
|
+ // 检查文件类型
|
|
|
+ const fileExtension = file.name.split('.').pop().toLowerCase();
|
|
|
+ if (!props.accept.includes(fileExtension)) {
|
|
|
+ ElMessage.warning(
|
|
|
+ t('message.unsupportedFormat', {
|
|
|
+ extension: fileExtension,
|
|
|
+ formats: props.accept.join('、'),
|
|
|
+ }),
|
|
|
+ );
|
|
|
+ return false;
|
|
|
+ }
|
|
|
|
|
|
- // 检查文件大小
|
|
|
- const fileSizeMB = file.size / 1024 / 1024;
|
|
|
- if (props.maxSize && fileSizeMB > props.maxSize) {
|
|
|
- ElMessage.warning(t('message.fileSizeExceeded', { size: props.maxSize }));
|
|
|
+ // 检查文件大小
|
|
|
+ const fileSizeMB = file.size / 1024 / 1024;
|
|
|
+ if (props.maxSize && fileSizeMB > props.maxSize) {
|
|
|
+ ElMessage.warning(t('message.fileSizeExceeded', { size: props.maxSize }));
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 检查图片宽高比例
|
|
|
+ if (props.aspectRatio && props.enforceAspectRatio) {
|
|
|
+ const isValidRatio = await validateImageAspectRatio(file, props.aspectRatio);
|
|
|
+ if (!isValidRatio) {
|
|
|
+ const ratioText = props.aspectRatio === 1 ? '1:1' : `${props.aspectRatio}:1`;
|
|
|
+ ElMessage.warning(t('message.invalidAspectRatio', { ratio: ratioText }));
|
|
|
return false;
|
|
|
}
|
|
|
+ }
|
|
|
|
|
|
- return true;
|
|
|
- };
|
|
|
-
|
|
|
- // 上传文件
|
|
|
- const uploadFile = async (file) => {
|
|
|
- try {
|
|
|
- var formData = new FormData();
|
|
|
- formData.append('file', file);
|
|
|
- const response = await adminApi.file.upload({}, formData);
|
|
|
- if (response.code == '200') {
|
|
|
- return {
|
|
|
- success: true,
|
|
|
- url: response.data,
|
|
|
- };
|
|
|
- } else {
|
|
|
- throw new Error(response.msg || t('message.uploadFailed'));
|
|
|
- }
|
|
|
- } catch (error) {
|
|
|
- console.error('上传文件失败:', error);
|
|
|
+ return true;
|
|
|
+};
|
|
|
+
|
|
|
+// 验证图片宽高比例
|
|
|
+const validateImageAspectRatio = (file, expectedRatio) => {
|
|
|
+ return new Promise((resolve) => {
|
|
|
+ const img = new Image();
|
|
|
+ const url = URL.createObjectURL(file);
|
|
|
+
|
|
|
+ img.onload = () => {
|
|
|
+ const actualRatio = img.width / img.height;
|
|
|
+ const tolerance = 0.1; // 允许10%的误差
|
|
|
+ const isValid = Math.abs(actualRatio - expectedRatio) <= tolerance;
|
|
|
+ URL.revokeObjectURL(url);
|
|
|
+ resolve(isValid);
|
|
|
+ };
|
|
|
+
|
|
|
+ img.onerror = () => {
|
|
|
+ URL.revokeObjectURL(url);
|
|
|
+ resolve(false);
|
|
|
+ };
|
|
|
+
|
|
|
+ img.src = url;
|
|
|
+ });
|
|
|
+};
|
|
|
+
|
|
|
+// 上传文件
|
|
|
+const uploadFile = async (file) => {
|
|
|
+ try {
|
|
|
+ var formData = new FormData();
|
|
|
+ formData.append('file', file);
|
|
|
+ const response = await adminApi.file.upload({}, formData);
|
|
|
+ if (response.code == '200') {
|
|
|
return {
|
|
|
- success: false,
|
|
|
- error: error.message,
|
|
|
+ success: true,
|
|
|
+ url: response.data,
|
|
|
};
|
|
|
+ } else {
|
|
|
+ throw new Error(response.msg || t('message.uploadFailed'));
|
|
|
}
|
|
|
- };
|
|
|
-
|
|
|
- // 打开文件管理器(非直传模式)
|
|
|
- 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('图片排序已更新');
|
|
|
- };
|
|
|
+ } 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;
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
+.sa-upload-image {
|
|
|
+ .upload-container {
|
|
|
+ gap: 8px;
|
|
|
+ }
|
|
|
|
|
|
- &:hover .image-mask {
|
|
|
- opacity: 1;
|
|
|
- }
|
|
|
- }
|
|
|
+ .image-item {
|
|
|
+ position: relative;
|
|
|
+ border-radius: 6px;
|
|
|
+ overflow: hidden;
|
|
|
+ border: 1px solid #dcdfe6;
|
|
|
|
|
|
- .upload-wrapper {
|
|
|
- position: relative;
|
|
|
- display: inline-block;
|
|
|
+ .el-image {
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
}
|
|
|
|
|
|
- .upload-trigger {
|
|
|
+ .image-error {
|
|
|
display: flex;
|
|
|
- flex-direction: column;
|
|
|
align-items: center;
|
|
|
justify-content: center;
|
|
|
- border: 2px dashed #dcdfe6;
|
|
|
- border-radius: 6px;
|
|
|
- cursor: pointer;
|
|
|
- transition: all 0.3s;
|
|
|
- background: #fafafa;
|
|
|
-
|
|
|
- &:hover:not(.is-uploading) {
|
|
|
- border-color: var(--el-color-primary);
|
|
|
- }
|
|
|
-
|
|
|
- &.is-uploading {
|
|
|
- cursor: not-allowed;
|
|
|
- border-color: var(--el-color-primary);
|
|
|
- border-style: solid;
|
|
|
- background: var(--el-color-primary-light-9);
|
|
|
- animation: uploadingBorder 2s ease-in-out infinite;
|
|
|
- }
|
|
|
-
|
|
|
- .upload-icon {
|
|
|
- font-size: 24px;
|
|
|
- color: #8c939d;
|
|
|
- margin-bottom: 4px;
|
|
|
- transition: all 0.3s;
|
|
|
- }
|
|
|
-
|
|
|
- .upload-text {
|
|
|
- font-size: 12px;
|
|
|
- color: #8c939d;
|
|
|
- transition: all 0.3s;
|
|
|
- text-align: center;
|
|
|
- }
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ background: #f5f7fa;
|
|
|
+ color: #909399;
|
|
|
}
|
|
|
|
|
|
- .upload-loading {
|
|
|
+ .image-mask {
|
|
|
position: absolute;
|
|
|
top: 0;
|
|
|
left: 0;
|
|
|
right: 0;
|
|
|
bottom: 0;
|
|
|
+ background: rgba(0, 0, 0, 0.6);
|
|
|
display: flex;
|
|
|
- flex-direction: column;
|
|
|
align-items: center;
|
|
|
justify-content: center;
|
|
|
- background: rgba(255, 255, 255, 0.9);
|
|
|
- border-radius: 6px;
|
|
|
- backdrop-filter: blur(2px);
|
|
|
-
|
|
|
- .loading-spinner {
|
|
|
- width: 20px;
|
|
|
- height: 20px;
|
|
|
- border: 2px solid #e4e7ed;
|
|
|
- border-top: 2px solid var(--el-color-primary);
|
|
|
- border-radius: 50%;
|
|
|
- animation: spin 1s linear infinite;
|
|
|
- margin-bottom: 8px;
|
|
|
- }
|
|
|
+ gap: 8px;
|
|
|
+ opacity: 0;
|
|
|
+ transition: opacity 0.3s;
|
|
|
|
|
|
- .loading-text {
|
|
|
- font-size: 12px;
|
|
|
- color: var(--el-color-primary);
|
|
|
- font-weight: 500;
|
|
|
+ .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;
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- @keyframes spin {
|
|
|
- 0% {
|
|
|
- transform: rotate(0deg);
|
|
|
- }
|
|
|
- 100% {
|
|
|
- transform: rotate(360deg);
|
|
|
- }
|
|
|
+ &:hover .image-mask {
|
|
|
+ opacity: 1;
|
|
|
}
|
|
|
+ }
|
|
|
|
|
|
- @keyframes uploadingBorder {
|
|
|
- 0% {
|
|
|
- border-color: var(--el-color-primary);
|
|
|
- box-shadow: 0 0 0 0 var(--el-color-primary-light-7);
|
|
|
- }
|
|
|
- 50% {
|
|
|
- border-color: var(--el-color-primary-light-3);
|
|
|
- box-shadow: 0 0 0 4px var(--el-color-primary-light-8);
|
|
|
- }
|
|
|
- 100% {
|
|
|
- border-color: var(--el-color-primary);
|
|
|
- box-shadow: 0 0 0 0 var(--el-color-primary-light-7);
|
|
|
- }
|
|
|
+ .upload-wrapper {
|
|
|
+ position: relative;
|
|
|
+ display: inline-block;
|
|
|
+ }
|
|
|
+
|
|
|
+ .upload-trigger {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ border: 2px dashed #dcdfe6;
|
|
|
+ border-radius: 6px;
|
|
|
+ cursor: pointer;
|
|
|
+ transition: all 0.3s;
|
|
|
+ background: #fafafa;
|
|
|
+
|
|
|
+ &:hover:not(.is-uploading) {
|
|
|
+ border-color: var(--el-color-primary);
|
|
|
+ }
|
|
|
+
|
|
|
+ &.is-uploading {
|
|
|
+ cursor: not-allowed;
|
|
|
+ border-color: var(--el-color-primary);
|
|
|
+ border-style: solid;
|
|
|
+ background: var(--el-color-primary-light-9);
|
|
|
+ animation: uploadingBorder 2s ease-in-out infinite;
|
|
|
+ }
|
|
|
+
|
|
|
+ .upload-icon {
|
|
|
+ font-size: 24px;
|
|
|
+ color: #8c939d;
|
|
|
+ margin-bottom: 4px;
|
|
|
+ transition: all 0.3s;
|
|
|
}
|
|
|
|
|
|
- .upload-tip {
|
|
|
- margin-top: 8px;
|
|
|
+ .upload-text {
|
|
|
font-size: 12px;
|
|
|
- color: #909399;
|
|
|
- line-height: 1.4;
|
|
|
+ color: #8c939d;
|
|
|
+ transition: all 0.3s;
|
|
|
+ text-align: center;
|
|
|
}
|
|
|
+ }
|
|
|
|
|
|
- /* 精简模式样式 */
|
|
|
- .compact-mode {
|
|
|
- border: 1px solid #dcdfe6;
|
|
|
- border-radius: 4px;
|
|
|
+ .upload-loading {
|
|
|
+ position: absolute;
|
|
|
+ top: 0;
|
|
|
+ left: 0;
|
|
|
+ right: 0;
|
|
|
+ bottom: 0;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ background: rgba(255, 255, 255, 0.9);
|
|
|
+ border-radius: 6px;
|
|
|
+ backdrop-filter: blur(2px);
|
|
|
+
|
|
|
+ .loading-spinner {
|
|
|
+ width: 20px;
|
|
|
+ height: 20px;
|
|
|
+ border: 2px solid #e4e7ed;
|
|
|
+ border-top: 2px solid var(--el-color-primary);
|
|
|
+ border-radius: 50%;
|
|
|
+ animation: spin 1s linear infinite;
|
|
|
+ margin-bottom: 8px;
|
|
|
+ }
|
|
|
|
|
|
- &:hover {
|
|
|
- border-color: var(--el-color-primary);
|
|
|
- }
|
|
|
+ .loading-text {
|
|
|
+ font-size: 12px;
|
|
|
+ color: var(--el-color-primary);
|
|
|
+ font-weight: 500;
|
|
|
}
|
|
|
+ }
|
|
|
|
|
|
- .compact-mask {
|
|
|
- background: transparent;
|
|
|
- pointer-events: none;
|
|
|
+ @keyframes spin {
|
|
|
+ 0% {
|
|
|
+ transform: rotate(0deg);
|
|
|
+ }
|
|
|
|
|
|
- .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;
|
|
|
- }
|
|
|
+ 100% {
|
|
|
+ transform: rotate(360deg);
|
|
|
}
|
|
|
+ }
|
|
|
|
|
|
- .compact-trigger {
|
|
|
- border: 1px dashed #dcdfe6;
|
|
|
- border-radius: 4px;
|
|
|
- background: #fafafa;
|
|
|
- transition: all 0.3s;
|
|
|
+ @keyframes uploadingBorder {
|
|
|
+ 0% {
|
|
|
+ border-color: var(--el-color-primary);
|
|
|
+ box-shadow: 0 0 0 0 var(--el-color-primary-light-7);
|
|
|
+ }
|
|
|
|
|
|
- &:hover {
|
|
|
- border-color: var(--el-color-primary);
|
|
|
- background: var(--el-color-primary-light-9);
|
|
|
- }
|
|
|
+ 50% {
|
|
|
+ border-color: var(--el-color-primary-light-3);
|
|
|
+ box-shadow: 0 0 0 4px var(--el-color-primary-light-8);
|
|
|
+ }
|
|
|
|
|
|
- .upload-icon {
|
|
|
- color: #8c939d;
|
|
|
- }
|
|
|
+ 100% {
|
|
|
+ border-color: var(--el-color-primary);
|
|
|
+ box-shadow: 0 0 0 0 var(--el-color-primary-light-7);
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
- &:hover .upload-icon {
|
|
|
- color: var(--el-color-primary);
|
|
|
- }
|
|
|
+ .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>
|