Merge branch 'develop' of https://gitee.com/layui-vue/layui-vue into develop

This commit is contained in:
就眠儀式 2022-04-03 15:56:22 +08:00
commit 5cf3eb5a00
8 changed files with 279 additions and 35 deletions

View File

@ -311,12 +311,12 @@ export default {
| zIndex | 自定义层级 | -- | | zIndex | 自定义层级 | -- |
| type | 类型 | `1: component` `2: iframe` | | type | 类型 | `1: component` `2: iframe` |
| closeBtn | 显示关闭 | true | | closeBtn | 显示关闭 | true |
| btn | 按钮 | | | btn | 按钮 格式:{text:"",callback:function(){}} | -- |
| btnAlign | 按钮布局 | `l` `r` `c` | | btnAlign | 按钮布局 | `l` `r` `c` |
| anim | 入场动画 | `0` `-` `6` | | anim | 入场动画 | `0` `-` `6` |
| isOutAnim | 关闭动画 | `true` `false` | | isOutAnim | 关闭动画 | `true` `false` |
| success | 显示回调 | -- | | success | 显示回调 | -- |
| end | 关闭回调 | -- | | close | 关闭回调 | -- |
::: :::

View File

@ -106,13 +106,13 @@ export default {
::: :::
::: title 自定义预览 ::: title 自定义预览/上传禁用
::: :::
::: demo 使用 `lay-upload` 标签, 使用 `#preview` 自定义预览的UI交互 ::: demo 使用 `lay-upload` 标签, 使用 `#preview` 自定义预览的UI交互,使用 `disabled` 添加上传禁用
<template> <template>
  <lay-upload @done="getUploadFile2">   <lay-upload @done="getUploadFile2" :disabled="true">
<template #preview> <template #preview>
<div class="easy-wrap"> <div class="easy-wrap">
<img src="https://chixian.oss-cn-hangzhou.aliyuncs.com/20211023003617_0706a.jpg" style="width:62.9px;height:63.2px"/> <img src="https://chixian.oss-cn-hangzhou.aliyuncs.com/20211023003617_0706a.jpg" style="width:62.9px;height:63.2px"/>
@ -141,6 +141,49 @@ export default {
::: :::
:::
::: title 提供默认剪裁功能
::: demo 使用 `lay-upload` 标签, 添加 `cut` 开启 选择文件后剪裁功能
<template>
  <lay-upload @cutdone="getCutDone" @cutcancel="getCutCancel" :cut="true" :multiple="false" @done="getFileDone">
<template #preview>
<div class="easy-wrap" v-if="cutUrl">
<img :src="cutUrl"/>
</div>
</template>
</lay-upload>
</template>
<script>
import { ref } from 'vue'
export default {
  setup() {
const cutUrl = ref("");
const getCutDone=(res)=>{
console.log("getCutDone",res);
cutUrl.value = res.msg;
};
const getCutCancel=(res)=>{
console.log("getCutCancel",res);
};
const getFileDone=(res)=>{
console.log("getFileDone",res);
};
    return {
getCutDone,
getCutCancel,
getFileDone,
cutUrl
    }
  }
}
</script>
:::
::: title Upload 属性 ::: title Upload 属性
::: :::
@ -157,6 +200,9 @@ export default {
| multiple | 是否允许多文件上传。设置 true即可开启。不支持ie8/9 | boolean | false | -- | | multiple | 是否允许多文件上传。设置 true即可开启。不支持ie8/9 | boolean | false | -- |
| number | 设置同时可上传的文件数量,一般配合 multiple 参数出现。 | number | `0(不限制)` | -- | | number | 设置同时可上传的文件数量,一般配合 multiple 参数出现。 | number | `0(不限制)` | -- |
| drag | 是否接受拖拽的文件上传,设置 false 可禁用。不支持ie8/9 | boolean | true | -- | | drag | 是否接受拖拽的文件上传,设置 false 可禁用。不支持ie8/9 | boolean | true | -- |
| disabled | 设置文件禁用 | boolean | false | -- |
| cut | 是否开启选择图片后检测,设置true可开启 | boolean | false | -- |
| cutOptions | 开启剪裁的模态弹窗与剪裁框的配置 | object | { layerOption,copperOption } | -- |
::: :::
@ -182,7 +228,8 @@ export default {
| before | 上传事务开启前的回调 | -- | | before | 上传事务开启前的回调 | -- |
| done | 上传事务结束的回调 | -- | | done | 上传事务结束的回调 | -- |
| error | 上传事务中出现错误的回调 | -- | | error | 上传事务中出现错误的回调 | -- |
| cutdown | 剪裁完成 | -- |
| cutclose | 剪裁取消 | -- |
::: :::

View File

@ -43,6 +43,7 @@
"@layui/layer-vue": "^1.3.11", "@layui/layer-vue": "^1.3.11",
"@vueuse/core": "^7.6.2", "@vueuse/core": "^7.6.2",
"async-validator": "^4.0.7", "async-validator": "^4.0.7",
"cropperjs": "^1.5.12",
"darkreader": "^4.9.46", "darkreader": "^4.9.46",
"evtd": "^0.2.3", "evtd": "^0.2.3",
"moment": "^2.29.1", "moment": "^2.29.1",

View File

@ -7,7 +7,7 @@ export default {
<script setup lang="ts"> <script setup lang="ts">
import "./index.less"; import "./index.less";
import { withDefaults } from "vue"; import { withDefaults } from "vue";
import { String } from "src/types"; import { String } from "../../types";
export interface LayEmptyProps { export interface LayEmptyProps {
description?: String; description?: String;

View File

@ -8,7 +8,7 @@ export default {
import "./index.less"; import "./index.less";
import { useSlots } from "vue"; import { useSlots } from "vue";
import { useI18n } from "vue-i18n"; import { useI18n } from "vue-i18n";
import { Boolean, String } from "src/types"; import { Boolean, String } from "../../types";
const { t } = useI18n(); const { t } = useI18n();
const slots = useSlots(); const slots = useSlots();

9
src/component/upload/cropper.min.css vendored Normal file
View File

@ -0,0 +1,9 @@
/*!
* Cropper.js v1.5.12
* https://fengyuanchen.github.io/cropperjs
*
* Copyright 2015-present Chen Fengyuan
* Released under the MIT license
*
* Date: 2021-06-12T08:00:11.623Z
*/.cropper-container{direction:ltr;font-size:0;line-height:0;position:relative;-ms-touch-action:none;touch-action:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.cropper-container img{image-orientation:0deg;display:block;height:100%;max-height:none!important;max-width:none!important;min-height:0!important;min-width:0!important;width:100%}.cropper-canvas,.cropper-crop-box,.cropper-drag-box,.cropper-modal,.cropper-wrap-box{bottom:0;left:0;position:absolute;right:0;top:0}.cropper-canvas,.cropper-wrap-box{overflow:hidden}.cropper-drag-box{background-color:#fff;opacity:0}.cropper-modal{background-color:#000;opacity:.5}.cropper-view-box{display:block;height:100%;outline:1px solid #39f;outline-color:rgba(51,153,255,.75);overflow:hidden;width:100%}.cropper-dashed{border:0 dashed #eee;display:block;opacity:.5;position:absolute}.cropper-dashed.dashed-h{border-bottom-width:1px;border-top-width:1px;height:33.33333%;left:0;top:33.33333%;width:100%}.cropper-dashed.dashed-v{border-left-width:1px;border-right-width:1px;height:100%;left:33.33333%;top:0;width:33.33333%}.cropper-center{display:block;height:0;left:50%;opacity:.75;position:absolute;top:50%;width:0}.cropper-center:after,.cropper-center:before{background-color:#eee;content:" ";display:block;position:absolute}.cropper-center:before{height:1px;left:-3px;top:0;width:7px}.cropper-center:after{height:7px;left:0;top:-3px;width:1px}.cropper-face,.cropper-line,.cropper-point{display:block;height:100%;opacity:.1;position:absolute;width:100%}.cropper-face{background-color:#fff;left:0;top:0}.cropper-line{background-color:#39f}.cropper-line.line-e{cursor:ew-resize;right:-3px;top:0;width:5px}.cropper-line.line-n{cursor:ns-resize;height:5px;left:0;top:-3px}.cropper-line.line-w{cursor:ew-resize;left:-3px;top:0;width:5px}.cropper-line.line-s{bottom:-3px;cursor:ns-resize;height:5px;left:0}.cropper-point{background-color:#39f;height:5px;opacity:.75;width:5px}.cropper-point.point-e{cursor:ew-resize;margin-top:-3px;right:-3px;top:50%}.cropper-point.point-n{cursor:ns-resize;left:50%;margin-left:-3px;top:-3px}.cropper-point.point-w{cursor:ew-resize;left:-3px;margin-top:-3px;top:50%}.cropper-point.point-s{bottom:-3px;cursor:s-resize;left:50%;margin-left:-3px}.cropper-point.point-ne{cursor:nesw-resize;right:-3px;top:-3px}.cropper-point.point-nw{cursor:nwse-resize;left:-3px;top:-3px}.cropper-point.point-sw{bottom:-3px;cursor:nesw-resize;left:-3px}.cropper-point.point-se{bottom:-3px;cursor:nwse-resize;height:20px;opacity:1;right:-3px;width:20px}@media (min-width:768px){.cropper-point.point-se{height:15px;width:15px}}@media (min-width:992px){.cropper-point.point-se{height:10px;width:10px}}@media (min-width:1200px){.cropper-point.point-se{height:5px;opacity:.75;width:5px}}.cropper-point.point-se:before{background-color:#39f;bottom:-50%;content:" ";display:block;height:200%;opacity:0;position:absolute;right:-50%;width:200%}.cropper-invisible{opacity:0}.cropper-bg{background-image:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAQMAAAAlPW0iAAAAA3NCSVQICAjb4U/gAAAABlBMVEXMzMz////TjRV2AAAACXBIWXMAAArrAAAK6wGCiw1aAAAAHHRFWHRTb2Z0d2FyZQBBZG9iZSBGaXJld29ya3MgQ1M26LyyjAAAABFJREFUCJlj+M/AgBVhF/0PAH6/D/HkDxOGAAAAAElFTkSuQmCC")}.cropper-hide{display:block;height:0;position:absolute;width:0}.cropper-hidden{display:none!important}.cropper-move{cursor:move}.cropper-crop{cursor:crosshair}.cropper-disabled .cropper-drag-box,.cropper-disabled .cropper-face,.cropper-disabled .cropper-line,.cropper-disabled .cropper-point{cursor:not-allowed}

View File

@ -1,5 +1,5 @@
@import (reference) "../../theme/variable.less"; @import (reference) "../../theme/variable.less";
@import "./cropper.min.css";
.layui-upload-file { .layui-upload-file {
// display: none !important; // display: none !important;
opacity: 0.01; opacity: 0.01;
@ -85,3 +85,17 @@
.layui-btn-container .layui-upload-choose { .layui-btn-container .layui-upload-choose {
padding-left: 0; padding-left: 0;
} }
.layui-upload-drag-disable{
opacity:0.8;
z-index:1;
cursor: not-allowed;
}
.copper-container{
// width:1000px;
}
._lay_upload_img{
display: block;
max-width: 100%;
}

View File

@ -7,11 +7,49 @@ export default {
import "./index.less"; import "./index.less";
import { Recordable } from "../../types"; import { Recordable } from "../../types";
import { layer } from "@layui/layer-vue"; import { layer } from "@layui/layer-vue";
import { ref, useSlots, withDefaults } from "vue"; import {
computed,
ComputedRef,
getCurrentInstance,
nextTick,
ref,
toRaw,
useSlots,
withDefaults,
} from "vue";
import { templateRef } from "@vueuse/core"; import { templateRef } from "@vueuse/core";
import { LayLayer } from "@layui/layer-vue";
import Cropper from "cropperjs";
// //
//https://www.layuiweb.com/doc/modules/upload.html#options //https://www.layuiweb.com/doc/modules/upload.html#options
export interface LayerButton {
text: string;
callback: Function;
}
export interface LayerModal {
title?: string;
resize?: boolean;
move?: boolean;
maxmin?: boolean;
offset?: string[];
content?: string;
shade?: boolean;
shadeClose?: boolean;
shadeOpacity?: number;
zIndex?: number;
type?: "component" | "iframe";
closeBtn?: boolean;
area: string[];
btn?: LayerButton[];
btnAlign?: "l" | "r" | "c";
anim?: boolean;
isOutAnim?: boolean;
}
export interface cutOptions {
layerOption: LayerModal;
copperOption?: typeof Cropper;
}
export interface LayUploadProps { export interface LayUploadProps {
url?: string; url?: string;
data?: any; data?: any;
@ -22,8 +60,50 @@ export interface LayUploadProps {
multiple?: boolean; multiple?: boolean;
number?: number; number?: number;
drag?: boolean; drag?: boolean;
disabled?: boolean;
cut?: boolean;
cutOptions: cutOptions;
} }
const getCutDownResult = () => {
if (_cropper) {
const canvas = _cropper.getCroppedCanvas();
let imgData = canvas.toDataURL('"image/png"');
let currentTimeStamp = new Date().valueOf();
emit("cutdone", Object.assign({ currentTimeStamp, msg: imgData }));
let newFile = dataURLtoFile(imgData);
commonUploadTransaction([newFile]);
nextTick(() => clearAllCutEffect());
} else {
errorF(cutInitErrorMsg);
}
};
const closeCutDownModal = () => {
let currentTimeStamp = new Date().valueOf();
emit("cutcancel", Object.assign({ currentTimeStamp }));
nextTick(() => clearAllCutEffect());
};
const clearAllCutEffect = () => {
activeUploadFiles.value = [];
activeUploadFilesImgs.value = [];
innerCutVisible.value = false;
};
let defaultCutLayerOption: LayerModal = {
title: "标题",
move: true,
maxmin: false,
offset: [],
btn: [
{ text: "导出", callback: getCutDownResult },
{ text: "取消", callback: closeCutDownModal },
],
area: ["640px", "640px"],
content: "11",
shade: true,
shadeClose: true,
type: "component",
};
const props = withDefaults(defineProps<LayUploadProps>(), { const props = withDefaults(defineProps<LayUploadProps>(), {
acceptMime: "images", acceptMime: "images",
field: "file", field: "file",
@ -31,22 +111,48 @@ const props = withDefaults(defineProps<LayUploadProps>(), {
multiple: false, multiple: false,
number: 0, number: 0,
drag: false, drag: false,
disabled: false,
cut: false,
cutOptions: void 0,
}); });
const slot = useSlots(); const slot = useSlots();
const slots = slot.default && slot.default(); const slots = slot.default && slot.default();
const emit = defineEmits(["choose", "before", "done", "error"]); const context = getCurrentInstance();
const emit = defineEmits([
"choose",
"before",
"done",
"error",
"cutdone",
"cutcancel",
]);
// //
const isDragEnter = ref(false); const isDragEnter = ref(false);
//
const activeUploadFiles = ref<any[]>([]);
//
const activeUploadFilesImgs = ref<any[]>([]);
const orgFileInput = templateRef<HTMLElement>("orgFileInput"); const orgFileInput = templateRef<HTMLElement>("orgFileInput");
let _cropper: any = null;
let computedCutLayerOption: ComputedRef<LayerModal>;
if (props.cutOptions && props.cutOptions.layerOption) {
computedCutLayerOption = computed(() =>
Object.assign(defaultCutLayerOption, props.cutOptions.layerOption)
);
} else {
computedCutLayerOption = computed(() => defaultCutLayerOption);
}
// //
const defaultErrorMsg = "上传失败"; const defaultErrorMsg = "上传失败";
const urlErrorMsg = "上传地址格式不合法"; const urlErrorMsg = "上传地址格式不合法";
const numberErrorMsg = "文件上传超过规定的个数"; const numberErrorMsg = "文件上传超过规定的个数";
const sizeErrorMsg = "文件大小超过限制"; const sizeErrorMsg = "文件大小超过限制";
const uploadRemoteErrorMsg = "请求上传接口出现异常"; const uploadRemoteErrorMsg = "请求上传接口出现异常";
const cutInitErrorMsg = "剪裁插件初始化失败";
//
const uploadSuccess = "上传成功"; const uploadSuccess = "上传成功";
// -> start // -> start
@ -56,6 +162,8 @@ interface localUploadTransaction {
files: File[] | Blob[]; files: File[] | Blob[];
[propMame: string]: any; [propMame: string]: any;
} }
const innerCutVisible = ref<boolean>(false);
const localUploadTransaction = (option: localUploadTransaction) => { const localUploadTransaction = (option: localUploadTransaction) => {
const { url, files } = option; const { url, files } = option;
let formData = new FormData(); let formData = new FormData();
@ -88,6 +196,20 @@ interface localUploadOption {
url: string; url: string;
[propMame: string]: any; [propMame: string]: any;
} }
const dataURLtoFile = (dataurl: string) => {
let arr: any[] = dataurl.split(",");
let mime: string = "";
if (arr.length > 0) {
mime = arr[0].match(/:(.*?);/)[1];
}
let bstr = atob(arr[1]);
let n = bstr.length;
let u8arr = new Uint8Array(n);
while (n--) {
u8arr[n] = bstr.charCodeAt(n);
}
return new Blob([u8arr], { type: mime });
};
const errorF = (errorText: string) => { const errorF = (errorText: string) => {
let currentTimeStamp = new Date().valueOf(); let currentTimeStamp = new Date().valueOf();
@ -139,7 +261,13 @@ const localUpload = (option: localUploadOption, callback: Function) => {
cb(); cb();
} }
}; };
const filetoDataURL = (file: File, fn: Function) => {
const reader = new FileReader();
reader.onloadend = function (e: any) {
fn(e.target.result);
};
reader.readAsDataURL(file);
};
const getUploadChange = (e: any) => { const getUploadChange = (e: any) => {
const files = e.target.files; const files = e.target.files;
const _files = [...files]; const _files = [...files];
@ -165,17 +293,43 @@ const getUploadChange = (e: any) => {
_sizeErrorFile.name _sizeErrorFile.name
} ${sizeErrorMsg},文件最大不可超过${props.size * 1000}kb`; } ${sizeErrorMsg},文件最大不可超过${props.size * 1000}kb`;
errorF(errorMsg); errorF(errorMsg);
return;
} }
} }
} }
for (let item of _files) {
activeUploadFiles.value.push(item);
filetoDataURL(item, function (res: any) {
activeUploadFilesImgs.value.push(res);
});
}
let arm1 = props.cut && props.acceptMime == "images" && !props.multiple;
let arm2 = props.cut && props.acceptMime == "images" && props.multiple;
if (arm1) {
innerCutVisible.value = true;
setTimeout(() => {
let _imgs = document.getElementsByClassName("_lay_upload_img");
let _img = _imgs[0];
_cropper = new Cropper(_img, {
aspectRatio: 16 / 9,
});
}, 400);
} else {
if (arm2) {
console.warn(
"layui-vue:当前版本暂不支持单次多文件剪裁,尝试设置 multiple 为false,通过@done获取返回文件对象"
);
}
commonUploadTransaction(_files);
}
};
const commonUploadTransaction = (_files: any[]) => {
if (props.url) { if (props.url) {
//
localUploadTransaction({ localUploadTransaction({
url: props.url, url: props.url,
files: _files, files: _files,
}); });
} else { } else {
//
emit("done", _files); emit("done", _files);
} }
}; };
@ -188,21 +342,9 @@ const chooseFile = () => {
}; };
const clickOrgInput = () => { const clickOrgInput = () => {
let currentTimeStamp = new Date().valueOf(); let currentTimeStamp = new Date().valueOf();
//console.log(currentTimeStamp);
emit("choose", currentTimeStamp); emit("choose", currentTimeStamp);
}; };
const uploadDragOver = (e: any) => {}; const cutTransaction = () => {};
const uploadDragDrop = (e: any) => {
isDragEnter.value = false;
console.log(e);
};
const uploadDragStop = (e: any) => {};
const uploadDragEnter = (e: any) => {
isDragEnter.value = true;
};
const uploadDragLeave = (e: any) => {
isDragEnter.value = false;
};
// -> end // -> end
</script> </script>
<template> <template>
@ -216,11 +358,12 @@ const uploadDragLeave = (e: any) => {
:name="field" :name="field"
@change="getUploadChange" @change="getUploadChange"
:field="field" :field="field"
:disabled="disabled"
ref="orgFileInput" ref="orgFileInput"
/> />
<div v-if="!drag"> <div v-if="!drag">
<div class="layui-upload-btn-box"> <div class="layui-upload-btn-box">
<lay-button type="primary" @click.stop="chooseFile" <lay-button type="primary" @click.stop="chooseFile" :disabled="disabled"
>上传图片</lay-button >上传图片</lay-button
> >
</div> </div>
@ -228,11 +371,13 @@ const uploadDragLeave = (e: any) => {
<div <div
v-else v-else
class="layui-upload-drag" class="layui-upload-drag"
:class="isDragEnter ? 'layui-upload-drag-draging' : ''" :class="
@dragleave.stop="uploadDragLeave" disabled
@dragenter.stop="uploadDragEnter" ? 'layui-upload-drag-disable'
@dragover.stop="uploadDragOver" : isDragEnter
@drop="uploadDragDrop" ? 'layui-upload-drag-draging'
: ''
"
@click.stop="chooseFile" @click.stop="chooseFile"
> >
<i class="layui-icon"></i> <i class="layui-icon"></i>
@ -242,6 +387,34 @@ const uploadDragLeave = (e: any) => {
<img src="" alt="上传成功后渲染" style="max-width: 196px" /> <img src="" alt="上传成功后渲染" style="max-width: 196px" />
</div> </div>
</div> </div>
<lay-layer
:title="computedCutLayerOption.title"
:move="computedCutLayerOption.move"
:resize="computedCutLayerOption.resize"
:shade="computedCutLayerOption.shade"
:shadeClose="computedCutLayerOption.shadeClose"
:shadeOpacity="computedCutLayerOption.shadeOpacity"
:zIndex="computedCutLayerOption.zIndex"
:btnAlign="computedCutLayerOption.btnAlign"
:area="computedCutLayerOption.area"
:anim="computedCutLayerOption.anim"
:isOutAnim="computedCutLayerOption.isOutAnim"
:btn="computedCutLayerOption.btn"
v-model="innerCutVisible"
@close="clearAllCutEffect"
>
<div
class="copper-container"
v-for="(base64str, index) in activeUploadFilesImgs"
:key="`file${index}`"
>
<img
:src="base64str"
:id="`_lay_upload_img${index}`"
class="_lay_upload_img"
/>
</div>
</lay-layer>
<div class="layui-upload-list"> <div class="layui-upload-list">
<slot name="preview"></slot> <slot name="preview"></slot>
</div> </div>