song-ui-u
Version:
vue3 + js的PC前端组件库
597 lines (541 loc) • 18.9 kB
JavaScript
import { mergeModels, ref, computed, useModel, resolveComponent, openBlock, createElementBlock, normalizeClass, createElementVNode, createBlock, createCommentVNode, normalizeStyle, createVNode, withCtx, createTextVNode, toDisplayString, Fragment, renderList, withModifiers, watch } from 'vue';
import * as SparkMD5 from 'spark-md5';
import '../../button/index.mjs';
import '../../buttonGroup/index.mjs';
import { XIcon } from '../../icon/index.mjs';
import '../../input/index.mjs';
import '../../textarea/index.mjs';
import '../../row/index.mjs';
import '../../col/index.mjs';
import '../../container/index.mjs';
import '../../checkbox/index.mjs';
import '../../switch/index.mjs';
import '../../form/index.mjs';
import { XMessage } from '../../message/index.mjs';
import '../../mask/src/mask.mjs';
import '../../modal/index.mjs';
import '../../messageBox/index.mjs';
import '../../drawer/index.mjs';
import '../../badge/index.mjs';
import '../../space/index.mjs';
import { XImage } from '../../image/index.mjs';
import '../../radio/index.mjs';
import '../../divider/index.mjs';
import '../../chat/index.mjs';
import { XProgress } from '../../progress/index.mjs';
import '../index.mjs';
import '../../vTree/index.mjs';
import '../../table/index.mjs';
import '../../tabs/index.mjs';
import '../../menu/index.mjs';
import '../../steps/index.mjs';
import '../../header/index.mjs';
import '../../breadcrumble/index.mjs';
import '../../datePicker/index.mjs';
import '../../tooltip/index.mjs';
import '../../popover/index.mjs';
import '../../timePicker/index.mjs';
import '../../select/index.mjs';
import '../../collapse/index.mjs';
import '../../card/index.mjs';
import '../../timeline/index.mjs';
import '../../tag/index.mjs';
import '../../result/index.mjs';
import '../../sender/index.mjs';
import { useNamespace } from '../../../hook/use-namespace/index.mjs';
import { Upload, Pause, Plus, X } from 'song-ui-pro-icon';
import '../../../hook/use-zindex/index.mjs';
import _export_sfc from '../../../_virtual/_plugin-vue_export-helper.mjs';
const _sfc_main = /*#__PURE__*/Object.assign({
name: "x-upload",
}, {
__name: 'index',
props: /*#__PURE__*/mergeModels({
// 切片大小
chunkSize: {
type: Number,
default: 1,
},
// 文件验证地址
verifyUrl: {
type: String,
default: "",
},
// 上传地址
uploadUrl: {
type: String,
default: "",
},
// 合并地址
mergeUrl: {
type: String,
default: "",
},
showProgress: {
type: Boolean,
default: true,
},
formData: {
type: Object,
default: () => ({}),
},
headers: {
type: Object,
default: () => ({}),
},
// 手动上传
manualUpload: {
type: Boolean,
default: false,
},
// type
type: {
type: String,
default: "normal", // image, imageList
},
imageSize: {
type: String,
default: "130",
},
// 是否开启秒传
isVerify: {
type: Boolean,
default: true,
},
}, {
"imageList": {
type: Array,
default: () => [],
},
"imageListModifiers": {},
"imgUrl": {
type: String,
default: "",
},
"imgUrlModifiers": {},
"fileList": {
type: Array,
default: () => [],
},
"fileListModifiers": {},
"fileUrl": {
type: String,
default: "",
},
"fileUrlModifiers": {},
}),
emits: /*#__PURE__*/mergeModels([
"success",
"error",
"beforeUpload",
"afterUpload",
"changeFile",
// 暂停
"pause",
// 继续
"resume",
// delete图片
"delete",
], ["update:imageList", "update:imgUrl", "update:fileList", "update:fileUrl"]),
setup(__props, { expose: __expose, emit: __emit }) {
const props = __props;
const ns = useNamespace("upload");
const file = ref(null);
const chunkSize = computed(() => {
return props.chunkSize * 1024 * 1024;
});
const imageList = useModel(__props, "imageList");
const imgUrl = useModel(__props, "imgUrl");
const fileList = useModel(__props, "fileList");
const fileUrl = useModel(__props, "fileUrl");
const uploadedChunks = ref([]); // 已上传的分片索引
const isPaused = ref(false);
const uploadProgress = ref(0);
const fileHash = ref(""); // 文件的哈希值(唯一标识)
let currentChunkIndex = 0;
const emits = __emit;
// 验证文件是否已存在(秒传)
const verifyUpload = async (fileHash, fileName) => {
if (!props.isVerify) {
return { exists: false };
}
try {
const response = await fetch(props.verifyUrl, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
fileHash,
fileName,
}),
});
const result = await response.json();
return result;
} catch (error) {
XMessage({
content: "验证失败",
type: "warning",
});
return { exists: false };
}
};
// 处理文件选择
const handleFileChange = async (event, files) => {
file.value = files || event.target.files[0];
uploadedChunks.value = [];
currentChunkIndex = 0;
uploadProgress.value = 0;
// 计算文件哈希值
fileHash.value = await calculateFileHash(file.value);
// 触发 changeFile 事件
emits("changeFile", { file: file.value, fileHash: fileHash.value });
// 验证文件是否已存在
const { exists } = await verifyUpload(fileHash.value, file.value.name);
if (exists) {
XMessage({
content: "文件已存在",
type: "success",
});
uploadProgress.value = 100;
return;
}
if (!props.manualUpload) {
startUpload();
}
};
// 计算文件哈希值
const calculateFileHash = (file) => {
return new Promise((resolve, reject) => {
const spark = new SparkMD5.ArrayBuffer();
const reader = new FileReader();
const fileChunkSize = 1 * 1024 * 1024;
let currentChunk = 0;
const chunks = Math.ceil(file.size / fileChunkSize);
const loadNextChunk = () => {
const start = currentChunk * fileChunkSize;
const end = Math.min(start + fileChunkSize, file.size);
reader.readAsArrayBuffer(file.slice(start, end));
};
reader.onload = (e) => {
spark.append(e.target.result);
currentChunk++;
if (currentChunk < chunks) {
loadNextChunk();
} else {
resolve(spark.end());
}
};
reader.onerror = () => reject("文件哈希值计算失败");
loadNextChunk();
});
};
// 开始上传
const startUpload = async () => {
emits("beforeUpload");
if (!file.value) {
XMessage({
content: "请选择文件",
type: "warning",
});
return;
}
isPaused.value = false;
await uploadFileInChunks();
};
// 暂停上传
const pauseUpload = () => {
emits("pause");
isPaused.value = true;
};
// 继续上传
const resumeUpload = async () => {
isPaused.value = false;
emits("resume");
await uploadFileInChunks();
};
// 分片上传逻辑
const uploadFileInChunks = async () => {
while (currentChunkIndex < Math.ceil(file.value.size / chunkSize.value)) {
if (isPaused.value) {
return;
}
if (uploadedChunks.value.includes(currentChunkIndex)) {
currentChunkIndex++;
continue;
}
const chunk = file.value.slice(
currentChunkIndex * chunkSize.value,
(currentChunkIndex + 1) * chunkSize.value
);
const formData = new FormData();
formData.append("file", chunk);
formData.append("fileName", file.value.name);
formData.append("fileHash", fileHash.value);
formData.append("chunkIndex", currentChunkIndex);
formData.append(
"totalChunks",
Math.ceil(file.value.size / chunkSize.value)
);
props.formData &&
Object.keys(props.formData).forEach((key) => {
formData.append(key, props.formData[key]);
});
try {
await fetch(props.uploadUrl, {
method: "POST",
body: formData,
headers: props.headers,
});
uploadedChunks.value.push(currentChunkIndex);
currentChunkIndex++;
uploadProgress.value = (
(uploadedChunks.value.length /
Math.ceil(file.value.size / chunkSize.value)) *
100
).toFixed(2);
} catch (error) {
console.error("上传失败:", error);
emits("error", error); // 添加错误事件
break;
}
}
if (
uploadedChunks.value.length === Math.ceil(file.value.size / chunkSize.value)
) {
XMessage({
content: "上传完成",
type: "success",
});
await mergeChunks();
emits("afterUpload"); // 添加上传完成事件
}
};
// 合并分片
const mergeChunks = async () => {
try {
const response = await fetch(props.mergeUrl, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
fileName: file.value.name,
fileHash: fileHash.value,
totalChunks: Math.ceil(file.value.size / chunkSize.value),
}),
});
const result = await response.json();
emits("success", result); // 添加成功事件
} catch (error) {
console.error("文件合并失败:", error);
emits("error", error); // 添加错误事件
}
};
// const preImageList = computed(() => {
// return imageList.value.map((item) => {
// return item.url;
// });
// });
// handleDelImg
const handleDelImg = (index) => {
imageList.value.splice(index, 1);
emits("delete", index);
};
const upload = (files) => {
return handleFileChange(null, files);
};
__expose({
upload,
});
const __returned__ = { props, ns, file, chunkSize, imageList, imgUrl, fileList, fileUrl, uploadedChunks, isPaused, uploadProgress, fileHash, get currentChunkIndex() { return currentChunkIndex }, set currentChunkIndex(v) { currentChunkIndex = v; }, emits, verifyUpload, handleFileChange, calculateFileHash, startUpload, pauseUpload, resumeUpload, uploadFileInChunks, mergeChunks, handleDelImg, upload, ref, computed, watch, get SparkMD5() { return SparkMD5 }, get XProgress() { return XProgress }, get XMessage() { return XMessage }, get XIcon() { return XIcon }, get XImage() { return XImage }, get useNamespace() { return useNamespace }, get Upload() { return Upload }, get Pause() { return Pause }, get Plus() { return Plus }, get X() { return X } };
Object.defineProperty(__returned__, '__isScriptSetup', { enumerable: false, value: true });
return __returned__
}
});
const _hoisted_1 = ["accept"];
const _hoisted_2 = /*#__PURE__*/createElementVNode("svg", {
t: "1744352467807",
class: "icon",
viewBox: "0 0 1024 1024",
version: "1.1",
xmlns: "http://www.w3.org/2000/svg",
"p-id": "3031",
width: "200",
height: "200"
}, [
/*#__PURE__*/createElementVNode("path", {
d: "M899.117 526.883a30.04 30.04 0 0 1-3.976 5.365 29.922 29.922 0 0 1-1.548 1.58l-0.2 0.186a30.087 30.087 0 0 1-5.472 4.057L165.858 955.094c-0.047 0.027-0.1 0.046-0.143 0.073q-1.155 0.659-2.37 1.214c-0.145 0.066-0.289 0.132-0.434 0.2q-1.084 0.476-2.212 0.864c-0.238 0.083-0.475 0.165-0.713 0.241-0.645 0.205-1.3 0.386-1.963 0.548-0.348 0.085-0.7 0.168-1.044 0.24-0.56 0.116-1.126 0.213-1.7 0.3q-1.416 0.21-2.833 0.283h-0.016a29.977 29.977 0 0 1-4.5-0.107c-0.243-0.024-0.483-0.057-0.723-0.086a30.513 30.513 0 0 1-2.109-0.331 31.377 31.377 0 0 1-1.021-0.222c-0.6-0.139-1.191-0.293-1.78-0.469-0.331-0.1-0.658-0.2-0.984-0.315q-0.921-0.309-1.82-0.676c-0.241-0.1-0.479-0.2-0.716-0.307q-1.047-0.461-2.055-1c-0.108-0.058-0.215-0.118-0.322-0.177a29.753 29.753 0 0 1-8.652-7.174l-0.071-0.089a29.873 29.873 0 0 1-1.59-2.124c-0.3-0.439-0.589-0.885-0.866-1.34-0.108-0.179-0.233-0.341-0.338-0.524a29.859 29.859 0 0 1-4-16V95.886a30 30 0 0 1 44.981-26.984l722.265 417a30 30 0 0 1 10.988 40.981zM180.89 877.13l632.327-365.2L180.89 146.856V877.13z",
fill: "#ffffff",
"p-id": "3032"
})
], -1 /* HOISTED */);
const _hoisted_3 = ["onClick"];
function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
const _component_x_button = resolveComponent("x-button");
return (openBlock(), createElementBlock("div", {
class: normalizeClass([$setup.ns.b(), $setup.ns.is('image', $props.type != 'normal')])
}, [
createElementVNode("div", {
class: normalizeClass([$setup.ns.e('wrapper')])
}, [
($props.type !== 'normal')
? (openBlock(), createElementBlock("div", {
key: 0,
class: normalizeClass($setup.ns.e('image'))
}, [
($setup.imgUrl && $props.type == 'image')
? (openBlock(), createBlock($setup["XImage"], {
key: 0,
src: $setup.imgUrl,
preview: true
}, null, 8 /* PROPS */, ["src"]))
: createCommentVNode("v-if", true),
createElementVNode("div", {
class: normalizeClass($setup.ns.e('upload-image')),
style: normalizeStyle({
height: $props.imageSize + 'px',
width: $props.imageSize + 'px',
})
}, [
createVNode($setup["XIcon"], { size: "50" }, {
default: withCtx(() => [
createVNode($setup["Plus"])
]),
_: 1 /* STABLE */
})
], 6 /* CLASS, STYLE */)
], 2 /* CLASS */))
: createCommentVNode("v-if", true),
($props.type == 'normal')
? (openBlock(), createBlock(_component_x_button, {
key: 1,
icon: $setup.Upload,
type: "primary"
}, {
default: withCtx(() => [
createTextVNode("选择文件")
]),
_: 1 /* STABLE */
}, 8 /* PROPS */, ["icon"]))
: createCommentVNode("v-if", true),
createElementVNode("input", {
type: "file",
accept: $props.type != 'normal' ? 'image/*' : '',
onChange: $setup.handleFileChange,
class: normalizeClass($setup.ns.e('input'))
}, null, 42 /* CLASS, PROPS, NEED_HYDRATION */, _hoisted_1),
($props.type == 'normal')
? (openBlock(), createElementBlock("span", {
key: 2,
class: normalizeClass($setup.ns.e('warp-text'))
}, toDisplayString($setup.file?.name), 3 /* TEXT, CLASS */))
: createCommentVNode("v-if", true)
], 2 /* CLASS */),
createElementVNode("div", {
class: normalizeClass($setup.ns.e('control'))
}, [
($setup.file && $props.type == 'normal')
? (openBlock(), createElementBlock("div", {
key: 0,
class: normalizeClass($setup.ns.e('button'))
}, [
($props.manualUpload)
? (openBlock(), createBlock(_component_x_button, {
key: 0,
size: "small",
circle: "",
type: "success",
onClick: $setup.startUpload,
icon: $setup.Upload
}, null, 8 /* PROPS */, ["icon"]))
: createCommentVNode("v-if", true),
(!$setup.isPaused)
? (openBlock(), createBlock(_component_x_button, {
key: 1,
size: "small",
circle: "",
type: "warning",
onClick: $setup.pauseUpload
}, {
default: withCtx(() => [
createVNode($setup["XIcon"], null, {
default: withCtx(() => [
createVNode($setup["Pause"])
]),
_: 1 /* STABLE */
})
]),
_: 1 /* STABLE */
}))
: (openBlock(), createBlock(_component_x_button, {
key: 2,
size: "small",
onClick: $setup.resumeUpload,
circle: "",
type: "success"
}, {
default: withCtx(() => [
createVNode($setup["XIcon"], null, {
default: withCtx(() => [
_hoisted_2
]),
_: 1 /* STABLE */
})
]),
_: 1 /* STABLE */
}))
], 2 /* CLASS */))
: createCommentVNode("v-if", true),
($setup.imageList.length > 0 && $props.type == 'imageList')
? (openBlock(), createElementBlock("div", {
key: 1,
class: normalizeClass($setup.ns.e('imageList'))
}, [
(openBlock(true), createElementBlock(Fragment, null, renderList($setup.imageList, (item, index) => {
return (openBlock(), createElementBlock("div", {
class: normalizeClass($setup.ns.e('img'))
}, [
createElementVNode("div", {
class: normalizeClass($setup.ns.e('del-img')),
onClick: withModifiers($event => ($setup.handleDelImg(index)), ["stop"])
}, [
createVNode($setup["XIcon"], null, {
default: withCtx(() => [
createVNode($setup["X"])
]),
_: 1 /* STABLE */
})
], 10 /* CLASS, PROPS */, _hoisted_3),
(openBlock(), createBlock($setup["XImage"], {
fit: "cover",
key: item.name,
src: item.url,
preview: true,
width: $setup.props.imageSize,
height: $setup.props.imageSize
}, null, 8 /* PROPS */, ["src", "width", "height"]))
], 2 /* CLASS */))
}), 256 /* UNKEYED_FRAGMENT */))
], 2 /* CLASS */))
: createCommentVNode("v-if", true),
($props.showProgress && $props.type == 'normal')
? (openBlock(), createElementBlock("div", {
key: 2,
class: normalizeClass($setup.ns.e('progress'))
}, [
($props.showProgress)
? (openBlock(), createBlock($setup["XProgress"], {
key: 0,
value: $setup.uploadProgress,
height: "10"
}, null, 8 /* PROPS */, ["value"]))
: createCommentVNode("v-if", true)
], 2 /* CLASS */))
: createCommentVNode("v-if", true)
], 2 /* CLASS */)
], 2 /* CLASS */))
}
var upload = /*#__PURE__*/_export_sfc(_sfc_main, [['render',_sfc_render],['__file',"E:\\code\\my-code\\song-ui-ultra\\packages\\components\\upload\\src\\index.vue"]]);
export { upload as default };
//# sourceMappingURL=index.vue.mjs.map