|
@@ -1,194 +1,224 @@
|
|
|
<template>
|
|
|
- <div class="input file" :class="{ suffix: $slots.icon, disabled: disabled, valuable }">
|
|
|
- <template v-if="valuable">
|
|
|
- <slot name="valuable" :key="modelValue" />
|
|
|
- </template>
|
|
|
- <input class="ui-text" type="file" ref="inputRef" :accept="accept" :multiple="multiple" @change="selectFileHandler" v-if="!maxLen || maxLen > modelValue.length" />
|
|
|
- <template v-if="!$slots.replace">
|
|
|
- <span class="replace">
|
|
|
- <div class="placeholder" v-if="!valuable">
|
|
|
- <p><ui-icon type="add" /></p>
|
|
|
- <p>{{ placeholder }}</p>
|
|
|
- <p class="bottom">
|
|
|
- <template v-if="!othPlaceholder">
|
|
|
- <template v-if="accept">支持 {{ accept }} 等格式,</template>
|
|
|
- <template v-if="normalizeScale">宽*高比例 {{ scale }},</template>
|
|
|
- <template v-if="maxSize">大小不超过 {{ sizeStr }}{{ maxLen ? ',' : '' }}</template>
|
|
|
- <template v-if="maxLen">个数不超过 {{ maxLen }}个</template>
|
|
|
- </template>
|
|
|
- <template v-else>
|
|
|
- {{ othPlaceholder }}
|
|
|
- </template>
|
|
|
- </p>
|
|
|
- </div>
|
|
|
-
|
|
|
- <span v-else v-if="!maxLen || maxLen > modelValue.length">
|
|
|
- {{ multiple ? '继续添加' : '替换' }}
|
|
|
- </span>
|
|
|
- <span class="tj" v-if="maxLen && modelValue.length">
|
|
|
- <span>{{ modelValue.length || 0 }}</span> / {{ maxLen }}
|
|
|
- </span>
|
|
|
- </span>
|
|
|
- </template>
|
|
|
- <div class="use-replace" v-else>
|
|
|
- <slot name="replace" />
|
|
|
+ <div class="input file" :class="{ suffix: $slots.icon, disabled: disabled, valuable }">
|
|
|
+ <template v-if="valuable">
|
|
|
+ <slot name="valuable" :key="modelValue" />
|
|
|
+ </template>
|
|
|
+ <input
|
|
|
+ class="ui-text"
|
|
|
+ type="file"
|
|
|
+ ref="inputRef"
|
|
|
+ :accept="accept"
|
|
|
+ :multiple="multiple"
|
|
|
+ @change="selectFileHandler"
|
|
|
+ v-if="!maxLen || maxLen > modelValue.length"
|
|
|
+ />
|
|
|
+ <template v-if="!$slots.replace">
|
|
|
+ <span class="replace">
|
|
|
+ <div class="placeholder" v-if="!valuable">
|
|
|
+ <p><ui-icon type="add" /></p>
|
|
|
+ <p>{{ placeholder }}</p>
|
|
|
+ <p class="bottom">
|
|
|
+ <template v-if="!othPlaceholder">
|
|
|
+ <template v-if="accept">{{ $t("sys.acceptTip", { accept }) }} </template>
|
|
|
+ <template v-if="normalizeScale"
|
|
|
+ >{{ $t("sys.scaleTip", { scale }) }}
|
|
|
+ </template>
|
|
|
+ <template v-if="maxSize">{{ $t("sys.sizeTip", { sizeStr }) }} </template>
|
|
|
+ <template v-if="maxLen">{{ $t("sys.countTip", { maxLen }) }} </template>
|
|
|
+ </template>
|
|
|
+ <template v-else>
|
|
|
+ {{ othPlaceholder }}
|
|
|
+ </template>
|
|
|
+ </p>
|
|
|
</div>
|
|
|
+
|
|
|
+ <span v-else v-if="!maxLen || maxLen > modelValue.length">
|
|
|
+ {{ multiple ? $t("sys.continueAdd") : $t("sys.rep") }}
|
|
|
+ </span>
|
|
|
+ <span class="tj" v-if="maxLen && modelValue.length">
|
|
|
+ <span>{{ modelValue.length || 0 }}</span> / {{ maxLen }}
|
|
|
+ </span>
|
|
|
+ </span>
|
|
|
+ </template>
|
|
|
+ <div class="use-replace" v-else>
|
|
|
+ <slot name="replace" />
|
|
|
</div>
|
|
|
+ </div>
|
|
|
</template>
|
|
|
|
|
|
<script setup>
|
|
|
-import { filePropsDesc } from './state'
|
|
|
-import { toRawType } from '../../utils'
|
|
|
-import Message from '../message'
|
|
|
-import { defineProps, defineEmits, defineExpose, ref, computed } from 'vue'
|
|
|
+import { filePropsDesc } from "./state";
|
|
|
+import { toRawType } from "../../utils";
|
|
|
+import Message from "../message";
|
|
|
+import { defineProps, defineEmits, defineExpose, ref, computed } from "vue";
|
|
|
+import { ui18n } from "@/lang";
|
|
|
|
|
|
const props = defineProps({
|
|
|
- ...filePropsDesc,
|
|
|
-})
|
|
|
-const emit = defineEmits(['update:modelValue'])
|
|
|
-const inputRef = ref(null)
|
|
|
+ ...filePropsDesc,
|
|
|
+});
|
|
|
+const emit = defineEmits(["update:modelValue"]);
|
|
|
+const inputRef = ref(null);
|
|
|
const normalizeScale = computed(() => {
|
|
|
- if (props.scale) {
|
|
|
- const [w, h] = props.scale.split(':')
|
|
|
- if (Number(w) && Number(h)) {
|
|
|
- return [Number(w), Number(h)]
|
|
|
- }
|
|
|
+ if (props.scale) {
|
|
|
+ const [w, h] = props.scale.split(":");
|
|
|
+ if (Number(w) && Number(h)) {
|
|
|
+ return [Number(w), Number(h)];
|
|
|
}
|
|
|
-})
|
|
|
+ }
|
|
|
+});
|
|
|
|
|
|
-const valuable = computed(() => (Array.isArray(props.modelValue) ? props.modelValue.length : !!props.modelValue))
|
|
|
+const valuable = computed(() =>
|
|
|
+ Array.isArray(props.modelValue) ? props.modelValue.length : !!props.modelValue
|
|
|
+);
|
|
|
const sizeStr = computed(() => {
|
|
|
- if (props.maxSize) {
|
|
|
- const mb = props.maxSize / 1024 / 1024
|
|
|
- if (mb > 1024) {
|
|
|
- return mb / 1024 + 'GB'
|
|
|
- } else {
|
|
|
- return mb + 'MB'
|
|
|
- }
|
|
|
+ if (props.maxSize) {
|
|
|
+ const mb = props.maxSize / 1024 / 1024;
|
|
|
+ if (mb > 1024) {
|
|
|
+ return mb / 1024 + "GB";
|
|
|
+ } else {
|
|
|
+ return mb + "MB";
|
|
|
}
|
|
|
-})
|
|
|
+ }
|
|
|
+});
|
|
|
|
|
|
const supports = {
|
|
|
- image: {
|
|
|
- types: ['image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/webp'],
|
|
|
- preview(file, url) {
|
|
|
- return new Promise((resolve, reject) => {
|
|
|
- const img = new Image()
|
|
|
- img.onload = () => resolve([img.width, img.height, file])
|
|
|
- img.onerror = reject
|
|
|
- img.src = url
|
|
|
- })
|
|
|
- },
|
|
|
+ image: {
|
|
|
+ types: ["image/jpeg", "image/jpg", "image/png", "image/gif", "image/webp"],
|
|
|
+ preview(file, url) {
|
|
|
+ return new Promise((resolve, reject) => {
|
|
|
+ const img = new Image();
|
|
|
+ img.onload = () => resolve([img.width, img.height, file]);
|
|
|
+ img.onerror = reject;
|
|
|
+ img.src = url;
|
|
|
+ });
|
|
|
},
|
|
|
- video: {
|
|
|
- types: ['video/mp4'],
|
|
|
- preview(file, url) {
|
|
|
- return new Promise((resolve, reject) => {
|
|
|
- const video = document.createElement('video')
|
|
|
- video.preload = 'metadata'
|
|
|
- video.onloadedmetadata = () => resolve([video.videoWidth, video.videoHeight, file])
|
|
|
- video.onerror = reject
|
|
|
- video.src = url
|
|
|
- })
|
|
|
- },
|
|
|
+ },
|
|
|
+ video: {
|
|
|
+ types: ["video/mp4"],
|
|
|
+ preview(file, url) {
|
|
|
+ return new Promise((resolve, reject) => {
|
|
|
+ const video = document.createElement("video");
|
|
|
+ video.preload = "metadata";
|
|
|
+ video.onloadedmetadata = () =>
|
|
|
+ resolve([video.videoWidth, video.videoHeight, file]);
|
|
|
+ video.onerror = reject;
|
|
|
+ video.src = url;
|
|
|
+ });
|
|
|
},
|
|
|
-}
|
|
|
-
|
|
|
-const producePreviews = files =>
|
|
|
- Promise.all(
|
|
|
- files.map(
|
|
|
- file =>
|
|
|
- new Promise((resolve, reject) => {
|
|
|
- const fr = new FileReader()
|
|
|
- fr.onloadend = e => resolve(e.target.result)
|
|
|
- fr.onerror = e => loaderror(file, reject(e))
|
|
|
- fr.readAsDataURL(file)
|
|
|
- })
|
|
|
- )
|
|
|
+ },
|
|
|
+};
|
|
|
+
|
|
|
+const producePreviews = (files) =>
|
|
|
+ Promise.all(
|
|
|
+ files.map(
|
|
|
+ (file) =>
|
|
|
+ new Promise((resolve, reject) => {
|
|
|
+ const fr = new FileReader();
|
|
|
+ fr.onloadend = (e) => resolve(e.target.result);
|
|
|
+ fr.onerror = (e) => loaderror(file, reject(e));
|
|
|
+ fr.readAsDataURL(file);
|
|
|
+ })
|
|
|
)
|
|
|
-
|
|
|
-const calcScale = (w, h) => parseInt((w / h) * 1000)
|
|
|
-
|
|
|
-const selectFileHandler = async ev => {
|
|
|
- const fileEl = ev.target
|
|
|
- const files = Array.from(fileEl.files)
|
|
|
- const previewError = (e, msg = `预览加载失败!`) => {
|
|
|
- console.error(e)
|
|
|
- Message.error(msg)
|
|
|
- fileEl.value = ''
|
|
|
- }
|
|
|
-
|
|
|
- if (props.accept) {
|
|
|
- for (const file of files) {
|
|
|
- const accepts = props.accept.split(',').map(atom => atom.trim())
|
|
|
- const hname = file.name.substr(file.name.lastIndexOf('.'))
|
|
|
- if (!accepts.includes(hname)) {
|
|
|
- return previewError('格式错误', `仅支持${props.accept}格式文件`)
|
|
|
- }
|
|
|
- }
|
|
|
+ );
|
|
|
+
|
|
|
+const calcScale = (w, h) => parseInt((w / h) * 1000);
|
|
|
+
|
|
|
+const selectFileHandler = async (ev) => {
|
|
|
+ const fileEl = ev.target;
|
|
|
+ const files = Array.from(fileEl.files);
|
|
|
+ const previewError = (e, msg = ui18n.t("sys.previewError")) => {
|
|
|
+ console.error(e);
|
|
|
+ Message.error(msg);
|
|
|
+ fileEl.value = "";
|
|
|
+ };
|
|
|
+
|
|
|
+ if (props.accept) {
|
|
|
+ for (const file of files) {
|
|
|
+ const accepts = props.accept.split(",").map((atom) => atom.trim());
|
|
|
+ const hname = file.name.substr(file.name.lastIndexOf("."));
|
|
|
+ if (!accepts.includes(hname)) {
|
|
|
+ return previewError(ui18n.t("sys.gsError"), ui18n.t("sys.gsAcceptTip", props));
|
|
|
+ }
|
|
|
}
|
|
|
-
|
|
|
- let previews
|
|
|
- if (props.preview || normalizeScale.value) {
|
|
|
- try {
|
|
|
- previews = await producePreviews(files)
|
|
|
- } catch (e) {
|
|
|
- return previewError(e)
|
|
|
- }
|
|
|
+ }
|
|
|
+
|
|
|
+ let previews;
|
|
|
+ if (props.preview || normalizeScale.value) {
|
|
|
+ try {
|
|
|
+ previews = await producePreviews(files);
|
|
|
+ } catch (e) {
|
|
|
+ return previewError(e);
|
|
|
}
|
|
|
-
|
|
|
- if (normalizeScale.value) {
|
|
|
- const sizesConfirm = []
|
|
|
- for (let i = 0; i < files.length; i++) {
|
|
|
- const support = Object.values(supports).find(support => support.types.includes(files[i].type))
|
|
|
- if (support) {
|
|
|
- sizesConfirm.push(support.preview(files[i], previews[i]))
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- let sizes
|
|
|
- try {
|
|
|
- sizes = await Promise.all(sizesConfirm)
|
|
|
- } catch (e) {
|
|
|
- return previewError(e)
|
|
|
- }
|
|
|
-
|
|
|
- for (const [w, h, file] of sizes) {
|
|
|
- const scaleDiff = calcScale(...normalizeScale.value) - calcScale(w, h)
|
|
|
-
|
|
|
- if (Math.abs(scaleDiff) > 300) {
|
|
|
- return previewError('error scale', `${file.name}的比例部位不为${props.scale}`)
|
|
|
- }
|
|
|
- }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (normalizeScale.value) {
|
|
|
+ const sizesConfirm = [];
|
|
|
+ for (let i = 0; i < files.length; i++) {
|
|
|
+ const support = Object.values(supports).find((support) =>
|
|
|
+ support.types.includes(files[i].type)
|
|
|
+ );
|
|
|
+ if (support) {
|
|
|
+ sizesConfirm.push(support.preview(files[i], previews[i]));
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
- if (props.maxSize) {
|
|
|
- for (const file of files) {
|
|
|
- if (file.size > props.maxSize) {
|
|
|
- return previewError('error size', `${file.name}的大小超过${sizeStr.value}`)
|
|
|
- }
|
|
|
- }
|
|
|
+ let sizes;
|
|
|
+ try {
|
|
|
+ sizes = await Promise.all(sizesConfirm);
|
|
|
+ } catch (e) {
|
|
|
+ return previewError(e);
|
|
|
}
|
|
|
|
|
|
- const value = props.modelValue ? (props.multiple ? (toRawType(props.modelValue) === 'Array' ? props.modelValue : [props.modelValue]) : null) : props.multiple ? [] : null
|
|
|
-
|
|
|
- const emitData = props.multiple
|
|
|
- ? props.preview
|
|
|
- ? [...value, ...files.map((file, i) => ({ file, preview: previews[i] }))]
|
|
|
- : [...value, files]
|
|
|
- : props.preview
|
|
|
- ? { file: files[0], preview: previews[0] }
|
|
|
- : files[0]
|
|
|
+ for (const [w, h, file] of sizes) {
|
|
|
+ const scaleDiff = calcScale(...normalizeScale.value) - calcScale(w, h);
|
|
|
|
|
|
- if (Array.isArray(emitData) && props.maxLen && emitData.length > props.maxLen) {
|
|
|
- return previewError('err len', `最多仅支持${props.maxLen}个文件!`)
|
|
|
+ if (Math.abs(scaleDiff) > 300) {
|
|
|
+ return previewError(
|
|
|
+ "error scale",
|
|
|
+ ui18n.t("sys.gsScaleTip", { ...props, name: file.name })
|
|
|
+ );
|
|
|
+ }
|
|
|
}
|
|
|
-
|
|
|
- emit('update:modelValue', emitData)
|
|
|
- fileEl.value = ''
|
|
|
-}
|
|
|
+ }
|
|
|
+
|
|
|
+ if (props.maxSize) {
|
|
|
+ for (const file of files) {
|
|
|
+ if (file.size > props.maxSize) {
|
|
|
+ return previewError(
|
|
|
+ "error size",
|
|
|
+ ui18n.t("sys.gsSizeTip", { sizeStr: sizeStr.value, name: file.name })
|
|
|
+ );
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ const value = props.modelValue
|
|
|
+ ? props.multiple
|
|
|
+ ? toRawType(props.modelValue) === "Array"
|
|
|
+ ? props.modelValue
|
|
|
+ : [props.modelValue]
|
|
|
+ : null
|
|
|
+ : props.multiple
|
|
|
+ ? []
|
|
|
+ : null;
|
|
|
+
|
|
|
+ const emitData = props.multiple
|
|
|
+ ? props.preview
|
|
|
+ ? [...value, ...files.map((file, i) => ({ file, preview: previews[i] }))]
|
|
|
+ : [...value, files]
|
|
|
+ : props.preview
|
|
|
+ ? { file: files[0], preview: previews[0] }
|
|
|
+ : files[0];
|
|
|
+
|
|
|
+ if (Array.isArray(emitData) && props.maxLen && emitData.length > props.maxLen) {
|
|
|
+ return previewError("error len", ui18n.t("sys.gsCountTip", props));
|
|
|
+ }
|
|
|
+
|
|
|
+ emit("update:modelValue", emitData);
|
|
|
+ fileEl.value = "";
|
|
|
+};
|
|
|
|
|
|
defineExpose({
|
|
|
- input: inputRef,
|
|
|
-})
|
|
|
+ input: inputRef,
|
|
|
+});
|
|
|
</script>
|