index.vue 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327
  1. <template>
  2. <div class="image-or-url-input">
  3. <el-tabs v-model="activeTab" type="card" @tab-click="handleTabClick">
  4. <el-tab-pane label="上传图片" name="upload">
  5. <div class="upload-container">
  6. <el-upload
  7. ref="imageUploadRef"
  8. :action="uploadImgUrl"
  9. list-type="picture-card"
  10. :on-success="handleUploadSuccess"
  11. :before-upload="handleBeforeUpload"
  12. :limit="1"
  13. :accept="fileAccept"
  14. :on-error="handleUploadError"
  15. :on-exceed="handleExceed"
  16. :before-remove="handleDelete"
  17. :show-file-list="true"
  18. :headers="headers"
  19. :file-list="fileList"
  20. :on-preview="handlePictureCardPreview"
  21. :class="{ hide: fileList.length >= 1 }"
  22. >
  23. <el-icon class="avatar-uploader-icon">
  24. <plus />
  25. </el-icon>
  26. </el-upload>
  27. <div v-if="showTip" class="el-upload__tip">
  28. 请上传
  29. <template v-if="fileSize">
  30. 大小不超过 <b style="color: #f56c6c">{{ fileSize }}MB</b>
  31. </template>
  32. <template v-if="fileType">
  33. 格式为 <b style="color: #f56c6c">{{ fileType.join('/') }}</b>
  34. </template>
  35. 的文件
  36. </div>
  37. </div>
  38. </el-tab-pane>
  39. <el-tab-pane label="输入链接" name="url">
  40. <div class="url-container">
  41. <el-input
  42. v-model="urlValue"
  43. placeholder="请输入链接地址"
  44. clearable
  45. @input="handleUrlInput"
  46. @blur="handleUrlBlur"
  47. >
  48. <template #prepend>URL</template>
  49. </el-input>
  50. <div class="url-error" v-if="urlValue && !isValidUrl">
  51. <el-alert title="请输入有效的链接地址" type="warning" :closable="false" />
  52. </div>
  53. </div>
  54. </el-tab-pane>
  55. </el-tabs>
  56. <el-dialog v-model="dialogVisible" title="预览" width="800px" append-to-body>
  57. <img :src="dialogImageUrl" style="display: block; max-width: 100%; margin: 0 auto" />
  58. </el-dialog>
  59. </div>
  60. </template>
  61. <script setup lang="ts">
  62. import { listByIds, delOss } from '@/api/system/oss';
  63. import { OssVO } from '@/api/system/oss/types';
  64. import { propTypes } from '@/utils/propTypes';
  65. import { globalHeaders } from '@/utils/request';
  66. import { compressAccurately } from 'image-conversion';
  67. const props = defineProps({
  68. modelValue: {
  69. type: String,
  70. default: ''
  71. },
  72. // 大小限制(MB)
  73. fileSize: propTypes.number.def(5),
  74. // 文件类型, 例如['png', 'jpg', 'jpeg']
  75. fileType: propTypes.array.def(['png', 'jpg', 'jpeg']),
  76. // 是否显示提示
  77. isShowTip: {
  78. type: Boolean,
  79. default: true
  80. },
  81. // 是否支持压缩,默认否
  82. compressSupport: {
  83. type: Boolean,
  84. default: false
  85. },
  86. // 压缩目标大小,单位KB。默认300KB以上文件才压缩,并压缩至300KB以内
  87. compressTargetSize: propTypes.number.def(300)
  88. });
  89. const { proxy } = getCurrentInstance() as ComponentInternalInstance;
  90. const emit = defineEmits(['update:modelValue']);
  91. const activeTab = ref('upload');
  92. const number = ref(0);
  93. const uploadList = ref<any[]>([]);
  94. const dialogImageUrl = ref('');
  95. const dialogVisible = ref(false);
  96. const urlValue = ref('');
  97. const isValidUrl = ref(false);
  98. const baseUrl = import.meta.env.VITE_APP_BASE_API;
  99. const uploadImgUrl = ref(baseUrl + '/resource/oss/upload');
  100. const headers = ref(globalHeaders());
  101. const fileList = ref<any[]>([]);
  102. const showTip = computed(() => props.isShowTip && (props.fileType || props.fileSize));
  103. const imageUploadRef = ref<ElUploadInstance>();
  104. // 监听 fileType 变化,更新 fileAccept
  105. const fileAccept = computed(() => props.fileType.map((type) => `.${type}`).join(','));
  106. // URL验证函数
  107. const validateUrl = (url: string) => {
  108. if (!url) return false;
  109. try {
  110. const urlObj = new URL(url);
  111. return urlObj.protocol === 'http:' || urlObj.protocol === 'https:';
  112. } catch {
  113. return false;
  114. }
  115. };
  116. // 处理URL输入
  117. const handleUrlInput = (value: string) => {
  118. urlValue.value = value;
  119. isValidUrl.value = validateUrl(value);
  120. if (isValidUrl.value) {
  121. emit('update:modelValue', value);
  122. }
  123. };
  124. // 处理URL失焦
  125. const handleUrlBlur = () => {
  126. if (urlValue.value && isValidUrl.value) {
  127. emit('update:modelValue', urlValue.value);
  128. }
  129. };
  130. // 移除图片加载错误处理函数,因为不再需要预览图片
  131. // 处理标签页切换
  132. const handleTabClick = () => {
  133. // 切换标签页时清空当前值
  134. if (activeTab.value === 'url') {
  135. urlValue.value = '';
  136. isValidUrl.value = false;
  137. }
  138. };
  139. // 加载OSS数据
  140. const loadOssData = async (ossId: string) => {
  141. try {
  142. const res = await listByIds(ossId);
  143. if (res.data && res.data.length > 0) {
  144. const item = res.data[0];
  145. fileList.value = [{ name: item.ossId, url: item.url, ossId: item.ossId }];
  146. }
  147. } catch (error) {
  148. console.warn('Invalid ossId:', ossId);
  149. fileList.value = [];
  150. }
  151. };
  152. // 监听 modelValue 变化
  153. watch(
  154. () => props.modelValue,
  155. (val: string) => {
  156. if (val) {
  157. // 判断是否为URL
  158. if (validateUrl(val)) {
  159. activeTab.value = 'url';
  160. urlValue.value = val;
  161. isValidUrl.value = true;
  162. } else {
  163. // 判断是否为OSS ID
  164. if (/^\d+$/.test(val)) {
  165. activeTab.value = 'upload';
  166. loadOssData(val);
  167. }
  168. }
  169. } else {
  170. urlValue.value = '';
  171. isValidUrl.value = false;
  172. fileList.value = [];
  173. }
  174. },
  175. { immediate: true }
  176. );
  177. /** 上传前loading加载 */
  178. const handleBeforeUpload = (file: any) => {
  179. let isImg = false;
  180. if (props.fileType.length) {
  181. let fileExtension = '';
  182. if (file.name.lastIndexOf('.') > -1) {
  183. fileExtension = file.name.slice(file.name.lastIndexOf('.') + 1);
  184. }
  185. isImg = props.fileType.some((type: any) => {
  186. if (file.type.indexOf(type) > -1) return true;
  187. if (fileExtension && fileExtension.indexOf(type) > -1) return true;
  188. return false;
  189. });
  190. } else {
  191. isImg = file.type.indexOf('image') > -1;
  192. }
  193. if (!isImg) {
  194. proxy?.$modal.msgError(`文件格式不正确, 请上传${props.fileType.join('/')}图片格式文件!`);
  195. return false;
  196. }
  197. if (file.name.includes(',')) {
  198. proxy?.$modal.msgError('文件名不正确,不能包含英文逗号!');
  199. return false;
  200. }
  201. if (props.fileSize) {
  202. const isLt = file.size / 1024 / 1024 < props.fileSize;
  203. if (!isLt) {
  204. proxy?.$modal.msgError(`上传头像图片大小不能超过 ${props.fileSize} MB!`);
  205. return false;
  206. }
  207. }
  208. //压缩图片,开启压缩并且大于指定的压缩大小时才压缩
  209. if (props.compressSupport && file.size / 1024 > props.compressTargetSize) {
  210. proxy?.$modal.loading('正在上传图片,请稍候...');
  211. number.value++;
  212. return compressAccurately(file, props.compressTargetSize);
  213. } else {
  214. proxy?.$modal.loading('正在上传图片,请稍候...');
  215. number.value++;
  216. }
  217. };
  218. // 文件个数超出
  219. const handleExceed = () => {
  220. proxy?.$modal.msgError('只能上传一个图片文件!');
  221. };
  222. // 上传成功回调
  223. const handleUploadSuccess = (res: any, file: UploadFile) => {
  224. if (res.code === 200) {
  225. uploadList.value.push({ name: res.data.fileName, url: res.data.url, ossId: res.data.ossId });
  226. uploadedSuccessfully();
  227. } else {
  228. number.value--;
  229. proxy?.$modal.closeLoading();
  230. proxy?.$modal.msgError(res.msg);
  231. imageUploadRef.value?.handleRemove(file);
  232. uploadedSuccessfully();
  233. }
  234. };
  235. // 删除图片
  236. const handleDelete = (file: UploadFile): boolean => {
  237. const findex = fileList.value.map((f) => f.name).indexOf(file.name);
  238. if (findex > -1 && uploadList.value.length === number.value) {
  239. const ossId = fileList.value[findex].ossId;
  240. delOss(ossId);
  241. fileList.value.splice(findex, 1);
  242. emit('update:modelValue', listToString(fileList.value));
  243. return false;
  244. }
  245. return true;
  246. };
  247. // 上传结束处理
  248. const uploadedSuccessfully = () => {
  249. if (number.value > 0 && uploadList.value.length === number.value) {
  250. fileList.value = fileList.value.filter((f) => f.url !== undefined).concat(uploadList.value);
  251. uploadList.value = [];
  252. number.value = 0;
  253. emit('update:modelValue', listToString(fileList.value));
  254. proxy?.$modal.closeLoading();
  255. }
  256. };
  257. // 上传失败
  258. const handleUploadError = () => {
  259. proxy?.$modal.msgError('上传图片失败');
  260. proxy?.$modal.closeLoading();
  261. };
  262. // 预览
  263. const handlePictureCardPreview = (file: any) => {
  264. dialogImageUrl.value = file.url;
  265. dialogVisible.value = true;
  266. };
  267. // 对象转成指定字符串分隔
  268. const listToString = (list: any[], separator?: string) => {
  269. let strs = '';
  270. separator = separator || ',';
  271. for (const i in list) {
  272. if (undefined !== list[i].ossId && list[i].url.indexOf('blob:') !== 0) {
  273. strs += list[i].ossId + separator;
  274. }
  275. }
  276. return strs != '' ? strs.substring(0, strs.length - 1) : '';
  277. };
  278. </script>
  279. <style lang="scss" scoped>
  280. .image-or-url-input {
  281. .upload-container {
  282. .el-upload__tip {
  283. margin-top: 10px;
  284. font-size: 12px;
  285. color: #606266;
  286. }
  287. }
  288. .url-container {
  289. .url-error {
  290. margin-top: 10px;
  291. }
  292. }
  293. }
  294. // .el-upload--picture-card 控制加号部分
  295. :deep(.hide .el-upload--picture-card) {
  296. display: none;
  297. }
  298. </style>