This commit is contained in:
2022-11-15 08:28:03 +08:00
commit c892ebfd44
37 changed files with 5495 additions and 0 deletions

View File

@@ -0,0 +1,29 @@
<script lang="ts">
export default {
name: "CloseBtn",
};
</script>
<script lang="ts" setup>
import { computed, defineEmits } from "vue";
export interface CloseBtnProps {
closeBtn?: number | string | boolean;
}
const props = defineProps<CloseBtnProps>();
const emit = defineEmits(["closeHandle"]);
const closeHandle = () => {
emit("closeHandle");
};
</script>
<template>
<a
:class="['layui-layer-ico layui-layer-close layui-layer-close' + closeBtn]"
href="javascript:;"
@click="closeHandle"
></a>
</template>

28
src/component/Iframe.vue Normal file
View File

@@ -0,0 +1,28 @@
<script lang="ts">
import { computed, VNodeTypes } from "vue";
export default {
name: "Iframe",
};
</script>
<script lang="ts" setup>
export interface IframeProps {
src?: string | Function | object | VNodeTypes;
}
const props = defineProps<IframeProps>();
const src = computed(() => {
return props.src as string;
});
</script>
<template>
<iframe
scrolling="auto"
class="layui-layer-iframe"
allowtransparency="true"
frameborder="0"
:src="src"
></iframe>
</template>

63
src/component/Notifiy.vue Normal file
View File

@@ -0,0 +1,63 @@
<template>
<div class="layui-layer-notifiy-wrapper" ref="notifyRef">
<h2 class="title">
<i v-if="icon" :class="iconClass"></i>
{{ title }}
</h2>
<div class="content" v-if="!isHtmlFragment">
<p>{{ content }}</p>
</div>
<div class="content" v-html="content" v-else></div>
<CloseBtnVue @click="close"></CloseBtnVue>
</div>
</template>
<script lang="ts">
export default {
name: "Notifiy",
};
</script>
<script lang="ts" setup>
import { nextTick, onMounted, ref, shallowRef } from "vue";
import CloseBtnVue from "./CloseBtn.vue";
export interface LayNotifyProps {
title: any;
content: any;
isHtmlFragment?: boolean;
icon?: string | number | undefined;
iconClass: string[];
}
const props = withDefaults(defineProps<LayNotifyProps>(), {
isHtmlFragment: false,
});
const emit = defineEmits(["close"]);
const notifyRef = shallowRef<HTMLElement | null>(null);
const close = () => {
emit("close");
};
function addClass(obj: HTMLElement, cls: string) {
//获取 class 内容.
let obj_class = obj.className,
//判断获取到的 class 是否为空, 如果不为空在前面加个'空格'.
blank = obj_class != "" ? " " : "";
let added = obj_class + blank + cls; //组合原来的 class 和需要添加的 class.
obj.className = added; //替换原来的 class.
}
onMounted(() => {
nextTick(() => {
setTimeout(() => {
//此处延迟加载class以免影响弹出效果
if (!notifyRef.value?.parentElement?.parentElement) return;
addClass(
notifyRef.value?.parentElement?.parentElement,
"layui-layer-notifiy-transition"
);
}, 300);
});
});
</script>

100
src/component/Photos.vue Normal file
View File

@@ -0,0 +1,100 @@
<template>
<div class="layui-layer-phimg">
<img :src="imgList[index].src" />
<div class="layui-layer-imgsee" v-if="imgList.length > 0">
<span class="layui-layer-imguide" v-if="imgList.length > 1">
<a
href="javascript:;"
class="layui-layer-iconext layui-layer-imgprev"
@click="changeIndex(-1)"
></a>
<a
href="javascript:;"
class="layui-layer-iconext layui-layer-imgnext"
@click="changeIndex(1)"
></a>
</span>
<div
class="layui-layer-imgbar"
v-if="imgList.length > 1 || imgList[index].alt"
:style="{ opacity: showLayerImgBar ? 1 : 0 }"
>
<div class="thumb-row" v-if="ifSetThumb">
<div
class="thumb-box"
v-for="(item, i) in imgList"
:key="'thumb-box' + i"
@click="index = i"
>
<img :src="item.thumb" />
</div>
<div
class="thumb-box-border"
:style="{
left: `calc(calc( calc(100% - ${100 * imgList.length}px) / 2) + ${
index * 100
}px)`,
}"
></div>
</div>
<span class="layui-layer-imgtit" v-else>
<span v-if="imgList[index].alt">{{ imgList[index].alt }}</span>
<em v-if="imgList.length > 1"
>{{ index + 1 }} / {{ imgList.length }}</em
>
</span>
</div>
</div>
</div>
</template>
<script lang="ts">
export default {
name: "Photos",
};
</script>
<script lang="ts" setup>
import { watch, ref, onMounted, nextTick, computed } from "vue";
export interface LayPhotoProps {
imgList: { src: string; alt: string; thumb: string }[];
startIndex: number;
}
const emit = defineEmits(["resetCalculationPohtosArea"]);
const props = withDefaults(defineProps<LayPhotoProps>(), {
startIndex: 0,
});
const index = ref(props.startIndex);
watch(index, () => {
//当图片索引改变的时候 重新计算弹层的大小
emit("resetCalculationPohtosArea", index.value);
});
const changeIndex = (step: number) => {
let nowIndex = index.value;
let next = nowIndex + step;
if (next < 0) {
next = props.imgList.length - 1;
}
if (next >= props.imgList.length) {
next = 0;
}
index.value = next;
};
const showLayerImgBar = ref(false);
onMounted(() => {
nextTick(() => {
showLayerImgBar.value = true;
});
});
const ifSetThumb = computed(() => {
let res = false;
props.imgList.forEach((e) => {
if (e.thumb) {
res = true;
}
});
return res;
});
</script>

9
src/component/Resize.vue Normal file
View File

@@ -0,0 +1,9 @@
<script lang="ts">
export default {
name: "Resize",
};
</script>
<template>
<span class="layui-layer-resize"></span>
</template>

39
src/component/Shade.vue Normal file
View File

@@ -0,0 +1,39 @@
<script lang="ts">
export default {
name: "Shade",
};
</script>
<script lang="ts" setup>
import { computed, defineEmits, StyleValue } from "vue";
export interface ShadeProps {
opacity: string;
index: number | Function;
visible: boolean | string;
}
const props = defineProps<ShadeProps>();
const emit = defineEmits(["shadeClick"]);
const styles = computed<any>(() => {
return {
opacity: props.opacity,
zIndex: props.index,
};
});
const shadeClick = () => {
emit("shadeClick");
};
</script>
<template>
<div
class="layui-layer-shade"
:style="styles"
@click="shadeClick"
v-if="visible"
></div>
</template>

26
src/component/Title.vue Normal file
View File

@@ -0,0 +1,26 @@
<script lang="ts">
export default {
name: "Title",
};
</script>
<script lang="ts" setup>
export interface HeaderProps {
title: string | boolean | Function;
}
const renderContent = function (content: any) {
if (typeof content === "function") {
return content();
}
return content;
};
const props = defineProps<HeaderProps>();
</script>
<template>
<div class="layui-layer-title" style="cursor: move">
{{ renderContent(title) }}
</div>
</template>

1
src/component/index.ts Normal file
View File

@@ -0,0 +1 @@
export { default as LayLayer } from "./index.vue"; // layer component

712
src/component/index.vue Normal file
View File

@@ -0,0 +1,712 @@
<script lang="ts">
export default {
name: "LayLayer",
};
</script>
<script lang="ts" setup>
import Shade from "./Shade.vue";
import Iframe from "./Iframe.vue";
import Title from "./Title.vue";
import CloseBtn from "./CloseBtn.vue";
import Resize from "./Resize.vue";
import Photos from "./Photos.vue";
import Notifiy from "./Notifiy.vue";
import {
Ref,
ref,
watch,
computed,
useSlots,
VNodeTypes,
nextTick,
inject,
} from "vue";
import {
nextId,
maxArea,
maxOffset,
getArea,
calculateArea,
calculateOffset,
calculateContent,
calculateType,
minArea,
minOffset,
updateMinArrays,
getDrawerAnimationClass,
calculateDrawerArea,
calculatePhotosArea,
calculateNotifOffset,
removeNotifiyFromQueen,
getNotifyAnimationClass,
} from "../utils";
import useResize from "../composable/useResize";
import useMove from "../composable/useMove";
import { zIndexKey } from "../tokens";
export interface LayerProps {
id?: string;
title?: string | boolean | Function;
icon?: string | number;
skin?: string;
zIndex?: number | Function;
setTop?: boolean;
offset?: string[] | string;
area?: string[] | "auto";
modelValue?: boolean;
maxmin?: boolean | string;
btn?: Record<string, Function>[] | false;
move?: boolean | string;
resize?: boolean | string;
type?:
| 0
| 1
| 2
| 3
| 4
| 5
| 6
| "dialog"
| "page"
| "iframe"
| "loading"
| "drawer"
| "photos"
| "notifiy";
content?: string | Function | object | VNodeTypes;
isHtmlFragment?: boolean;
shade?: boolean | string;
shadeClose?: boolean | string;
shadeOpacity?: string;
closeBtn?: boolean | string;
btnAlign?: "l" | "c" | "r";
time?: number;
load?: number;
anim?: 0 | 1 | 2 | 3 | 4 | 5 | 6;
isOutAnim?: boolean;
destroy?: Function;
success?: Function;
end?: Function;
yes?: Function;
yesText?: string;
isFunction?: boolean;
isMessage?: boolean;
appContext?: any;
startIndex?: number;
imgList?: { src: string; alt: string; thumb: string }[];
min?: Function;
full?: Function;
restore?: Function;
}
const props = withDefaults(defineProps<LayerProps>(), {
title: "标题",
setTop: false,
offset: () => ["50%", "50%"],
area: "auto",
modelValue: false,
maxmin: false,
move: true,
type: 1,
time: 0,
shade: true,
shadeClose: true,
shadeOpacity: "0.1",
closeBtn: "1",
btnAlign: "r",
load: 0,
anim: 0,
resize: false,
isHtmlFragment: false,
isOutAnim: true,
destroy: () => {},
success: () => {},
end: () => {},
full: () => {},
min: () => {},
restore: () => {},
yesText: "确定",
isFunction: false,
isMessage: false,
startIndex: 0,
imgList: () => [],
});
const emit = defineEmits(["close", "update:modelValue"]);
const slots = useSlots();
const max: Ref<boolean> = ref(false);
const min: Ref<boolean> = ref(false);
const id: Ref<string> = ref(props.id || nextId());
const layero = ref<HTMLElement | null>(null);
const type: number = calculateType(props.type);
const area: Ref<string[]> = ref(
calculateArea(props.type, props.area, props.offset)
);
const offset: Ref<string[]> = ref(
calculateOffset(props.offset, area.value, props.type)
);
const contentHeight = ref(
calculateContent(props.title, area.value[1], props.btn, type, props.isMessage)
);
const index: Ref<number | Function> = ref(
props.zIndex ?? inject(zIndexKey, 99999)
);
const visible: Ref<boolean> = ref(false);
const first: Ref<boolean> = ref(true);
const w: Ref<string> = ref(area.value[0]);
const h: Ref<string> = ref(area.value[1]);
const t: Ref<string> = ref(offset.value[0]);
const l: Ref<string> = ref(offset.value[1]);
const _w: Ref<string> = ref(area.value[0]);
const _h: Ref<string> = ref(area.value[0]);
const _t: Ref<string> = ref(offset.value[0]);
const _l: Ref<string> = ref(offset.value[1]);
/**
* 首次打开
* <p>
*/
const firstOpenDelayCalculation = function () {
nextTick(async () => {
area.value = getArea(layero.value);
if (type == 4) {
area.value = calculateDrawerArea(props.offset, props.area);
}
if (type == 5) {
area.value = await calculatePhotosArea(
props.imgList[props.startIndex].src,
props
);
}
offset.value = calculateOffset(props.offset, area.value, props.type);
if (type == 6) {
offset.value = calculateNotifOffset(props.offset, area.value, id.value);
}
w.value = area.value[0];
h.value = area.value[1];
_w.value = area.value[0];
_l.value = area.value[1];
t.value = offset.value[0];
l.value = offset.value[1];
_t.value = offset.value[0];
_l.value = offset.value[1];
supportMove();
});
};
/**
* 普通打开
* <p>
*/
const notFirstOpenLayerInit = function () {
w.value = _w.value;
h.value = _h.value;
t.value = _t.value;
l.value = _l.value;
supportMove();
};
/**
* Component 模式, 关闭事件
* <p>
* 在 Component 模式下, 隐藏前需要恢复正常窗体位置与尺寸, 保存
**/
const beforeCloseSaveData = function () {
if (min.value) minHandle();
if (max.value) maxHandle();
_w.value = w.value;
_h.value = h.value;
_t.value = t.value;
_l.value = l.value;
};
/**
* 弹层最大化
* <p>
*/
const maxHandle = () => {
if (max.value) {
w.value = _w.value;
h.value = _h.value;
t.value = _t.value;
l.value = _l.value;
props.restore(props.id);
} else {
_t.value = t.value;
_l.value = l.value;
_w.value = w.value;
_h.value = h.value;
w.value = maxArea().w;
h.value = maxArea().h;
t.value = maxOffset().t;
l.value = maxOffset().l;
props.full(props.id);
}
max.value = !max.value;
};
/**
* 弹层最小化
* <p>
*/
const minHandle = () => {
let left = 180 * updateMinArrays(id.value, !min.value);
if (left > document.documentElement.clientWidth - 180) {
left = document.documentElement.clientWidth - 180;
}
if (min.value) {
w.value = _w.value;
h.value = _h.value;
t.value = _t.value;
l.value = _l.value;
props.restore(props.id);
} else {
_w.value = w.value;
_h.value = h.value;
_t.value = t.value;
_l.value = l.value;
h.value = minArea().h;
w.value = minArea().w;
t.value = minOffset(left).t;
l.value = minOffset(left).l;
props.min(props.id);
}
min.value = !min.value;
};
/**
* 重置弹层
* <p>
*/
const reset = function () {
if (!first.value) {
min.value = false;
max.value = false;
w.value = area.value[0];
h.value = area.value[1];
t.value = offset.value[0];
l.value = offset.value[1];
_w.value = area.value[0];
_h.value = area.value[1];
_t.value = offset.value[0];
_l.value = offset.value[1];
}
if (!props.modelValue) {
emit("update:modelValue", true);
}
};
/**
* 监听 modalValue 的值
* <p>
* 只有 Component 模式会触发
*/
watch(
() => props.modelValue,
() => {
visible.value = props.modelValue;
if (visible.value) {
if (first.value) {
first.value = false;
firstOpenDelayCalculation();
} else {
notFirstOpenLayerInit();
}
} else {
beforeCloseSaveData();
}
},
{ deep: true, immediate: true }
);
/**
* 监听 visible 值
* <p>
*/
watch(
() => visible.value,
() => {
if (visible.value) {
if (props.isFunction) {
firstOpenDelayCalculation();
}
props.success();
}
},
{ immediate: true }
);
watch(
() => visible.value,
() => {
if (!visible.value) {
props.end();
}
}
);
/**
* 高度监听
* <p>
*
* 在发生拖拽时, 需根据弹出层的外层高度计算 content 高度, 需要
* 考虑 btn 操作栏, 计算公式contentHeight = h - btnHeight
*
* @param h 弹层高度
* @param btn 操作栏
* @param type 弹层类型
*/
watch(
() => h.value,
() => {
contentHeight.value = calculateContent(
props.title,
h.value,
props.btn,
type,
props.isMessage
);
}
);
/**
* 弹层类型
*
* @param type 类型
* @param isMessage 是消息
* @param icon 图标类型
*/
const boxClasses = computed(() => {
return [
{
"layui-layer-dialog": type === 0,
"layui-layer-page": type === 1,
"layui-layer-iframe": type === 2,
"layui-layer-loading": type === 3,
"layui-layer-drawer": type === 4,
"layui-layer-photos": type === 5,
"layui-layer-notifiy": type === 6,
"layui-layer-msg": props.isMessage,
"layui-layer-hui": props.isMessage && !props.icon,
},
props.skin,
];
});
/**
* 拖拽拉伸
* <p>
*/
const supportMove = function () {
if (props.move && type != 4) {
nextTick(() => {
// 拖拽
if (!layero.value) return;
useMove(layero.value, (left: string, top: string) => {
l.value = left;
t.value = top;
});
// 拉伸
useResize(layero.value, (width: string, height: string) => {
h.value = height;
w.value = width;
});
});
}
};
/**
* 弹层样式
* <p>
*/
const styles = computed<any>(() => {
let style = {
top: t.value,
left: l.value,
width: w.value,
height: h.value,
zIndex: index.value,
};
return style;
});
/**
* 弹层内容
* <p>
* @param type 类型
* @param load 加载动画
* @param icon 图标
*/
const contentClasses = computed(() => {
return [
type === 3 ? `layui-layer-loading${props.load}` : "",
props.icon ? "layui-layer-padding" : "",
];
});
/**
* 关闭操作
* <p>
* @param null
*/
const closeHandle = () => {
emit("close");
emit("update:modelValue", false);
props.destroy();
if (type === 6) {
//@ts-ignore
removeNotifiyFromQueen(props.id);
}
};
/**
* 确定操作
* <p>
* @param null
*/
const yesHandle = () => {
if (props.yes != undefined) props.yes();
else closeHandle();
};
/**
* 遮盖层点击
* <p>
* @param null
*/
const shadeHandle = () => {
if (props.shadeClose) closeHandle();
};
/**
* 获取内容
* <p>
* @param content 文本 / 方法
*/
const renderContent = function (content: any) {
if (content instanceof Function) {
return content();
}
return content;
};
/**
* 弹层图标
* <p>
* @param icon 图标
*/
const iconClass = computed(() => {
return ["layui-layer-ico", `layui-layer-ico${props.icon}`];
});
/**
* 入场动画
* <p>
* @param type 类型
* @param anim 入场动画
*/
const enterActiveClass = computed(() => {
if (type === 4) {
return getDrawerAnimationClass(props.offset);
}
if (type === 6) {
return getNotifyAnimationClass(props.offset);
}
return `layer-anim layer-anim-0${props.anim}`;
});
/**
* 离场动画
* <p>
* @param type 类型
* @param isOutAnim 离场动画
*/
const leaveActiveClass = computed(() => {
if (type === 4) {
return getDrawerAnimationClass(props.offset, true);
}
return props.isOutAnim ? `layer-anim-close` : "";
});
/**
* 打开弹层
* <p>
*/
const open = () => {
visible.value = true;
};
/**
* 关闭弹层
* <p>
*/
const close = () => {
visible.value = false;
};
/**
* 显示遮盖
* <p>
* @param visible 弹层状态
* @param shade 开启遮盖
* @param min 最小化
*/
const shadeVisible = computed(() => {
return visible.value && props.shade && !min.value;
});
/**
* 拉伸支持
* <p>
* @param resize 拉伸
* @param max 最大化
* @param min 最小化
*/
const showResize = computed(() => {
return props.resize && !max.value && !min.value;
});
/**
* 显示标题
* <p>
* @param title 标题
* @param type 类型
*/
const showTitle = computed(() => {
return props.title && props.type != 3 && props.type != 5 && props.type != 6;
});
/*
* 图片弹层重新计算
*/
const resetCalculationPohtosArea = function (index: number) {
nextTick(async () => {
area.value = await calculatePhotosArea(props.imgList[index].src, props);
offset.value = calculateOffset(props.offset, area.value, props.type);
w.value = area.value[0];
h.value = area.value[1];
t.value = offset.value[0];
l.value = offset.value[1];
_w.value = area.value[0];
_l.value = area.value[1];
_t.value = offset.value[0];
_l.value = offset.value[1];
});
};
defineExpose({ reset, open, close });
</script>
<template>
<div>
<!-- 遮盖层 -->
<Shade
:index="index"
:visible="shadeVisible"
:opacity="shadeOpacity"
@shadeClick="shadeHandle"
></Shade>
<!-- 动画容器 -->
<transition
:enter-active-class="enterActiveClass"
:leave-active-class="leaveActiveClass"
>
<!-- 弹出层 -->
<div
ref="layero"
class="layui-layer layui-layer-border"
:class="boxClasses"
:style="styles"
v-if="visible"
>
<!-- 标题 -->
<Title v-if="showTitle" :title="title"></Title>
<!-- 内容 -->
<div
class="layui-layer-content"
:style="{ height: contentHeight }"
:class="contentClasses"
>
<template v-if="type === 0 || type === 1 || type === 4">
<i v-if="icon" :class="iconClass"></i>
<slot v-if="slots.default"></slot>
<template v-else>
<template v-if="isHtmlFragment">
<span v-html="renderContent(props.content)"></span>
</template>
<template v-else>{{ renderContent(props.content) }}</template>
</template>
</template>
<Iframe v-if="type === 2" :src="props.content"></Iframe>
<Photos
v-if="type === 5"
:imgList="props.imgList"
:startIndex="props.startIndex"
@resetCalculationPohtosArea="resetCalculationPohtosArea"
></Photos>
<Notifiy
v-if="type === 6"
@close="closeHandle"
:title="props.title"
:content="props.content"
:isHtmlFragment="props.isHtmlFragment"
:icon="props.icon"
:iconClass="iconClass"
></Notifiy>
</div>
<!-- 工具栏 -->
<span
class="layui-layer-setwin"
v-if="type != 3 && type != 5 && type != 6"
>
<a
v-if="maxmin && !max"
class="layui-layer-min"
:class="[min ? 'layui-layer-ico layui-layer-maxmin' : '']"
href="javascript:;"
@click="minHandle"
>
<cite v-if="!min"></cite>
</a>
<a
v-if="maxmin && !min"
class="layui-layer-ico layui-layer-max"
:class="[max ? 'layui-layer-maxmin' : '']"
href="javascript:;"
@click="maxHandle"
></a>
<CloseBtn
v-if="closeBtn != false"
:close-btn="closeBtn"
@closeHandle="closeHandle"
></CloseBtn>
</span>
<!-- 操作栏 -->
<div
v-if="((btn && btn.length > 0) || type === 0) && !isMessage"
class="layui-layer-btn"
:class="[`layui-layer-btn-${btnAlign}`]"
>
<template v-if="btn && btn.length > 0">
<template v-for="(b, index) in btn" :key="index">
<a :class="[`layui-layer-btn${index}`]" @click="b.callback(id)">{{
b.text
}}</a>
</template>
</template>
<template v-else>
<template v-if="type === 0">
<a class="layui-layer-btn0" @click="yesHandle()">{{ yesText }}</a>
</template>
</template>
</div>
<!-- 辅助栏 -->
<Resize v-if="showResize"></Resize>
</div>
</transition>
</div>
</template>

53
src/composable/useMove.ts Normal file
View File

@@ -0,0 +1,53 @@
const useMove = function (el: HTMLElement, callback: Function) {
el.style.position = "fixed";
let offsetX: number;
let offsetY: number;
if (el != null) {
el.addEventListener("mousedown", function (event: any) {
const path = (event.composedPath && event.composedPath()) || event.path;
if (path[0].className === "layui-layer-title") {
if (event.button == 0 && el != null) {
const lexObj: any = getComputedStyle(el);
offsetX =
event.pageX - el.offsetLeft + parseInt(lexObj["margin-left"]);
offsetY =
event.pageY - el.offsetTop + parseInt(lexObj["margin-right"]);
const move = function (event: any) {
if (el != null) {
let x = event.pageX - offsetX;
let y = event.pageY - offsetY;
if (x < 0) {
x = 0;
} else if (
x >
document.documentElement.clientWidth - el.offsetWidth
) {
x = document.documentElement.clientWidth - el.offsetWidth;
}
if (y < 0) {
y = 0;
} else if (
y >
document.documentElement.clientHeight - el.offsetHeight
) {
y = document.documentElement.clientHeight - el.offsetHeight;
}
el.style.left = `${x}px`;
el.style.top = `${y}px`;
callback(el.style.left, el.style.top);
}
return false;
};
document.addEventListener("mousemove", move);
const stop = function () {
document.removeEventListener("mousemove", move);
document.removeEventListener("mouseup", stop);
};
document.addEventListener("mouseup", stop);
}
}
return false;
});
}
};
export default useMove;

View File

@@ -0,0 +1,35 @@
const useMove = function (el: HTMLElement, callback: Function) {
if (el != null) {
el.addEventListener("mousedown", function (event: any) {
const path = (event.composedPath && event.composedPath()) || event.path;
if (path[0].className === "layui-layer-resize") {
if (event.button == 0 && el != null) {
var x = el.offsetLeft;
var y = el.offsetTop;
const move = function (moveEvent: any) {
if (el != null) {
var offsetX = moveEvent.clientX;
var offsetY = moveEvent.clientY;
var w = offsetX - x;
var h = offsetY - y;
w < 260 && (w = 260);
h < 115 && (h = 115);
el.style.width = `${w}px`;
el.style.height = `${h}px`;
callback(el.style.width, el.style.height);
}
return false;
};
document.addEventListener("mousemove", move);
const stop = function () {
document.removeEventListener("mousemove", move);
document.removeEventListener("mouseup", stop);
};
document.addEventListener("mouseup", stop);
}
}
return false;
});
}
};
export default useMove;

270
src/index.ts Normal file
View File

@@ -0,0 +1,270 @@
import { render, h, isVNode, getCurrentInstance, AppContext, App } from "vue";
import LayLayer from "./component/index.vue";
import { InstallOptions } from "./types";
import { zIndexKey } from "./tokens";
import { nextId, removeNotifiyFromQueen } from "./utils";
// 实例队列
const layerInstance: any = [];
// 新增实例
const addInstance = (instance: any) => {
layerInstance.push(instance);
};
// 删除实例
const delInstance = (id: any) => {
layerInstance.forEach((item: any, index: number) => {
if (item.modalContainer.id === id) {
// 删除元素
layerInstance.splice(index, 1);
}
});
};
// 清空实例
const cleanInstance = () => {
layerInstance.splice(0, layerInstance.length);
};
// 是否存在
const isExist = (id: any) => {
let b = false;
layerInstance.forEach((item: any, index: number) => {
if (item.modalContainer.id == id) {
b = true;
}
});
return b;
};
const findById = (id: any) => {
let instance = null;
layerInstance.forEach((item: any, index: number) => {
if (item.modalContainer.id === id) {
instance = item;
}
});
return instance;
};
// 聚合 modal 配置
const mergeOption = (option: any, defaultOption: any) => {
if (option) defaultOption = Object.assign(defaultOption, option);
return defaultOption;
};
// 创建 modal 容器
const createContainer = (options: any) => {
const modalContainer = document.createElement("div");
modalContainer.id = options.id;
document.body.appendChild(modalContainer);
return modalContainer;
};
// modal 子 VNode
const modalChildrenVNode = (content: any) => {
if (typeof content === "function") {
return isVNode(content()) ? { default: () => content() } : undefined;
}
return isVNode(content) ? { default: () => content } : undefined;
};
const layer = {
_context: <AppContext | null>null,
// 页面
open: (option: any, callback?: Function) => {
let defaultOption = {};
return layer.create(option, defaultOption, callback);
},
// 抽屉
drawer: (option: any, callback?: Function) => {
let defaultOption = {
type: "drawer",
};
return layer.create(option, defaultOption, callback);
},
// 消息
msg: (message: string, option?: any, callback?: Function) => {
let defaultOption = {
type: 0,
title: false,
content: message,
closeBtn: false,
shadeClose: false,
isMessage: true,
shade: false,
time: 1000,
btn: false,
};
return layer.create(option, defaultOption, callback);
},
// 加载
load: (load: number, option?: any, callback?: Function) => {
let defaultOption = {
type: 3,
load: load,
anim: 5,
isOutAnim: false,
shadeClose: false,
};
return layer.create(option, defaultOption, callback);
},
// 确认
confirm: (msg: string, option?: any, callback?: Function) => {
let defaultOption = {
type: 0,
content: msg,
shadeClose: false,
};
return layer.create(option, defaultOption, callback);
},
//图片预览
photos: (option: any, callback?: Function) => {
if (typeof option === "string") {
option = {
imgList: [{ src: option }],
};
}
let defaultOption = {
type: 5,
anim: 2,
startIndex: 0,
isOutAnim: true,
shadeClose: true,
shadeOpacity: "0.7",
};
return layer.create(option, defaultOption, callback);
},
//通知
notifiy: (option: any = {}, callback?: Function) => {
option.type = 6;
let defaultOption = {
offset: "rt",
time: 2000,
area: "auto",
shade: false,
};
return layer.create(option, defaultOption, callback);
},
// 创建弹出层
create: (option: any, defaultOption: any, callback?: Function) => {
// 销毁定时
let timer: NodeJS.Timeout;
// 聚合配置 Opt
const options = mergeOption(option, defaultOption);
// 生成唯一标识
if (options.hasOwnProperty("id")) {
// 判断 id 存在, 并销毁窗体
layer.close(options.id);
} else {
// 生成新的唯一标识
options.id = nextId();
}
// 创建容器 Dom
const modalContainer = createContainer(options);
// 创建虚拟 Dom
const modalInstance = h(
LayLayer,
{
...options,
isFunction: true,
destroy() {
clearTimeout(timer);
modalInstance.component?.exposed?.close();
setTimeout(() => {
render(null, modalContainer);
// 清空 dom
if (document.body.contains(modalContainer)) {
document.body.removeChild(modalContainer);
}
}, 2000);
delInstance(modalContainer.id);
},
},
modalChildrenVNode(options.content)
);
modalInstance.appContext = options.appContext || layer._context;
// 将虚拟 dom 渲染到 dom 容器
render(modalInstance, modalContainer);
// 调用 open 函数
modalInstance.component?.exposed?.open();
// 延时 time 销毁
if (
defaultOption &&
defaultOption.time != undefined &&
defaultOption.time != 0
) {
timer = setTimeout(() => {
modalInstance.component?.exposed?.close();
if (callback) callback(modalContainer.id);
setTimeout(() => {
render(null, modalContainer);
if (document.body.contains(modalContainer)) {
document.body.removeChild(modalContainer);
}
}, 2000);
// 销毁实例
delInstance(modalContainer.id);
//Notifiy特殊处理
if (options.type === 6) {
removeNotifiyFromQueen(options.id);
}
}, defaultOption.time);
}
// 维护实例
addInstance({ modalContainer, modalInstance });
// 返回实例
return modalContainer.id;
},
// 关闭弹出层
close: (id: any) => {
if (id != null && isExist(id)) {
// 找到这个实例
const instance: any = findById(id);
instance.modalInstance.component?.exposed?.close();
setTimeout(() => {
render(null, instance.modalContainer);
if (document.body.contains(instance.modalContainer))
document.body.removeChild(instance.modalContainer);
}, 2000);
}
// 销毁实例
delInstance(id);
},
// 关闭所有弹出层
closeAll: () => {
layerInstance.forEach((item: any) => {
item.modalInstance.component?.exposed?.close();
setTimeout(() => {
render(null, item.modalContainer);
if (document.body.contains(item.modalContainer))
document.body.removeChild(item.modalContainer);
}, 2000);
});
// 清空实例
cleanInstance();
},
// 重置位置
reset: (instance: any) => {
instance.modalInstance.component?.exposed?.reset();
},
};
// 全局安装
const install = (app: App, options?: InstallOptions): void => {
layer._context = app._context;
app.component(LayLayer.name, LayLayer);
app.config.globalProperties.$layer = layer;
if (options) {
app.provide(zIndexKey, options.zIndex);
}
};
export { layer, LayLayer };
export default { install };
import "./theme/index.css";
import { getSystemErrorMap } from "util";

BIN
src/theme/icon-ext.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

BIN
src/theme/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

1128
src/theme/index.css Normal file

File diff suppressed because it is too large Load Diff

BIN
src/theme/loading-0.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

BIN
src/theme/loading-1.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 701 B

BIN
src/theme/loading-2.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

1
src/tokens/index.ts Normal file
View File

@@ -0,0 +1 @@
export const zIndexKey = Symbol("zIndex");

3
src/types/index.ts Normal file
View File

@@ -0,0 +1,3 @@
export type StringObject = Record<string, unknown>;
export interface InstallOptions extends StringObject {}

416
src/utils/index.ts Normal file
View File

@@ -0,0 +1,416 @@
import { title } from "process";
import { layer } from "../index";
// 随机数
export function nextId() {
var s: any = [];
var hexDigits = "0123456789abcdef";
for (var i = 0; i < 36; i++) {
s[i] = hexDigits.substr(Math.floor(Math.random() * 0x10), 1);
}
s[14] = "4";
s[19] = hexDigits.substr((s[19] & 0x3) | 0x8, 1);
s[8] = s[13] = s[18] = s[23] = "-";
var uuid = s.join("");
return uuid;
}
export function calculateMinArea(minArea: any) {
if (!minArea) {
return ["120px", "100px"];
}
if (typeof minArea == "string") {
return [minArea];
}
return [minArea];
}
export function calculateArea(type: any, area: any, offset: any) {
return type != "drawer"
? calculateBaseArea(area)
: calculateDrawerArea(offset, area);
}
// 计算宽高
// @param area
// @param type
// @return 正确宽高
export function calculateBaseArea(area: any) {
if (area === "auto") {
return [];
}
// @ts-ignore
if (typeof area == "string") {
return [area];
}
return [...area];
}
// 抽屉宽/高
export function calculateDrawerArea(
offset: any,
drawerArea: string[] | string = "30%"
) {
if (drawerArea instanceof Array) {
return drawerArea;
}
if (drawerArea === "auto") {
drawerArea = "30%";
}
if (offset === "l" || offset === "r") {
return [drawerArea, "100%"];
} else if (offset === "t" || offset === "b") {
return ["100%", drawerArea];
}
return [drawerArea, "100%"];
}
// 计算偏移
// @param offset
// @param domSize
// @return 正确位置
export function calculateOffset(offset: any, area: any, type: any) {
var arr = ["t", "r", "b", "l", "lt", "lb", "rt", "rb"];
var t = offset[0];
var l = offset[1];
if (offset instanceof Array && type === "drawer") {
offset = "r";
}
// @ts-ignore
if (arr.indexOf(offset) > -1) {
t = "50%";
l = "50%";
}
// 预备处理
if (arr.indexOf(offset) != -1 || t.indexOf("%") > -1)
t = "calc(" + t + " - (" + (area === "auto" ? "100px" : area[1]) + "/2 ))";
if (arr.indexOf(offset) != -1 || l.indexOf("%") > -1)
l = "calc(" + l + " - (" + (area === "auto" ? "100px" : area[0]) + "/2 ))";
// 关键字处理
if (offset === "t") t = "0px";
else if (offset === "r") l = "calc(100% - " + area[0] + ")";
else if (offset === "b") t = "calc(100% - " + area[1] + ")";
else if (offset === "l") l = "0px";
else if (offset === "lt") {
t = "0px";
l = "0px";
} else if (offset === "lb") {
t = "calc(100% - " + area[1] + ")";
l = "0px";
} else if (offset === "rt") {
t = "0px";
l = "calc(100% - " + area[0] + ")";
} else if (offset === "rb") {
t = "calc(100% - " + area[1] + ")";
l = "calc(100% - " + area[0] + ")";
}
// 返回位置
return [t, l];
}
// 窗体类型
export function calculateType(modalType: number | string) {
if (modalType === "dialog" || modalType == 0) {
return 0;
} else if (modalType === "page" || modalType == 1) {
return 1;
} else if (modalType === "iframe" || modalType == 2) {
return 2;
} else if (modalType === "loading" || modalType == 3) {
return 3;
} else if (modalType === "drawer" || modalType == 4) {
return 4;
} else if (modalType === "photos" || modalType == 5) {
return 5;
} else if (modalType === "notifiy" || modalType == 6) {
return 6;
}
return 0;
}
// 计算高度
// @param height 高度
// @param btn 操作集合
export function calculateContent(
title: any,
height: any,
btn: any,
type: any,
isMessage?: boolean
) {
if (height && height.indexOf("%") != -1) {
height = "100%";
}
if (btn && btn.length > 0) {
if (type == 0) {
if (title) {
return "calc(" + height + " - 137px)";
} else {
return "calc(" + height + " - 86px)";
}
}
if (type == 1 || type == 4) {
if (title) {
return "calc(" + height + " - 102px)";
} else {
return "calc(" + height + " - 51px)";
}
}
if (type == 2) {
if (title) {
return "calc(" + height + " - 102px)";
} else {
return "calc(" + height + " - 51px)";
}
}
} else {
if (type == 0) {
if (title) {
return isMessage ? "" : "calc(" + height + " - 137px)";
} else {
return isMessage ? "" : "calc(" + height + " - 86px)";
}
}
if (type == 1 || type == 4) {
if (title) {
return "calc(" + height + " - 51px)";
} else {
return "calc(" + height + " - 0px)";
}
}
if (type == 2) {
if (title) {
return "calc(" + height + " - 51px)";
} else {
return "calc(" + height + " - 0px)";
}
}
}
}
// 尺寸常量
export function maxArea() {
return { w: "100%", h: "100%" };
}
// 初始位置
export function maxOffset() {
return { t: "0px", l: "0px" };
}
// 最小化尺寸
export function minArea() {
return { w: "180px", h: "51px" };
}
// 最小化位置
export function minOffset(left: any) {
return { t: "calc(100% - 51px)", l: left + "px" };
}
// 元素位置
// @param x 横坐标
// @param y 纵坐标
export function getPosition(dom: any) {
return { x: dom?.style.left, y: dom?.style.top };
}
// 元素宽高
export function getArea(dom: any) {
// @ts-ignore
let width = getComputedStyle(dom, null).width;
// @ts-ignore
let height = getComputedStyle(dom, null).height;
return [width, height];
}
// 最小化的队列
let minArrays: Array<String> = [];
// 更新最小化队列
export function updateMinArrays(id: string, state: Boolean) {
var i = 0;
if (state) {
const index = minArrays.findIndex((v) => v === undefined);
if (index === -1) {
minArrays.push(id);
i = minArrays.length - 1;
} else {
minArrays[index] = id;
i = index;
}
} else {
delete minArrays[minArrays.findIndex((v) => v == id)];
i = -1;
}
return i;
}
// 抽屉动画类
export function getDrawerAnimationClass(offset: any, isClose: boolean = false) {
const prefix = "layer-drawer-anim layer-anim";
let suffix = "rl";
if (offset === "l") {
suffix = "lr";
} else if (offset === "r") {
suffix = "rl";
} else if (offset === "t") {
suffix = "tb";
} else if (offset === "b") {
suffix = "bt";
}
return isClose ? `${prefix}-${suffix}-close` : `${prefix}-${suffix}`;
}
//计算图片大小 并缩放
export async function calculatePhotosArea(
url: string,
options: object
): Promise<Array<string>> {
let img = new Image();
img.src = url;
return new Promise((resolve, reject) => {
if (img.complete) {
resolve(area(img));
return;
}
const layerId = layer.load(2);
img.onload = () => {
layer.close(layerId);
resolve(area(img));
};
img.onerror = () => {
layer.close(layerId);
layer.msg("图片加载失败");
reject(false);
};
});
function area(img: { width: number; height: number }) {
var imgarea = [img.width, img.height];
var winarea = [window.innerWidth - 250, window.innerHeight - 250];
//如果 实际图片的宽或者高比 屏幕大(那么进行缩放)
if (imgarea[0] > winarea[0] || imgarea[1] > winarea[1]) {
let wh = [imgarea[0] / winarea[0], imgarea[1] / winarea[1]]; //取宽度缩放比例、高度缩放比例
if (wh[0] > wh[1]) {
//取缩放比例最大的进行缩放
imgarea[0] = imgarea[0] / wh[0];
imgarea[1] = imgarea[1] / wh[0];
} else if (wh[0] < wh[1]) {
imgarea[0] = imgarea[0] / wh[1];
imgarea[1] = imgarea[1] / wh[1];
}
}
return [imgarea[0] + "px", imgarea[1] + "px"];
}
}
// 计算Notify位置 队列 此处先暂时定义Notify的间距为15px
export function calculateNotifOffset(offset: any, area: any, layerId: string) {
let arr = ["lt", "lb", "rt", "rb"];
let t = "0",
l = "0";
// 间隙
let transOffsetLeft = 15;
let transOffsetTop = 15;
(window as any).NotifiyQueen = (window as any).NotifiyQueen || [];
let notifiyQueen = (window as any).NotifiyQueen;
if (typeof offset != "string" || arr.indexOf(offset) === -1) {
offset = "rt";
}
// 当前区域元素集合
let nodeList = notifiyQueen.filter((e: { offset: any }) => {
if (e.offset === offset) {
return e;
}
});
//前一个元素
let prevNode = nodeList.length > 0 ? nodeList[nodeList.length - 1] : null;
if (prevNode) {
prevNode = document.getElementById(prevNode["id"])?.firstElementChild
?.firstElementChild;
if (offset === "rt" || offset === "lt") {
transOffsetTop +=
prevNode.offsetHeight + parseFloat(prevNode.style["top"]);
} else {
let bottom = parseFloat(prevNode.style["top"].split(" - ")[1]);
transOffsetTop += prevNode.offsetHeight + bottom;
}
} else {
if (offset === "rb" || offset === "lb") {
transOffsetTop += parseFloat(area[1]);
}
}
// 关键字处理
if (offset === "rt") {
t = transOffsetTop + "px";
l = "calc(100% - " + (parseFloat(area[0]) + transOffsetLeft) + "px)";
} else if (offset === "rb") {
t = "calc(100vh - " + transOffsetTop + "px)";
l = "calc(100% - " + (parseFloat(area[0]) + transOffsetLeft) + "px)";
} else if (offset === "lt") {
t = transOffsetTop + "px";
l = transOffsetLeft + "px";
} else if (offset === "lb") {
t = "calc(100vh - " + transOffsetTop + "px)";
l = transOffsetLeft + "px";
}
notifiyQueen.push({
id: layerId,
offset: offset,
});
// 返回位置
return [t, l];
}
//移除Notify队列中某项并且重新计算其他Notify位置
export function removeNotifiyFromQueen(layerId: string) {
// 间隙
let transOffsetTop = 15;
// 删除项的高度
let notifiyDom = document.getElementById(layerId)?.firstElementChild
?.firstElementChild as HTMLElement;
let offsetHeight = notifiyDom.offsetHeight;
(window as any).NotifiyQueen = (window as any).NotifiyQueen || [];
let notifiyQueen = (window as any).NotifiyQueen;
let index = notifiyQueen.findIndex((e: { id: string }) => e.id === layerId);
let offsetType = notifiyQueen[index].offset;
let list = notifiyQueen.filter((e: { offset: any }) => {
if (e.offset === offsetType) {
return e;
}
});
let findIndex = list.findIndex((e: { id: string }) => e.id === layerId);
// //得到需要修改的定位的Notifiy集合
let needCalculatelist = list.slice(findIndex + 1);
needCalculatelist.forEach((e: { id: string }) => {
let dom = document.getElementById(e.id)?.firstElementChild
?.firstElementChild as HTMLElement;
if (offsetType === "rt" || offsetType === "lt") {
dom.style["top"] =
parseFloat(dom.style["top"]) - transOffsetTop - offsetHeight + "px";
} else {
let bottom =
parseFloat(dom.style["top"].split(" - ")[1]) -
transOffsetTop -
offsetHeight;
dom.style["top"] = "calc(100vh - " + bottom + "px)";
}
});
notifiyQueen.splice(index, 1); //删除
}
// Notify动画类
export function getNotifyAnimationClass(offset: any) {
const prefix = "layer-drawer-anim layer-anim";
let suffix = "";
if (offset === "lt" || offset === "lb") {
suffix = "lr";
} else {
suffix = "rl";
}
return `${prefix}-${suffix}`;
}