feat:新增pure-admin前端

This commit is contained in:
chenchun
2024-08-23 14:31:00 +08:00
parent 556d32729a
commit 4bc2cebd60
579 changed files with 85268 additions and 0 deletions

View File

@@ -0,0 +1,7 @@
import { withInstall } from "@pureadmin/utils";
import reAnimateSelector from "./src/index.vue";
/** [animate.css](https://animate.style/) 选择器组件 */
export const ReAnimateSelector = withInstall(reAnimateSelector);
export default ReAnimateSelector;

View File

@@ -0,0 +1,114 @@
export const animates = [
/* Attention seekers */
"bounce",
"flash",
"pulse",
"rubberBand",
"shakeX",
"headShake",
"swing",
"tada",
"wobble",
"jello",
"heartBeat",
/* Back entrances */
"backInDown",
"backInLeft",
"backInRight",
"backInUp",
/* Back exits */
"backOutDown",
"backOutLeft",
"backOutRight",
"backOutUp",
/* Bouncing entrances */
"bounceIn",
"bounceInDown",
"bounceInLeft",
"bounceInRight",
"bounceInUp",
/* Bouncing exits */
"bounceOut",
"bounceOutDown",
"bounceOutLeft",
"bounceOutRight",
"bounceOutUp",
/* Fading entrances */
"fadeIn",
"fadeInDown",
"fadeInDownBig",
"fadeInLeft",
"fadeInLeftBig",
"fadeInRight",
"fadeInRightBig",
"fadeInUp",
"fadeInUpBig",
"fadeInTopLeft",
"fadeInTopRight",
"fadeInBottomLeft",
"fadeInBottomRight",
/* Fading exits */
"fadeOut",
"fadeOutDown",
"fadeOutDownBig",
"fadeOutLeft",
"fadeOutLeftBig",
"fadeOutRight",
"fadeOutRightBig",
"fadeOutUp",
"fadeOutUpBig",
"fadeOutTopLeft",
"fadeOutTopRight",
"fadeOutBottomRight",
"fadeOutBottomLeft",
/* Flippers */
"flip",
"flipInX",
"flipInY",
"flipOutX",
"flipOutY",
/* Lightspeed */
"lightSpeedInRight",
"lightSpeedInLeft",
"lightSpeedOutRight",
"lightSpeedOutLeft",
/* Rotating entrances */
"rotateIn",
"rotateInDownLeft",
"rotateInDownRight",
"rotateInUpLeft",
"rotateInUpRight",
/* Rotating exits */
"rotateOut",
"rotateOutDownLeft",
"rotateOutDownRight",
"rotateOutUpLeft",
"rotateOutUpRight",
/* Specials */
"hinge",
"jackInTheBox",
"rollIn",
"rollOut",
/* Zooming entrances */
"zoomIn",
"zoomInDown",
"zoomInLeft",
"zoomInRight",
"zoomInUp",
/* Zooming exits */
"zoomOut",
"zoomOutDown",
"zoomOutLeft",
"zoomOutRight",
"zoomOutUp",
/* Sliding entrances */
"slideInDown",
"slideInLeft",
"slideInRight",
"slideInUp",
/* Sliding exits */
"slideOutDown",
"slideOutLeft",
"slideOutRight",
"slideOutUp"
];

View File

@@ -0,0 +1,136 @@
<script setup lang="ts">
import { ref, computed } from "vue";
import { animates } from "./animate";
import { cloneDeep } from "@pureadmin/utils";
defineOptions({
name: "ReAnimateSelector"
});
defineProps({
placeholder: {
type: String,
default: "请选择动画"
}
});
const inputValue = defineModel({ type: String });
const searchVal = ref();
const animatesList = ref(animates);
const copyAnimatesList = cloneDeep(animatesList);
const animateClass = computed(() => {
return [
"mt-1",
"flex",
"border",
"w-[130px]",
"h-[100px]",
"items-center",
"cursor-pointer",
"transition-all",
"justify-center",
"border-[#e5e7eb]",
"hover:text-primary",
"hover:duration-[700ms]"
];
});
const animateStyle = computed(
() => (i: string) =>
inputValue.value === i
? {
borderColor: "var(--el-color-primary)",
color: "var(--el-color-primary)"
}
: ""
);
function onChangeIcon(animate: string) {
inputValue.value = animate;
}
function onClear() {
inputValue.value = "";
}
function filterMethod(value: any) {
searchVal.value = value;
animatesList.value = copyAnimatesList.value.filter((i: string | any[]) =>
i.includes(value)
);
}
const animateMap = ref({});
function onMouseEnter(index: string | number) {
animateMap.value[index] = animateMap.value[index]?.loading
? Object.assign({}, animateMap.value[index], {
loading: false
})
: Object.assign({}, animateMap.value[index], {
loading: true
});
}
function onMouseleave() {
animateMap.value = {};
}
</script>
<template>
<el-select
clearable
filterable
:placeholder="placeholder"
popper-class="pure-animate-popper"
:model-value="inputValue"
:filter-method="filterMethod"
@clear="onClear"
>
<template #empty>
<div class="w-[280px]">
<el-scrollbar
noresize
height="212px"
:view-style="{ overflow: 'hidden' }"
class="border-t border-[#e5e7eb]"
>
<ul class="flex flex-wrap justify-around mb-1">
<li
v-for="(animate, index) in animatesList"
:key="index"
:class="animateClass"
:style="animateStyle(animate)"
@mouseenter.prevent="onMouseEnter(index)"
@mouseleave.prevent="onMouseleave"
@click="onChangeIcon(animate)"
>
<h4
:class="[
`animate__animated animate__${
animateMap[index]?.loading
? animate + ' animate__infinite'
: ''
} `
]"
>
{{ animate }}
</h4>
</li>
</ul>
<el-empty
v-show="animatesList.length === 0"
:description="`${searchVal} 动画不存在`"
:image-size="60"
/>
</el-scrollbar>
</div>
</template>
</el-select>
</template>
<style>
.pure-animate-popper {
min-width: 0 !important;
}
</style>

View File

@@ -0,0 +1,5 @@
import auth from "./src/auth";
const Auth = auth;
export { Auth };

View File

@@ -0,0 +1,20 @@
import { defineComponent, Fragment } from "vue";
import { hasAuth } from "@/router/utils";
export default defineComponent({
name: "Auth",
props: {
value: {
type: undefined,
default: []
}
},
setup(props, { slots }) {
return () => {
if (!slots) return null;
return hasAuth(props.value) ? (
<Fragment>{slots.default?.()}</Fragment>
) : null;
};
}
});

View File

@@ -0,0 +1,7 @@
import { withInstall } from "@pureadmin/utils";
import reBarcode from "./src/index.vue";
/** 条形码组件 */
export const ReBarcode = withInstall(reBarcode);
export default ReBarcode;

View File

@@ -0,0 +1,42 @@
<script setup lang="ts">
import JsBarcode from "jsbarcode";
import { ref, onMounted } from "vue";
defineOptions({
name: "ReBarcode"
});
const props = defineProps({
tag: {
type: String,
default: "canvas"
},
text: {
type: String,
default: null
},
// 完整配置 https://github.com/lindell/JsBarcode/wiki/Options
options: {
type: Object,
default() {
return {};
}
},
// type 相当于 options.format如果 type 和 options.format 同时存在type 值优先;
type: {
type: String,
default: "CODE128"
}
});
const wrapEl = ref(null);
onMounted(() => {
const opt = { ...props.options, format: props.type };
JsBarcode(wrapEl.value, props.text, opt);
});
</script>
<template>
<component :is="tag" ref="wrapEl" />
</template>

View File

@@ -0,0 +1,29 @@
import { ElCol } from "element-plus";
import { h, defineComponent } from "vue";
// 封装element-plus的el-col组件
export default defineComponent({
name: "ReCol",
props: {
value: {
type: Number,
default: 24
}
},
render() {
const attrs = this.$attrs;
const val = this.value;
return h(
ElCol,
{
xs: val,
sm: val,
md: val,
lg: val,
xl: val,
...attrs
},
{ default: () => this.$slots.default() }
);
}
});

View File

@@ -0,0 +1,2 @@
normal 普通数字动画组件
rebound 回弹式数字动画组件

View File

@@ -0,0 +1,11 @@
import reNormalCountTo from "./src/normal";
import reboundCountTo from "./src/rebound";
import { withInstall } from "@pureadmin/utils";
/** 普通数字动画组件 */
const ReNormalCountTo = withInstall(reNormalCountTo);
/** 回弹式数字动画组件 */
const ReboundCountTo = withInstall(reboundCountTo);
export { ReNormalCountTo, ReboundCountTo };

View File

@@ -0,0 +1,179 @@
import {
watch,
unref,
computed,
reactive,
onMounted,
defineComponent
} from "vue";
import { countToProps } from "./props";
import { isNumber } from "@pureadmin/utils";
export default defineComponent({
name: "ReNormalCountTo",
props: countToProps,
emits: ["mounted", "callback"],
setup(props, { emit }) {
const state = reactive<{
localStartVal: number;
printVal: number | null;
displayValue: string;
paused: boolean;
localDuration: number | null;
startTime: number | null;
timestamp: number | null;
rAF: any;
remaining: number | null;
color: string;
fontSize: string;
}>({
localStartVal: props.startVal,
displayValue: formatNumber(props.startVal),
printVal: null,
paused: false,
localDuration: props.duration,
startTime: null,
timestamp: null,
remaining: null,
rAF: null,
color: null,
fontSize: "16px"
});
const getCountDown = computed(() => {
return props.startVal > props.endVal;
});
watch([() => props.startVal, () => props.endVal], () => {
if (props.autoplay) {
start();
}
});
function start() {
const { startVal, duration, color, fontSize } = props;
state.localStartVal = startVal;
state.startTime = null;
state.localDuration = duration;
state.paused = false;
state.color = color;
state.fontSize = fontSize;
state.rAF = requestAnimationFrame(count);
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
function pauseResume() {
if (state.paused) {
resume();
state.paused = false;
} else {
pause();
state.paused = true;
}
}
function pause() {
cancelAnimationFrame(state.rAF);
}
function resume() {
state.startTime = null;
state.localDuration = +(state.remaining as number);
state.localStartVal = +(state.printVal as number);
requestAnimationFrame(count);
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
function reset() {
state.startTime = null;
cancelAnimationFrame(state.rAF);
state.displayValue = formatNumber(props.startVal);
}
function count(timestamp: number) {
const { useEasing, easingFn, endVal } = props;
if (!state.startTime) state.startTime = timestamp;
state.timestamp = timestamp;
const progress = timestamp - state.startTime;
state.remaining = (state.localDuration as number) - progress;
if (useEasing) {
if (unref(getCountDown)) {
state.printVal =
state.localStartVal -
easingFn(
progress,
0,
state.localStartVal - endVal,
state.localDuration as number
);
} else {
state.printVal = easingFn(
progress,
state.localStartVal,
endVal - state.localStartVal,
state.localDuration as number
);
}
} else {
if (unref(getCountDown)) {
state.printVal =
state.localStartVal -
(state.localStartVal - endVal) *
(progress / (state.localDuration as number));
} else {
state.printVal =
state.localStartVal +
(endVal - state.localStartVal) *
(progress / (state.localDuration as number));
}
}
if (unref(getCountDown)) {
state.printVal = state.printVal < endVal ? endVal : state.printVal;
} else {
state.printVal = state.printVal > endVal ? endVal : state.printVal;
}
state.displayValue = formatNumber(state.printVal);
if (progress < (state.localDuration as number)) {
state.rAF = requestAnimationFrame(count);
} else {
emit("callback");
}
}
function formatNumber(num: number | string) {
const { decimals, decimal, separator, suffix, prefix } = props;
num = Number(num).toFixed(decimals);
num += "";
const x = num.split(".");
let x1 = x[0];
const x2 = x.length > 1 ? decimal + x[1] : "";
const rgx = /(\d+)(\d{3})/;
if (separator && !isNumber(separator)) {
while (rgx.test(x1)) {
x1 = x1.replace(rgx, "$1" + separator + "$2");
}
}
return prefix + x1 + x2 + suffix;
}
onMounted(() => {
if (props.autoplay) {
start();
}
emit("mounted");
});
return () => (
<>
<span
style={{
color: props.color,
fontSize: props.fontSize
}}
>
{state.displayValue}
</span>
</>
);
}
});

View File

@@ -0,0 +1,32 @@
import type { PropType } from "vue";
import propTypes from "@/utils/propTypes";
export const countToProps = {
startVal: propTypes.number.def(0),
endVal: propTypes.number.def(2020),
duration: propTypes.number.def(1300),
autoplay: propTypes.bool.def(true),
decimals: {
type: Number as PropType<number>,
required: false,
default: 0,
validator(value: number) {
return value >= 0;
}
},
color: propTypes.string.def(),
fontSize: propTypes.string.def(),
decimal: propTypes.string.def("."),
separator: propTypes.string.def(","),
prefix: propTypes.string.def(""),
suffix: propTypes.string.def(""),
useEasing: propTypes.bool.def(true),
easingFn: {
type: Function as PropType<
(t: number, b: number, c: number, d: number) => number
>,
default(t: number, b: number, c: number, d: number) {
return (c * (-Math.pow(2, (-10 * t) / d) + 1) * 1024) / 1023 + b;
}
}
};

View File

@@ -0,0 +1,72 @@
import "./rebound.css";
import {
ref,
unref,
onBeforeMount,
defineComponent,
onBeforeUnmount
} from "vue";
import { reboundProps } from "./props";
export default defineComponent({
name: "ReboundCountTo",
props: reboundProps,
setup(props) {
const ulRef = ref();
const timer = ref(null);
onBeforeMount(() => {
const ua = navigator.userAgent.toLowerCase();
const testUA = regexp => regexp.test(ua);
const isSafari = testUA(/safari/g) && !testUA(/chrome/g);
// Safari浏览器的兼容代码
isSafari &&
(timer.value = setTimeout(() => {
ulRef.value.setAttribute(
"style",
`
animation: none;
transform: translateY(calc(var(--i) * -9.09%))
`
);
}, props.delay * 1000));
});
onBeforeUnmount(() => {
clearTimeout(unref(timer));
});
return () => (
<>
<div
class="scroll-num"
style={{ "--i": props.i, "--delay": props.delay }}
>
<ul ref="ulRef" style={{ fontSize: "32px" }}>
<li>0</li>
<li>1</li>
<li>2</li>
<li>3</li>
<li>4</li>
<li>5</li>
<li>6</li>
<li>7</li>
<li>8</li>
<li>9</li>
<li>0</li>
</ul>
<svg width="0" height="0">
<filter id="blur">
<feGaussianBlur
in="SourceGraphic"
stdDeviation={`0 ${props.blur}`}
/>
</filter>
</svg>
</div>
</>
);
}
});

View File

@@ -0,0 +1,15 @@
import type { PropType } from "vue";
import propTypes from "@/utils/propTypes";
export const reboundProps = {
delay: propTypes.number.def(1),
blur: propTypes.number.def(2),
i: {
type: Number as PropType<number>,
required: false,
default: 0,
validator(value: number) {
return value < 10 && value >= 0 && Number.isInteger(value);
}
}
};

View File

@@ -0,0 +1,77 @@
.scroll-num {
width: var(--width, 20px);
height: var(--height, calc(var(--width, 20px) * 1.8));
color: var(--color, #333);
font-size: var(--height, calc(var(--width, 20px) * 1.1));
line-height: var(--height, calc(var(--width, 20px) * 1.8));
text-align: center;
overflow: hidden;
animation: enhance-bounce-in-down 1s calc(var(--delay) * 1s) forwards;
}
ul {
animation:
move 0.3s linear infinite,
bounce-in-down 1s calc(var(--delay) * 1s) forwards;
}
@keyframes move {
from {
transform: translateY(-90%);
filter: url(#blur);
}
to {
transform: translateY(1%);
filter: url(#blur);
}
}
@keyframes bounce-in-down {
from {
transform: translateY(calc(var(--i) * -9.09% - 7%));
filter: none;
}
25% {
transform: translateY(calc(var(--i) * -9.09% + 3%));
}
50% {
transform: translateY(calc(var(--i) * -9.09% - 1%));
}
70% {
transform: translateY(calc(var(--i) * -9.09% + 0.6%));
}
85% {
transform: translateY(calc(var(--i) * -9.09% - 0.3%));
}
to {
transform: translateY(calc(var(--i) * -9.09%));
}
}
@keyframes enhance-bounce-in-down {
25% {
transform: translateY(8%);
}
50% {
transform: translateY(-4%);
}
70% {
transform: translateY(2%);
}
85% {
transform: translateY(-1%);
}
to {
transform: translateY(0);
}
}

View File

@@ -0,0 +1,7 @@
import reCropper from "./src";
import { withInstall } from "@pureadmin/utils";
/** 图片裁剪组件 */
export const ReCropper = withInstall(reCropper);
export default ReCropper;

View File

@@ -0,0 +1,8 @@
@import "cropperjs/dist/cropper.css";
.re-circled {
.cropper-view-box,
.cropper-face {
border-radius: 50%;
}
}

View File

@@ -0,0 +1,457 @@
import "./circled.css";
import Cropper from "cropperjs";
import { ElUpload } from "element-plus";
import type { CSSProperties } from "vue";
import { useEventListener } from "@vueuse/core";
import { longpress } from "@/directives/longpress";
import { useTippy, directive as tippy } from "vue-tippy";
import {
type PropType,
ref,
unref,
computed,
onMounted,
onUnmounted,
defineComponent
} from "vue";
import {
delay,
debounce,
isArray,
downloadByBase64,
useResizeObserver
} from "@pureadmin/utils";
import {
Reload,
Upload,
ArrowH,
ArrowV,
ArrowUp,
ArrowDown,
ArrowLeft,
ChangeIcon,
ArrowRight,
RotateLeft,
SearchPlus,
RotateRight,
SearchMinus,
DownloadIcon
} from "./svg";
type Options = Cropper.Options;
const defaultOptions: Options = {
aspectRatio: 1,
zoomable: true,
zoomOnTouch: true,
zoomOnWheel: true,
cropBoxMovable: true,
cropBoxResizable: true,
toggleDragModeOnDblclick: true,
autoCrop: true,
background: true,
highlight: true,
center: true,
responsive: true,
restore: true,
checkCrossOrigin: true,
checkOrientation: true,
scalable: true,
modal: true,
guides: true,
movable: true,
rotatable: true
};
const props = {
src: { type: String, required: true },
alt: { type: String },
circled: { type: Boolean, default: false },
/** 是否可以通过点击裁剪区域关闭右键弹出的功能菜单,默认 `true` */
isClose: { type: Boolean, default: true },
realTimePreview: { type: Boolean, default: true },
height: { type: [String, Number], default: "360px" },
crossorigin: {
type: String as PropType<"" | "anonymous" | "use-credentials" | undefined>,
default: undefined
},
imageStyle: { type: Object as PropType<CSSProperties>, default: () => ({}) },
options: { type: Object as PropType<Options>, default: () => ({}) }
};
export default defineComponent({
name: "ReCropper",
props,
setup(props, { attrs, emit }) {
const tippyElRef = ref<ElRef<HTMLImageElement>>();
const imgElRef = ref<ElRef<HTMLImageElement>>();
const cropper = ref<Nullable<Cropper>>();
const inCircled = ref(props.circled);
const isInClose = ref(props.isClose);
const inSrc = ref(props.src);
const isReady = ref(false);
const imgBase64 = ref();
let scaleX = 1;
let scaleY = 1;
const debounceRealTimeCroppered = debounce(realTimeCroppered, 80);
const getImageStyle = computed((): CSSProperties => {
return {
height: props.height,
maxWidth: "100%",
...props.imageStyle
};
});
const getClass = computed(() => {
return [
attrs.class,
{
["re-circled"]: inCircled.value
}
];
});
const iconClass = computed(() => {
return [
"p-[6px]",
"h-[30px]",
"w-[30px]",
"outline-none",
"rounded-[4px]",
"cursor-pointer",
"hover:bg-[rgba(0,0,0,0.06)]"
];
});
const getWrapperStyle = computed((): CSSProperties => {
return { height: `${props.height}`.replace(/px/, "") + "px" };
});
onMounted(init);
onUnmounted(() => {
cropper.value?.destroy();
isReady.value = false;
cropper.value = null;
imgBase64.value = "";
scaleX = 1;
scaleY = 1;
});
useResizeObserver(tippyElRef, () => handCropper("reset"));
async function init() {
const imgEl = unref(imgElRef);
if (!imgEl) return;
cropper.value = new Cropper(imgEl, {
...defaultOptions,
ready: () => {
isReady.value = true;
realTimeCroppered();
delay(400).then(() => emit("readied", cropper.value));
},
crop() {
debounceRealTimeCroppered();
},
zoom() {
debounceRealTimeCroppered();
},
cropmove() {
debounceRealTimeCroppered();
},
...props.options
});
}
function realTimeCroppered() {
props.realTimePreview && croppered();
}
function croppered() {
if (!cropper.value) return;
const canvas = inCircled.value
? getRoundedCanvas()
: cropper.value.getCroppedCanvas();
// https://developer.mozilla.org/zh-CN/docs/Web/API/HTMLCanvasElement/toBlob
canvas.toBlob(blob => {
if (!blob) return;
const fileReader: FileReader = new FileReader();
fileReader.readAsDataURL(blob);
fileReader.onloadend = e => {
if (!e.target?.result || !blob) return;
imgBase64.value = e.target.result;
emit("cropper", {
base64: e.target.result,
blob,
info: { size: blob.size, ...cropper.value.getData() }
});
};
fileReader.onerror = () => {
emit("error");
};
});
}
function getRoundedCanvas() {
const sourceCanvas = cropper.value!.getCroppedCanvas();
const canvas = document.createElement("canvas");
const context = canvas.getContext("2d")!;
const width = sourceCanvas.width;
const height = sourceCanvas.height;
canvas.width = width;
canvas.height = height;
context.imageSmoothingEnabled = true;
context.drawImage(sourceCanvas, 0, 0, width, height);
context.globalCompositeOperation = "destination-in";
context.beginPath();
context.arc(
width / 2,
height / 2,
Math.min(width, height) / 2,
0,
2 * Math.PI,
true
);
context.fill();
return canvas;
}
function handCropper(event: string, arg?: number | Array<number>) {
if (event === "scaleX") {
scaleX = arg = scaleX === -1 ? 1 : -1;
}
if (event === "scaleY") {
scaleY = arg = scaleY === -1 ? 1 : -1;
}
arg && isArray(arg)
? cropper.value?.[event]?.(...arg)
: cropper.value?.[event]?.(arg);
}
function beforeUpload(file) {
const reader = new FileReader();
reader.readAsDataURL(file);
inSrc.value = "";
reader.onload = e => {
inSrc.value = e.target?.result as string;
};
reader.onloadend = () => {
init();
};
return false;
}
const menuContent = defineComponent({
directives: {
tippy,
longpress
},
setup() {
return () => (
<div class="flex flex-wrap w-[60px] justify-between">
<ElUpload
accept="image/*"
show-file-list={false}
before-upload={beforeUpload}
>
<Upload
class={iconClass.value}
v-tippy={{
content: "上传",
placement: "left-start"
}}
/>
</ElUpload>
<DownloadIcon
class={iconClass.value}
v-tippy={{
content: "下载",
placement: "right-start"
}}
onClick={() => downloadByBase64(imgBase64.value, "cropping.png")}
/>
<ChangeIcon
class={iconClass.value}
v-tippy={{
content: "圆形、矩形裁剪",
placement: "left-start"
}}
onClick={() => {
inCircled.value = !inCircled.value;
realTimeCroppered();
}}
/>
<Reload
class={iconClass.value}
v-tippy={{
content: "重置",
placement: "right-start"
}}
onClick={() => handCropper("reset")}
/>
<ArrowUp
class={iconClass.value}
v-tippy={{
content: "上移(可长按)",
placement: "left-start"
}}
v-longpress={[() => handCropper("move", [0, -10]), "0:100"]}
/>
<ArrowDown
class={iconClass.value}
v-tippy={{
content: "下移(可长按)",
placement: "right-start"
}}
v-longpress={[() => handCropper("move", [0, 10]), "0:100"]}
/>
<ArrowLeft
class={iconClass.value}
v-tippy={{
content: "左移(可长按)",
placement: "left-start"
}}
v-longpress={[() => handCropper("move", [-10, 0]), "0:100"]}
/>
<ArrowRight
class={iconClass.value}
v-tippy={{
content: "右移(可长按)",
placement: "right-start"
}}
v-longpress={[() => handCropper("move", [10, 0]), "0:100"]}
/>
<ArrowH
class={iconClass.value}
v-tippy={{
content: "水平翻转",
placement: "left-start"
}}
onClick={() => handCropper("scaleX", -1)}
/>
<ArrowV
class={iconClass.value}
v-tippy={{
content: "垂直翻转",
placement: "right-start"
}}
onClick={() => handCropper("scaleY", -1)}
/>
<RotateLeft
class={iconClass.value}
v-tippy={{
content: "逆时针旋转",
placement: "left-start"
}}
onClick={() => handCropper("rotate", -45)}
/>
<RotateRight
class={iconClass.value}
v-tippy={{
content: "顺时针旋转",
placement: "right-start"
}}
onClick={() => handCropper("rotate", 45)}
/>
<SearchPlus
class={iconClass.value}
v-tippy={{
content: "放大(可长按)",
placement: "left-start"
}}
v-longpress={[() => handCropper("zoom", 0.1), "0:100"]}
/>
<SearchMinus
class={iconClass.value}
v-tippy={{
content: "缩小(可长按)",
placement: "right-start"
}}
v-longpress={[() => handCropper("zoom", -0.1), "0:100"]}
/>
</div>
);
}
});
function onContextmenu(event) {
event.preventDefault();
const { show, setProps, destroy, state } = useTippy(tippyElRef, {
content: menuContent,
arrow: false,
theme: "light",
trigger: "manual",
interactive: true,
appendTo: "parent",
// hideOnClick: false,
placement: "bottom-end"
});
setProps({
getReferenceClientRect: () => ({
width: 0,
height: 0,
top: event.clientY,
bottom: event.clientY,
left: event.clientX,
right: event.clientX
})
});
show();
if (isInClose.value) {
if (!state.value.isShown && !state.value.isVisible) return;
useEventListener(tippyElRef, "click", destroy);
}
}
return {
inSrc,
props,
imgElRef,
tippyElRef,
getClass,
getWrapperStyle,
getImageStyle,
isReady,
croppered,
onContextmenu
};
},
render() {
const {
inSrc,
isReady,
getClass,
getImageStyle,
onContextmenu,
getWrapperStyle
} = this;
const { alt, crossorigin } = this.props;
return inSrc ? (
<div
ref="tippyElRef"
class={getClass}
style={getWrapperStyle}
onContextmenu={event => onContextmenu(event)}
>
<img
v-show={isReady}
ref="imgElRef"
style={getImageStyle}
src={inSrc}
alt={alt}
crossorigin={crossorigin}
/>
</div>
) : null;
}
});

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 1024 1024"><path fill="currentColor" d="M862 465.3h-81c-4.6 0-9 2-12.1 5.5L550 723.1V160c0-4.4-3.6-8-8-8h-60c-4.4 0-8 3.6-8 8v563.1L255.1 470.8c-3-3.5-7.4-5.5-12.1-5.5h-81c-6.8 0-10.5 8.1-6 13.2L487.9 861a31.96 31.96 0 0 0 48.3 0L868 478.5c4.5-5.2.8-13.2-6-13.2"/></svg>

After

Width:  |  Height:  |  Size: 346 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" class="icon" viewBox="0 0 1024 1024"><path d="m296.992 216.992-272 272L3.008 512l21.984 23.008 272 272 46.016-46.016L126.016 544h772L680.992 760.992l46.016 46.016 272-272L1020.992 512l-21.984-23.008-272-272-46.048 46.048L898.016 480h-772l216.96-216.992z"/></svg>

After

Width:  |  Height:  |  Size: 325 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 1024 1024"><path fill="currentColor" d="M872 474H286.9l350.2-304c5.6-4.9 2.2-14-5.2-14h-88.5c-3.9 0-7.6 1.4-10.5 3.9L155 487.8a31.96 31.96 0 0 0 0 48.3L535.1 866c1.5 1.3 3.3 2 5.2 2h91.5c7.4 0 10.8-9.2 5.2-14L286.9 550H872c4.4 0 8-3.6 8-8v-60c0-4.4-3.6-8-8-8"/></svg>

After

Width:  |  Height:  |  Size: 343 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 1024 1024"><path fill="currentColor" d="M869 487.8 491.2 159.9c-2.9-2.5-6.6-3.9-10.5-3.9h-88.5c-7.4 0-10.8 9.2-5.2 14l350.2 304H152c-4.4 0-8 3.6-8 8v60c0 4.4 3.6 8 8 8h585.1L386.9 854c-5.6 4.9-2.2 14 5.2 14h91.5c1.9 0 3.8-.7 5.2-2L869 536.2a32.07 32.07 0 0 0 0-48.4"/></svg>

After

Width:  |  Height:  |  Size: 350 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 1024 1024"><path fill="currentColor" d="M868 545.5 536.1 163a31.96 31.96 0 0 0-48.3 0L156 545.5a7.97 7.97 0 0 0 6 13.2h81c4.6 0 9-2 12.1-5.5L474 300.9V864c0 4.4 3.6 8 8 8h60c4.4 0 8-3.6 8-8V300.9l218.9 252.3c3 3.5 7.4 5.5 12.1 5.5h81c6.8 0 10.5-8 6-13.2"/></svg>

After

Width:  |  Height:  |  Size: 338 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" class="icon" viewBox="0 0 1024 1024"><path d="m512 67.008-23.008 21.984-256 256 46.048 46.048L480 190.016v644L279.008 632.96l-46.048 46.08 256 256 23.008 21.984 23.008-21.984 256-256-46.016-46.016L544 834.016v-644l200.992 200.96 46.016-45.984-256-256z"/></svg>

After

Width:  |  Height:  |  Size: 323 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" class="icon" viewBox="0 0 1024 1024"><path d="M956.8 988.8H585.6c-16 0-25.6-9.6-25.6-28.8V576c0-16 9.6-28.8 25.6-28.8h371.2c16 0 25.6 9.6 25.6 28.8v384c0 16-9.6 28.8-25.6 28.8M608 937.6h326.4V598.4H608zm-121.6 44.8C262.4 982.4 144 848 144 595.2c0-19.2 9.6-28.8 25.6-28.8s25.6 12.8 25.6 28.8c0 220.8 96 326.4 288 326.4 16 0 25.6 12.8 25.6 28.8s-6.4 32-22.4 32"/><path d="M262.4 694.4c-6.4 0-9.6-3.2-16-6.4L160 601.6c-9.6-9.6-9.6-22.4 0-28.8s22.4-9.6 28.8 0l86.4 86.4c9.6 9.6 9.6 22.4 0 28.8-3.2 3.2-6.4 6.4-12.8 6.4"/><path d="M86.4 694.4c-6.4 0-9.6-3.2-16-6.4-9.6-9.6-9.6-22.4 0-28.8l86.4-86.4c9.6-9.6 22.4-9.6 28.8 0 9.6 9.6 9.6 22.4 0 28.8L99.2 688c-3.2 3.2-6.4 6.4-12.8 6.4m790.4-249.6c-16 0-28.8-12.8-28.8-32 0-224-99.2-336-300.8-336-16 0-28.8-12.8-28.8-32s9.6-32 28.8-32c233.6 0 355.2 137.6 355.2 396.8 0 22.4-9.6 35.2-25.6 35.2"/><path d="M876.8 448c-6.4 0-9.6-3.2-16-6.4l-86.4-86.4c-9.6-9.6-9.6-22.4 0-28.8s22.4-9.6 28.8 0l86.4 86.4c9.6 9.6 9.6 22.4 0 28.8 0 3.2-6.4 6.4-12.8 6.4"/><path d="M876.8 448c-6.4 0-9.6-3.2-16-6.4-9.6-9.6-9.6-22.4 0-28.8l86.4-86.4c9.6-9.6 22.4-9.6 28.8 0s9.6 22.4 0 28.8l-86.4 86.4c-3.2 3.2-6.4 6.4-12.8 6.4M288 524.8C156.8 524.8 48 416 48 278.4S156.8 35.2 288 35.2 528 144 528 281.6 419.2 524.8 288 524.8m-3.2-432c-99.2 0-179.2 83.2-179.2 185.6S185.6 464 284.8 464 464 380.8 464 278.4 384 92.8 284.8 92.8"/></svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 1024 1024"><path fill="currentColor" d="M505.7 661a8 8 0 0 0 12.6 0l112-141.7c4.1-5.2.4-12.9-6.3-12.9h-74.1V168c0-4.4-3.6-8-8-8h-60c-4.4 0-8 3.6-8 8v338.3H400c-6.7 0-10.4 7.7-6.3 12.9zM878 626h-60c-4.4 0-8 3.6-8 8v154H214V634c0-4.4-3.6-8-8-8h-60c-4.4 0-8 3.6-8 8v198c0 17.7 14.3 32 32 32h684c17.7 0 32-14.3 32-32V634c0-4.4-3.6-8-8-8"/></svg>

After

Width:  |  Height:  |  Size: 417 B

View File

@@ -0,0 +1,31 @@
import Reload from "./reload.svg?component";
import Upload from "./upload.svg?component";
import ArrowH from "./arrow-h.svg?component";
import ArrowV from "./arrow-v.svg?component";
import ArrowUp from "./arrow-up.svg?component";
import ChangeIcon from "./change.svg?component";
import ArrowDown from "./arrow-down.svg?component";
import ArrowLeft from "./arrow-left.svg?component";
import DownloadIcon from "./download.svg?component";
import ArrowRight from "./arrow-right.svg?component";
import RotateLeft from "./rotate-left.svg?component";
import SearchPlus from "./search-plus.svg?component";
import RotateRight from "./rotate-right.svg?component";
import SearchMinus from "./search-minus.svg?component";
export {
Reload,
Upload,
ArrowH,
ArrowV,
ArrowUp,
ArrowDown,
ArrowLeft,
ChangeIcon,
ArrowRight,
RotateLeft,
SearchPlus,
RotateRight,
SearchMinus,
DownloadIcon
};

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 1024 1024"><path fill="currentColor" d="M168 504.2c1-43.7 10-86.1 26.9-126 17.3-41 42.1-77.7 73.7-109.4S337 212.3 378 195c42.4-17.9 87.4-27 133.9-27s91.5 9.1 133.8 27A341.5 341.5 0 0 1 755 268.8c9.9 9.9 19.2 20.4 27.8 31.4l-60.2 47a8 8 0 0 0 3 14.1l175.7 43c5 1.2 9.9-2.6 9.9-7.7l.8-180.9c0-6.7-7.7-10.5-12.9-6.3l-56.4 44.1C765.8 155.1 646.2 92 511.8 92 282.7 92 96.3 275.6 92 503.8a8 8 0 0 0 8 8.2h60c4.4 0 7.9-3.5 8-7.8m756 7.8h-60c-4.4 0-7.9 3.5-8 7.8-1 43.7-10 86.1-26.9 126-17.3 41-42.1 77.8-73.7 109.4A342.45 342.45 0 0 1 512.1 856a342.24 342.24 0 0 1-243.2-100.8c-9.9-9.9-19.2-20.4-27.8-31.4l60.2-47a8 8 0 0 0-3-14.1l-175.7-43c-5-1.2-9.9 2.6-9.9 7.7l-.7 181c0 6.7 7.7 10.5 12.9 6.3l56.4-44.1C258.2 868.9 377.8 932 512.2 932c229.2 0 415.5-183.7 419.8-411.8a8 8 0 0 0-8-8.2"/></svg>

After

Width:  |  Height:  |  Size: 863 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 1024 1024"><path fill="currentColor" d="M672 418H144c-17.7 0-32 14.3-32 32v414c0 17.7 14.3 32 32 32h528c17.7 0 32-14.3 32-32V450c0-17.7-14.3-32-32-32m-44 402H188V494h440z"/><path fill="currentColor" d="M819.3 328.5c-78.8-100.7-196-153.6-314.6-154.2l-.2-64c0-6.5-7.6-10.1-12.6-6.1l-128 101c-4 3.1-3.9 9.1 0 12.3L492 318.6c5.1 4 12.7.4 12.6-6.1v-63.9c12.9.1 25.9.9 38.8 2.5 42.1 5.2 82.1 18.2 119 38.7 38.1 21.2 71.2 49.7 98.4 84.3 27.1 34.7 46.7 73.7 58.1 115.8 11 40.7 14 82.7 8.9 124.8-.7 5.4-1.4 10.8-2.4 16.1h74.9c14.8-103.6-11.3-213-81-302.3"/></svg>

After

Width:  |  Height:  |  Size: 630 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 1024 1024"><path fill="currentColor" d="M480.5 251.2c13-1.6 25.9-2.4 38.8-2.5v63.9c0 6.5 7.5 10.1 12.6 6.1L660 217.6c4-3.2 4-9.2 0-12.3l-128-101c-5.1-4-12.6-.4-12.6 6.1l-.2 64c-118.6.5-235.8 53.4-314.6 154.2-69.6 89.2-95.7 198.6-81.1 302.4h74.9c-.9-5.3-1.7-10.7-2.4-16.1-5.1-42.1-2.1-84.1 8.9-124.8 11.4-42.2 31-81.1 58.1-115.8 27.2-34.7 60.3-63.2 98.4-84.3 37-20.6 76.9-33.6 119.1-38.8"/><path fill="currentColor" d="M880 418H352c-17.7 0-32 14.3-32 32v414c0 17.7 14.3 32 32 32h528c17.7 0 32-14.3 32-32V450c0-17.7-14.3-32-32-32m-44 402H396V494h440z"/></svg>

After

Width:  |  Height:  |  Size: 633 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 1024 1024"><path fill="currentColor" d="M637 443H325c-4.4 0-8 3.6-8 8v60c0 4.4 3.6 8 8 8h312c4.4 0 8-3.6 8-8v-60c0-4.4-3.6-8-8-8m284 424L775 721c122.1-148.9 113.6-369.5-26-509-148-148.1-388.4-148.1-537 0-148.1 148.6-148.1 389 0 537 139.5 139.6 360.1 148.1 509 26l146 146c3.2 2.8 8.3 2.8 11 0l43-43c2.8-2.7 2.8-7.8 0-11M696 696c-118.8 118.7-311.2 118.7-430 0-118.7-118.8-118.7-311.2 0-430 118.8-118.7 311.2-118.7 430 0 118.7 118.8 118.7 311.2 0 430"/></svg>

After

Width:  |  Height:  |  Size: 532 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 1024 1024"><path fill="currentColor" d="M637 443H519V309c0-4.4-3.6-8-8-8h-60c-4.4 0-8 3.6-8 8v134H325c-4.4 0-8 3.6-8 8v60c0 4.4 3.6 8 8 8h118v134c0 4.4 3.6 8 8 8h60c4.4 0 8-3.6 8-8V519h118c4.4 0 8-3.6 8-8v-60c0-4.4-3.6-8-8-8m284 424L775 721c122.1-148.9 113.6-369.5-26-509-148-148.1-388.4-148.1-537 0-148.1 148.6-148.1 389 0 537 139.5 139.6 360.1 148.1 509 26l146 146c3.2 2.8 8.3 2.8 11 0l43-43c2.8-2.7 2.8-7.8 0-11M696 696c-118.8 118.7-311.2 118.7-430 0-118.7-118.8-118.7-311.2 0-430 118.8-118.7 311.2-118.7 430 0 118.7 118.8 118.7 311.2 0 430"/></svg>

After

Width:  |  Height:  |  Size: 628 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 1024 1024"><path fill="currentColor" d="M400 317.7h73.9V656c0 4.4 3.6 8 8 8h60c4.4 0 8-3.6 8-8V317.7H624c6.7 0 10.4-7.7 6.3-12.9L518.3 163a8 8 0 0 0-12.6 0l-112 141.7c-4.1 5.3-.4 13 6.3 13M878 626h-60c-4.4 0-8 3.6-8 8v154H214V634c0-4.4-3.6-8-8-8h-60c-4.4 0-8 3.6-8 8v198c0 17.7 14.3 32 32 32h684c17.7 0 32-14.3 32-32V634c0-4.4-3.6-8-8-8"/></svg>

After

Width:  |  Height:  |  Size: 421 B

View File

@@ -0,0 +1,7 @@
import reCropperPreview from "./src/index.vue";
import { withInstall } from "@pureadmin/utils";
/** 图片裁剪预览组件 */
export const ReCropperPreview = withInstall(reCropperPreview);
export default ReCropperPreview;

View File

@@ -0,0 +1,76 @@
<script setup lang="tsx">
import { ref } from "vue";
import ReCropper from "@/components/ReCropper";
import { formatBytes } from "@pureadmin/utils";
defineOptions({
name: "ReCropperPreview"
});
defineProps({
imgSrc: String
});
const emit = defineEmits(["cropper"]);
const infos = ref();
const popoverRef = ref();
const refCropper = ref();
const showPopover = ref(false);
const cropperImg = ref<string>("");
function onCropper({ base64, blob, info }) {
infos.value = info;
cropperImg.value = base64;
emit("cropper", { base64, blob, info });
}
function hidePopover() {
popoverRef.value.hide();
}
defineExpose({ hidePopover });
</script>
<template>
<div v-loading="!showPopover" element-loading-background="transparent">
<el-popover
ref="popoverRef"
:visible="showPopover"
placement="right"
width="18vw"
>
<template #reference>
<div class="w-[18vw]">
<ReCropper
ref="refCropper"
:src="imgSrc"
circled
@cropper="onCropper"
@readied="showPopover = true"
/>
<p v-show="showPopover" class="mt-1 text-center">
温馨提示右键上方裁剪区可开启功能菜单
</p>
</div>
</template>
<div class="flex flex-wrap justify-center items-center text-center">
<el-image
v-if="cropperImg"
:src="cropperImg"
:preview-src-list="Array.of(cropperImg)"
fit="cover"
/>
<div v-if="infos" class="mt-1">
<p>
图像大小{{ parseInt(infos.width) }} ×
{{ parseInt(infos.height) }}像素
</p>
<p>
文件大小{{ formatBytes(infos.size) }}{{ infos.size }} 字节
</p>
</div>
</div>
</el-popover>
</div>
</template>

View File

@@ -0,0 +1,69 @@
import { ref } from "vue";
import reDialog from "./index.vue";
import { useTimeoutFn } from "@vueuse/core";
import { withInstall } from "@pureadmin/utils";
import type {
EventType,
ArgsType,
DialogProps,
ButtonProps,
DialogOptions
} from "./type";
const dialogStore = ref<Array<DialogOptions>>([]);
/** 打开弹框 */
const addDialog = (options: DialogOptions) => {
const open = () =>
dialogStore.value.push(Object.assign(options, { visible: true }));
if (options?.openDelay) {
useTimeoutFn(() => {
open();
}, options.openDelay);
} else {
open();
}
};
/** 关闭弹框 */
const closeDialog = (options: DialogOptions, index: number, args?: any) => {
dialogStore.value[index].visible = false;
options.closeCallBack && options.closeCallBack({ options, index, args });
const closeDelay = options?.closeDelay ?? 200;
useTimeoutFn(() => {
dialogStore.value.splice(index, 1);
}, closeDelay);
};
/**
* @description 更改弹框自身属性值
* @param value 属性值
* @param key 属性,默认`title`
* @param index 弹框索引(默认`0`,代表只有一个弹框,对于嵌套弹框要改哪个弹框的属性值就把该弹框索引赋给`index`
*/
const updateDialog = (value: any, key = "title", index = 0) => {
dialogStore.value[index][key] = value;
};
/** 关闭所有弹框 */
const closeAllDialog = () => {
dialogStore.value = [];
};
/** 千万别忘了在下面这三处引入并注册下,放心注册,不使用`addDialog`调用就不会被挂载
* https://github.com/pure-admin/vue-pure-admin/blob/main/src/App.vue#L4
* https://github.com/pure-admin/vue-pure-admin/blob/main/src/App.vue#L12
* https://github.com/pure-admin/vue-pure-admin/blob/main/src/App.vue#L22
*/
const ReDialog = withInstall(reDialog);
export type { EventType, ArgsType, DialogProps, ButtonProps, DialogOptions };
export {
ReDialog,
dialogStore,
addDialog,
closeDialog,
updateDialog,
closeAllDialog
};

View File

@@ -0,0 +1,206 @@
<script setup lang="ts">
import {
type EventType,
type ButtonProps,
type DialogOptions,
closeDialog,
dialogStore
} from "./index";
import { ref, computed } from "vue";
import { isFunction } from "@pureadmin/utils";
import Fullscreen from "@iconify-icons/ri/fullscreen-fill";
import ExitFullscreen from "@iconify-icons/ri/fullscreen-exit-fill";
defineOptions({
name: "ReDialog"
});
const sureBtnMap = ref({});
const fullscreen = ref(false);
const footerButtons = computed(() => {
return (options: DialogOptions) => {
return options?.footerButtons?.length > 0
? options.footerButtons
: ([
{
label: "取消",
text: true,
bg: true,
btnClick: ({ dialog: { options, index } }) => {
const done = () =>
closeDialog(options, index, { command: "cancel" });
if (options?.beforeCancel && isFunction(options?.beforeCancel)) {
options.beforeCancel(done, { options, index });
} else {
done();
}
}
},
{
label: "确定",
type: "primary",
text: true,
bg: true,
popconfirm: options?.popconfirm,
btnClick: ({ dialog: { options, index } }) => {
if (options?.sureBtnLoading) {
sureBtnMap.value[index] = Object.assign(
{},
sureBtnMap.value[index],
{
loading: true
}
);
}
const closeLoading = () => {
if (options?.sureBtnLoading) {
sureBtnMap.value[index].loading = false;
}
};
const done = () => {
closeLoading();
closeDialog(options, index, { command: "sure" });
};
if (options?.beforeSure && isFunction(options?.beforeSure)) {
options.beforeSure(done, { options, index, closeLoading });
} else {
done();
}
}
}
] as Array<ButtonProps>);
};
});
const fullscreenClass = computed(() => {
return [
"el-icon",
"el-dialog__close",
"-translate-x-2",
"cursor-pointer",
"hover:!text-[red]"
];
});
function eventsCallBack(
event: EventType,
options: DialogOptions,
index: number,
isClickFullScreen = false
) {
if (!isClickFullScreen) fullscreen.value = options?.fullscreen ?? false;
if (options?.[event] && isFunction(options?.[event])) {
return options?.[event]({ options, index });
}
}
function handleClose(
options: DialogOptions,
index: number,
args = { command: "close" }
) {
closeDialog(options, index, args);
eventsCallBack("close", options, index);
}
</script>
<template>
<el-dialog
v-for="(options, index) in dialogStore"
:key="index"
v-bind="options"
v-model="options.visible"
class="pure-dialog"
:fullscreen="fullscreen ? true : options?.fullscreen ? true : false"
@closed="handleClose(options, index)"
@opened="eventsCallBack('open', options, index)"
@openAutoFocus="eventsCallBack('openAutoFocus', options, index)"
@closeAutoFocus="eventsCallBack('closeAutoFocus', options, index)"
>
<!-- header -->
<template
v-if="options?.fullscreenIcon || options?.headerRenderer"
#header="{ close, titleId, titleClass }"
>
<div
v-if="options?.fullscreenIcon"
class="flex items-center justify-between"
>
<span :id="titleId" :class="titleClass">{{ options?.title }}</span>
<i
v-if="!options?.fullscreen"
:class="fullscreenClass"
@click="
() => {
fullscreen = !fullscreen;
eventsCallBack(
'fullscreenCallBack',
{ ...options, fullscreen },
index,
true
);
}
"
>
<IconifyIconOffline
class="pure-dialog-svg"
:icon="
options?.fullscreen
? ExitFullscreen
: fullscreen
? ExitFullscreen
: Fullscreen
"
/>
</i>
</div>
<component
:is="options?.headerRenderer({ close, titleId, titleClass })"
v-else
/>
</template>
<component
v-bind="options?.props"
:is="options.contentRenderer({ options, index })"
@close="args => handleClose(options, index, args)"
/>
<!-- footer -->
<template v-if="!options?.hideFooter" #footer>
<template v-if="options?.footerRenderer">
<component :is="options?.footerRenderer({ options, index })" />
</template>
<span v-else>
<template v-for="(btn, key) in footerButtons(options)" :key="key">
<el-popconfirm
v-if="btn.popconfirm"
v-bind="btn.popconfirm"
@confirm="
btn.btnClick({
dialog: { options, index },
button: { btn, index: key }
})
"
>
<template #reference>
<el-button v-bind="btn">{{ btn?.label }}</el-button>
</template>
</el-popconfirm>
<el-button
v-else
v-bind="btn"
:loading="key === 1 && sureBtnMap[index]?.loading"
@click="
btn.btnClick({
dialog: { options, index },
button: { btn, index: key }
})
"
>
{{ btn?.label }}
</el-button>
</template>
</span>
</template>
</el-dialog>
</template>

View File

@@ -0,0 +1,275 @@
import type { CSSProperties, VNode, Component } from "vue";
type DoneFn = (cancel?: boolean) => void;
type EventType =
| "open"
| "close"
| "openAutoFocus"
| "closeAutoFocus"
| "fullscreenCallBack";
type ArgsType = {
/** `cancel` 点击取消按钮、`sure` 点击确定按钮、`close` 点击右上角关闭按钮或空白页或按下了esc键 */
command: "cancel" | "sure" | "close";
};
type ButtonType =
| "primary"
| "success"
| "warning"
| "danger"
| "info"
| "text";
/** https://element-plus.org/zh-CN/component/dialog.html#attributes */
type DialogProps = {
/** `Dialog` 的显示与隐藏 */
visible?: boolean;
/** `Dialog` 的标题 */
title?: string;
/** `Dialog` 的宽度,默认 `50%` */
width?: string | number;
/** 是否为全屏 `Dialog`(会一直处于全屏状态,除非弹框关闭),默认 `false``fullscreen` 和 `fullscreenIcon` 都传时只有 `fullscreen` 会生效 */
fullscreen?: boolean;
/** 是否显示全屏操作图标,默认 `false``fullscreen` 和 `fullscreenIcon` 都传时只有 `fullscreen` 会生效 */
fullscreenIcon?: boolean;
/** `Dialog CSS` 中的 `margin-top` 值,默认 `15vh` */
top?: string;
/** 是否需要遮罩层,默认 `true` */
modal?: boolean;
/** `Dialog` 自身是否插入至 `body` 元素上。嵌套的 `Dialog` 必须指定该属性并赋值为 `true`,默认 `false` */
appendToBody?: boolean;
/** 是否在 `Dialog` 出现时将 `body` 滚动锁定,默认 `true` */
lockScroll?: boolean;
/** `Dialog` 的自定义类名 */
class?: string;
/** `Dialog` 的自定义样式 */
style?: CSSProperties;
/** `Dialog` 打开的延时时间,单位毫秒,默认 `0` */
openDelay?: number;
/** `Dialog` 关闭的延时时间,单位毫秒,默认 `0` */
closeDelay?: number;
/** 是否可以通过点击 `modal` 关闭 `Dialog`,默认 `true` */
closeOnClickModal?: boolean;
/** 是否可以通过按下 `ESC` 关闭 `Dialog`,默认 `true` */
closeOnPressEscape?: boolean;
/** 是否显示关闭按钮,默认 `true` */
showClose?: boolean;
/** 关闭前的回调,会暂停 `Dialog` 的关闭. 回调函数内执行 `done` 参数方法的时候才是真正关闭对话框的时候 */
beforeClose?: (done: DoneFn) => void;
/** 为 `Dialog` 启用可拖拽功能,默认 `false` */
draggable?: boolean;
/** 是否让 `Dialog` 的 `header` 和 `footer` 部分居中排列,默认 `false` */
center?: boolean;
/** 是否水平垂直对齐对话框,默认 `false` */
alignCenter?: boolean;
/** 当关闭 `Dialog` 时,销毁其中的元素,默认 `false` */
destroyOnClose?: boolean;
};
//element-plus.org/zh-CN/component/popconfirm.html#attributes
type Popconfirm = {
/** 标题 */
title?: string;
/** 确定按钮文字 */
confirmButtonText?: string;
/** 取消按钮文字 */
cancelButtonText?: string;
/** 确定按钮类型,默认 `primary` */
confirmButtonType?: ButtonType;
/** 取消按钮类型,默认 `text` */
cancelButtonType?: ButtonType;
/** 自定义图标,默认 `QuestionFilled` */
icon?: string | Component;
/** `Icon` 颜色,默认 `#f90` */
iconColor?: string;
/** 是否隐藏 `Icon`,默认 `false` */
hideIcon?: boolean;
/** 关闭时的延迟,默认 `200` */
hideAfter?: number;
/** 是否将 `popover` 的下拉列表插入至 `body` 元素,默认 `true` */
teleported?: boolean;
/** 当 `popover` 组件长时间不触发且 `persistent` 属性设置为 `false` 时, `popover` 将会被删除,默认 `false` */
persistent?: boolean;
/** 弹层宽度,最小宽度 `150px`,默认 `150` */
width?: string | number;
};
type BtnClickDialog = {
options?: DialogOptions;
index?: number;
};
type BtnClickButton = {
btn?: ButtonProps;
index?: number;
};
/** https://element-plus.org/zh-CN/component/button.html#button-attributes */
type ButtonProps = {
/** 按钮文字 */
label: string;
/** 按钮尺寸 */
size?: "large" | "default" | "small";
/** 按钮类型 */
type?: "primary" | "success" | "warning" | "danger" | "info";
/** 是否为朴素按钮,默认 `false` */
plain?: boolean;
/** 是否为文字按钮,默认 `false` */
text?: boolean;
/** 是否显示文字按钮背景颜色,默认 `false` */
bg?: boolean;
/** 是否为链接按钮,默认 `false` */
link?: boolean;
/** 是否为圆角按钮,默认 `false` */
round?: boolean;
/** 是否为圆形按钮,默认 `false` */
circle?: boolean;
/** 确定按钮的 `Popconfirm` 气泡确认框相关配置 */
popconfirm?: Popconfirm;
/** 是否为加载中状态,默认 `false` */
loading?: boolean;
/** 自定义加载中状态图标组件 */
loadingIcon?: string | Component;
/** 按钮是否为禁用状态,默认 `false` */
disabled?: boolean;
/** 图标组件 */
icon?: string | Component;
/** 是否开启原生 `autofocus` 属性,默认 `false` */
autofocus?: boolean;
/** 原生 `type` 属性,默认 `button` */
nativeType?: "button" | "submit" | "reset";
/** 自动在两个中文字符之间插入空格 */
autoInsertSpace?: boolean;
/** 自定义按钮颜色, 并自动计算 `hover` 和 `active` 触发后的颜色 */
color?: string;
/** `dark` 模式, 意味着自动设置 `color` 为 `dark` 模式的颜色,默认 `false` */
dark?: boolean;
/** 自定义元素标签 */
tag?: string | Component;
/** 点击按钮后触发的回调 */
btnClick?: ({
dialog,
button
}: {
/** 当前 `Dialog` 信息 */
dialog: BtnClickDialog;
/** 当前 `button` 信息 */
button: BtnClickButton;
}) => void;
};
interface DialogOptions extends DialogProps {
/** 内容区组件的 `props`,可通过 `defineProps` 接收 */
props?: any;
/** 是否隐藏 `Dialog` 按钮操作区的内容 */
hideFooter?: boolean;
/** 确定按钮的 `Popconfirm` 气泡确认框相关配置 */
popconfirm?: Popconfirm;
/** 点击确定按钮后是否开启 `loading` 加载动画 */
sureBtnLoading?: boolean;
/**
* @description 自定义对话框标题的内容渲染器
* @see {@link https://element-plus.org/zh-CN/component/dialog.html#%E8%87%AA%E5%AE%9A%E4%B9%89%E5%A4%B4%E9%83%A8}
*/
headerRenderer?: ({
close,
titleId,
titleClass
}: {
close: Function;
titleId: string;
titleClass: string;
}) => VNode | Component;
/** 自定义内容渲染器 */
contentRenderer?: ({
options,
index
}: {
options: DialogOptions;
index: number;
}) => VNode | Component;
/** 自定义按钮操作区的内容渲染器,会覆盖`footerButtons`以及默认的 `取消` 和 `确定` 按钮 */
footerRenderer?: ({
options,
index
}: {
options: DialogOptions;
index: number;
}) => VNode | Component;
/** 自定义底部按钮操作 */
footerButtons?: Array<ButtonProps>;
/** `Dialog` 打开后的回调 */
open?: ({
options,
index
}: {
options: DialogOptions;
index: number;
}) => void;
/** `Dialog` 关闭后的回调只有点击右上角关闭按钮或空白页或按下了esc键关闭页面时才会触发 */
close?: ({
options,
index
}: {
options: DialogOptions;
index: number;
}) => void;
/** `Dialog` 关闭后的回调。 `args` 返回的 `command` 值解析:`cancel` 点击取消按钮、`sure` 点击确定按钮、`close` 点击右上角关闭按钮或空白页或按下了esc键 */
closeCallBack?: ({
options,
index,
args
}: {
options: DialogOptions;
index: number;
args: any;
}) => void;
/** 点击全屏按钮时的回调 */
fullscreenCallBack?: ({
options,
index
}: {
options: DialogOptions;
index: number;
}) => void;
/** 输入焦点聚焦在 `Dialog` 内容时的回调 */
openAutoFocus?: ({
options,
index
}: {
options: DialogOptions;
index: number;
}) => void;
/** 输入焦点从 `Dialog` 内容失焦时的回调 */
closeAutoFocus?: ({
options,
index
}: {
options: DialogOptions;
index: number;
}) => void;
/** 点击底部取消按钮的回调,会暂停 `Dialog` 的关闭. 回调函数内执行 `done` 参数方法的时候才是真正关闭对话框的时候 */
beforeCancel?: (
done: Function,
{
options,
index
}: {
options: DialogOptions;
index: number;
}
) => void;
/** 点击底部确定按钮的回调,会暂停 `Dialog` 的关闭. 回调函数内执行 `done` 参数方法的时候才是真正关闭对话框的时候 */
beforeSure?: (
done: Function,
{
options,
index,
closeLoading
}: {
options: DialogOptions;
index: number;
/** 关闭确定按钮的 `loading` 加载动画 */
closeLoading: Function;
}
) => void;
}
export type { EventType, ArgsType, DialogProps, ButtonProps, DialogOptions };

View File

@@ -0,0 +1,39 @@
.point {
width: var(--point-width);
height: var(--point-height);
background: var(--point-background);
position: relative;
border-radius: var(--point-border-radius);
}
.point-flicker:after {
background: var(--point-background);
}
.point-flicker:before,
.point-flicker:after {
content: "";
width: 100%;
height: 100%;
top: 0;
left: 0;
position: absolute;
border-radius: var(--point-border-radius);
animation: flicker 1.2s ease-out infinite;
}
@keyframes flicker {
0% {
transform: scale(0.5);
opacity: 1;
}
30% {
opacity: 1;
}
100% {
transform: scale(var(--point-scale));
opacity: 0;
}
}

View File

@@ -0,0 +1,44 @@
import "./index.css";
import { type Component, h, defineComponent } from "vue";
export interface attrsType {
width?: string;
height?: string;
borderRadius?: number | string;
background?: string;
scale?: number | string;
}
/**
* 圆点、方形闪烁动画组件
* @param width 可选 string 宽
* @param height 可选 string 高
* @param borderRadius 可选 number | string 传0为方形、传50%或者不传为圆形
* @param background 可选 string 闪烁颜色
* @param scale 可选 number | string 闪烁范围默认2值越大闪烁范围越大
* @returns Component
*/
export function useRenderFlicker(attrs?: attrsType): Component {
return defineComponent({
name: "ReFlicker",
render() {
return h(
"div",
{
class: "point point-flicker",
style: {
"--point-width": attrs?.width ?? "12px",
"--point-height": attrs?.height ?? "12px",
"--point-background":
attrs?.background ?? "var(--el-color-primary)",
"--point-border-radius": attrs?.borderRadius ?? "50%",
"--point-scale": attrs?.scale ?? "2"
}
},
{
default: () => []
}
);
}
});
}

View File

@@ -0,0 +1,7 @@
import reFlop from "./src/index.vue";
import { withInstall } from "@pureadmin/utils";
/** 时间翻牌组件 */
export const ReFlop = withInstall(reFlop);
export default ReFlop;

View File

@@ -0,0 +1,184 @@
.m-flipper {
display: inline-block;
position: relative;
width: 60px;
height: 100px;
line-height: 100px;
border: solid 1px #000;
border-radius: 10px;
background: #fff;
font-size: 66px;
color: #fff;
box-shadow: 0 0 6px rgb(0 0 0 / 50%);
text-align: center;
font-family: "Helvetica Neue";
}
.m-flipper .digital::before,
.m-flipper .digital::after {
content: "";
position: absolute;
left: 0;
right: 0;
background: #000;
overflow: hidden;
box-sizing: border-box;
}
.m-flipper .digital::before {
top: 0;
bottom: 50%;
border-radius: 10px 10px 0 0;
border-bottom: solid 1px #666;
}
.m-flipper .digital::after {
top: 50%;
bottom: 0;
border-radius: 0 0 10px 10px;
line-height: 0;
}
/* 向下翻 */
.m-flipper.down .front::before {
z-index: 3;
}
.m-flipper.down .back::after {
z-index: 2;
transform-origin: 50% 0%;
transform: perspective(160px) rotateX(180deg);
}
.m-flipper.down .front::after,
.m-flipper.down .back::before {
z-index: 1;
}
.m-flipper.down.go .front::before {
transform-origin: 50% 100%;
animation: frontFlipDown 0.6s ease-in-out both;
box-shadow: 0 -2px 6px rgb(255 255 255 / 30%);
backface-visibility: hidden;
}
.m-flipper.down.go .back::after {
animation: backFlipDown 0.6s ease-in-out both;
}
/* 向上翻 */
.m-flipper.up .front::after {
z-index: 3;
}
.m-flipper.up .back::before {
z-index: 2;
transform-origin: 50% 100%;
transform: perspective(160px) rotateX(-180deg);
}
.m-flipper.up .front::before,
.m-flipper.up .back::after {
z-index: 1;
}
.m-flipper.up.go .front::after {
transform-origin: 50% 0;
animation: frontFlipUp 0.6s ease-in-out both;
box-shadow: 0 2px 6px rgb(255 255 255 / 30%);
backface-visibility: hidden;
}
.m-flipper.up.go .back::before {
animation: backFlipUp 0.6s ease-in-out both;
}
@keyframes frontFlipDown {
0% {
transform: perspective(160px) rotateX(0deg);
}
100% {
transform: perspective(160px) rotateX(-180deg);
}
}
@keyframes backFlipDown {
0% {
transform: perspective(160px) rotateX(180deg);
}
100% {
transform: perspective(160px) rotateX(0deg);
}
}
@keyframes frontFlipUp {
0% {
transform: perspective(160px) rotateX(0deg);
}
100% {
transform: perspective(160px) rotateX(180deg);
}
}
@keyframes backFlipUp {
0% {
transform: perspective(160px) rotateX(-180deg);
}
100% {
transform: perspective(160px) rotateX(0deg);
}
}
.m-flipper .number0::before,
.m-flipper .number0::after {
content: "0";
}
.m-flipper .number1::before,
.m-flipper .number1::after {
content: "1";
}
.m-flipper .number2::before,
.m-flipper .number2::after {
content: "2";
}
.m-flipper .number3::before,
.m-flipper .number3::after {
content: "3";
}
.m-flipper .number4::before,
.m-flipper .number4::after {
content: "4";
}
.m-flipper .number5::before,
.m-flipper .number5::after {
content: "5";
}
.m-flipper .number6::before,
.m-flipper .number6::after {
content: "6";
}
.m-flipper .number7::before,
.m-flipper .number7::after {
content: "7";
}
.m-flipper .number8::before,
.m-flipper .number8::after {
content: "8";
}
.m-flipper .number9::before,
.m-flipper .number9::after {
content: "9";
}

View File

@@ -0,0 +1,92 @@
import "./filpper.css";
import propTypes from "@/utils/propTypes";
import { defineComponent, ref } from "vue";
const props = {
// front paper text
// 前牌文字
frontText: propTypes.number.def(0),
// back paper text
// 后牌文字
backText: propTypes.number.def(1),
// flipping duration, please be consistent with the CSS animation-duration value.
// 翻牌动画时间与CSS中设置的animation-duration保持一致
duration: propTypes.number.def(600)
};
export default defineComponent({
name: "ReFlop",
props,
setup(props) {
const { frontText, backText, duration } = props;
const isFlipping = ref(false);
const flipType = ref("down");
const frontTextFromData = ref(frontText);
const backTextFromData = ref(backText);
const textClass = (number: number) => {
return "number" + number;
};
const flip = (type: string, front: number, back: number) => {
// 如果处于翻转中,则不执行
if (isFlipping.value) return false;
frontTextFromData.value = front;
backTextFromData.value = back;
// 根据传递过来的type设置翻转方向
flipType.value = type;
// 设置翻转状态为true
isFlipping.value = true;
setTimeout(() => {
// 设置翻转状态为false
isFlipping.value = false;
frontTextFromData.value = back;
}, duration);
};
// 下翻牌
const flipDown = (front: any, back: any): void => {
flip("down", front, back);
};
// 上翻牌
const flipUp = (front: any, back: any): void => {
flip("up", front, back);
};
// 设置前牌文字
function setFront(text: number): void {
frontTextFromData.value = text;
}
// 设置后牌文字
const setBack = (text: number): void => {
backTextFromData.value = text;
};
return {
flipType,
isFlipping,
frontTextFromData,
backTextFromData,
textClass,
flipDown,
flipUp,
setFront,
setBack
};
},
render() {
const main = `m-flipper ${this.flipType} ${this.isFlipping ? "go" : ""}`;
const front = `digital front ${this.textClass(this.frontTextFromData)}`;
const back = `digital back ${this.textClass(this.backTextFromData)}`;
return (
<div class={main}>
<div class={front} />
<div class={back} />
</div>
);
}
});

View File

@@ -0,0 +1,135 @@
<script setup lang="ts">
import flippers from "./filpper";
import { ref, unref, nextTick, onUnmounted } from "vue";
defineOptions({
name: "ReFlop"
});
const timer = ref(null);
const flipObjs = ref([]);
const flipperHour1 = ref();
const flipperHour2 = ref();
const flipperMinute1 = ref();
const flipperMinute2 = ref();
const flipperSecond1 = ref();
const flipperSecond2 = ref();
// 初始化数字
const init = () => {
const now = new Date();
const nowTimeStr = formatDate(new Date(now.getTime()), "hhiiss");
for (let i = 0; i < flipObjs.value.length; i++) {
flipObjs?.value[i]?.setFront(nowTimeStr[i]);
}
};
// 开始计时
const run = () => {
timer.value = setInterval(() => {
// 获取当前时间
const now = new Date();
const nowTimeStr = formatDate(new Date(now.getTime() - 1000), "hhiiss");
const nextTimeStr = formatDate(now, "hhiiss");
for (let i = 0; i < flipObjs.value.length; i++) {
if (nowTimeStr[i] === nextTimeStr[i]) {
continue;
}
flipObjs?.value[i]?.flipDown(nowTimeStr[i], nextTimeStr[i]);
}
}, 1000);
};
// 正则格式化日期
const formatDate = (date: Date, dateFormat: string) => {
/* 单独格式化年份根据y的字符数量输出年份
* 例如yyyy => 2019
yy => 19
y => 9
*/
if (/(y+)/.test(dateFormat)) {
dateFormat = dateFormat.replace(
RegExp.$1,
(date.getFullYear() + "").substr(4 - RegExp.$1.length)
);
}
// 格式化月、日、时、分、秒
const o = {
"m+": date.getMonth() + 1,
"d+": date.getDate(),
"h+": date.getHours(),
"i+": date.getMinutes(),
"s+": date.getSeconds()
};
for (const k in o) {
if (new RegExp(`(${k})`).test(dateFormat)) {
// 取出对应的值
const str = o[k] + "";
/* 根据设置的格式,输出对应的字符
* 例如: 早上8时hh => 08h => 8
* 但是,当数字>=10时无论格式为一位还是多位不做截取这是与年份格式化不一致的地方
* 例如: 下午15时hh => 15, h => 15
*/
dateFormat = dateFormat.replace(
RegExp.$1,
RegExp.$1.length === 1 ? str : padLeftZero(str)
);
}
}
return dateFormat;
};
// 日期时间补零
const padLeftZero = (str: string | any[]) => {
return ("00" + str).substr(str.length);
};
nextTick(() => {
flipObjs.value = [
unref(flipperHour1),
unref(flipperHour2),
unref(flipperMinute1),
unref(flipperMinute2),
unref(flipperSecond1),
unref(flipperSecond2)
];
init();
run();
});
onUnmounted(() => {
if (timer.value) {
clearInterval(timer.value);
timer.value = null;
}
});
</script>
<template>
<div class="flip-clock">
<flippers ref="flipperHour1" />
<flippers ref="flipperHour2" />
<em>:</em>
<flippers ref="flipperMinute1" />
<flippers ref="flipperMinute2" />
<em>:</em>
<flippers ref="flipperSecond1" />
<flippers ref="flipperSecond2" />
</div>
</template>
<style>
.flip-clock .m-flipper {
margin: 0 3px;
}
.flip-clock em {
display: inline-block;
font-size: 66px;
font-style: normal;
line-height: 102px;
vertical-align: top;
}
</style>

View File

@@ -0,0 +1,17 @@
import control from "./src/Control.vue";
import nodePanel from "./src/NodePanel.vue";
import dataDialog from "./src/DataDialog.vue";
import { withInstall } from "@pureadmin/utils";
/** LogicFlow流程图-控制面板 */
const Control = withInstall(control);
/** LogicFlow流程图-拖拽面板 */
const NodePanel = withInstall(nodePanel);
/** LogicFlow流程图-查看数据 */
const DataDialog = withInstall(dataDialog);
export { Control, NodePanel, DataDialog };
// LogicFlow流程图文档http://logic-flow.org/

View File

@@ -0,0 +1,147 @@
<script setup lang="ts">
import { ref, unref, onMounted } from "vue";
import { LogicFlow } from "@logicflow/core";
interface Props {
lf: LogicFlow;
catTurboData?: boolean;
}
const props = withDefaults(defineProps<Props>(), {
lf: null
});
const emit = defineEmits<{
(e: "catData"): void;
}>();
const controlButton3 = ref();
const controlButton4 = ref();
const focusIndex = ref<Number>(-1);
const titleLists = ref([
{
icon: "icon-zoom-out-hs",
text: "缩小",
size: "18",
disabled: false
},
{
icon: "icon-enlarge-hs",
text: "放大",
size: "18",
disabled: false
},
{
icon: "icon-full-screen-hs",
text: "适应",
size: "15",
disabled: false
},
{
icon: "icon-previous-hs",
text: "上一步",
size: "15",
disabled: true
},
{
icon: "icon-next-step-hs",
text: "下一步",
size: "17",
disabled: true
},
{
icon: "icon-download-hs",
text: "下载图片",
size: "17",
disabled: false
},
{
icon: "icon-watch-hs",
text: "查看数据",
size: "17",
disabled: false
}
]);
const onControl = (item, key) => {
["zoom", "zoom", "resetZoom", "undo", "redo", "getSnapshot"].forEach(
(v, i) => {
const domControl = props.lf;
if (key === 1) {
domControl.zoom(true);
}
if (key === 6) {
emit("catData");
}
if (key === i) {
domControl[v]();
}
}
);
};
const onEnter = key => {
focusIndex.value = key;
};
onMounted(() => {
props.lf.on("history:change", ({ data: { undoAble, redoAble } }) => {
unref(titleLists)[3].disabled = unref(controlButton3).disabled = !undoAble;
unref(titleLists)[4].disabled = unref(controlButton4).disabled = !redoAble;
});
});
</script>
<template>
<div class="control-container">
<!-- 功能按钮 -->
<ul>
<li
v-for="(item, key) in titleLists"
:key="key"
:title="item.text"
class="dark:text-bg_color"
@mouseenter.prevent="onEnter(key)"
@mouseleave.prevent="focusIndex = -1"
>
<button
:ref="'controlButton' + key"
v-tippy="{
content: item.text
}"
:disabled="item.disabled"
:style="{
cursor: item.disabled === false ? 'pointer' : 'not-allowed',
color: item.disabled === false ? '' : '#00000040',
background: 'transparent'
}"
@click="onControl(item, key)"
>
<span
:class="'iconfont ' + item.icon"
:style="{ fontSize: `${item.size}px` }"
/>
</button>
</li>
</ul>
</div>
</template>
<style scoped>
@import url("./assets/iconfont/iconfont.css");
.control-container {
background: hsl(0deg 0% 100% / 80%);
box-shadow: 0 1px 4px rgb(0 0 0 / 20%);
}
.control-container ul li {
margin: 10px;
text-align: center;
}
.control-container ul li span:hover {
color: var(--el-color-primary);
}
</style>

View File

@@ -0,0 +1,17 @@
<script setup lang="ts">
import VueJsonPretty from "vue-json-pretty";
import "vue-json-pretty/lib/styles.css";
defineProps({
graphData: Object
});
</script>
<template>
<vue-json-pretty
:path="'res'"
:deep="3"
:showLength="true"
:data="graphData"
/>
</template>

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,166 @@
const TurboType = {
SEQUENCE_FLOW: 1,
START_EVENT: 2,
END_EVENT: 3,
USER_TASK: 4,
SERVICE_TASK: 5,
EXCLUSIVE_GATEWAY: 6
};
function getTurboType(type) {
switch (type) {
case "bpmn:sequenceFlow":
return TurboType.SEQUENCE_FLOW;
case "bpmn:startEvent":
return TurboType.START_EVENT;
case "bpmn:endEvent":
return TurboType.END_EVENT;
case "bpmn:userTask":
return TurboType.USER_TASK;
case "bpmn:serviceTask":
return TurboType.SERVICE_TASK;
case "bpmn:exclusiveGateway":
return TurboType.EXCLUSIVE_GATEWAY;
default:
return type;
}
}
function convertNodeToTurboElement(node) {
const { id, type, x, y, text = "", properties } = node;
return {
incoming: [],
outgoing: [],
dockers: [],
type: getTurboType(node.type),
properties: {
...properties,
name: (text && text.value) || "",
x: x,
y: y,
text,
logicFlowType: type
},
key: id
};
}
function convertEdgeToTurboElement(edge) {
const {
id,
type,
sourceNodeId,
targetNodeId,
startPoint,
endPoint,
pointsList,
text = "",
properties
} = edge;
return {
incoming: [sourceNodeId],
outgoing: [targetNodeId],
type: getTurboType(type),
dockers: [],
properties: {
...properties,
name: (text && text.value) || "",
text,
startPoint,
endPoint,
pointsList,
logicFlowType: type
},
key: id
};
}
export function toTurboData(data) {
const nodeMap = new Map();
const turboData = {
flowElementList: []
};
data.nodes.forEach(node => {
const flowElement = convertNodeToTurboElement(node);
turboData.flowElementList.push(flowElement);
nodeMap.set(node.id, flowElement);
});
data.edges.forEach(edge => {
const flowElement = convertEdgeToTurboElement(edge);
const sourceElement = nodeMap.get(edge.sourceNodeId);
sourceElement.outgoing.push(flowElement.key);
const targetElement = nodeMap.get(edge.targetNodeId);
targetElement.incoming.push(flowElement.key);
turboData.flowElementList.push(flowElement);
});
return turboData;
}
function convertFlowElementToEdge(element) {
const { incoming, outgoing, properties, key } = element;
const { text, startPoint, endPoint, pointsList, logicFlowType } = properties;
const edge = {
id: key,
type: logicFlowType,
sourceNodeId: incoming[0],
targetNodeId: outgoing[0],
text,
startPoint,
endPoint,
pointsList,
properties: {}
};
const excludeProperties = [
"startPoint",
"endPoint",
"pointsList",
"text",
"logicFlowType"
];
Object.keys(element.properties).forEach(property => {
if (excludeProperties.indexOf(property) === -1) {
edge.properties[property] = element.properties[property];
}
});
return edge;
}
function convertFlowElementToNode(element) {
const { properties, key } = element;
const { x, y, text, logicFlowType } = properties;
const node = {
id: key,
type: logicFlowType,
x,
y,
text,
properties: {}
};
const excludeProperties = ["x", "y", "text", "logicFlowType"];
Object.keys(element.properties).forEach(property => {
if (excludeProperties.indexOf(property) === -1) {
node.properties[property] = element.properties[property];
}
});
return node;
}
export function toLogicflowData(data) {
const lfData = {
nodes: [],
edges: []
};
const list = data.flowElementList;
list &&
list.length > 0 &&
list.forEach(element => {
if (element.type === TurboType.SEQUENCE_FLOW) {
const edge = convertFlowElementToEdge(element);
lfData.edges.push(edge);
} else {
const node = convertFlowElementToNode(element);
lfData.nodes.push(node);
}
});
return lfData;
}

View File

@@ -0,0 +1,49 @@
@font-face {
font-family: "iconfont";
src: url("iconfont.eot?t=1618544337340"); /* IE9 */
src:
url("iconfont.eot?t=1618544337340#iefix") format("embedded-opentype"),
/* IE6-IE8 */
url("data:application/x-font-woff2;charset=utf-8;base64,d09GMgABAAAAAAZ0AAsAAAAADKgAAAYmAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHEIGVgCDZAqLQIldATYCJAMgCxIABCAFhG0HgQkb6ApRlA9Sk+xngd1wXQyjTXRCW7pkEvLB0N9/pZhyo7nvIIK1Nisnipg3omjUREiURDXNNEL/jDRCI5H/riTu/9q0D5OakT05VaM3E4kMJI2QhanZillesmYnVT0pD5+399suTrCEkjDhqLtAxyURhIU6Ser/1tp8aDPgI2g7ex2ah+Q7i0rI+Gy9rSNYOtEEdPFQVkrlj/1c3oZFk6Sv/bYQqWUunsgkk8QRkrgkCJEKpUcO8zx0cFLQr+x6CEiNi0BN2YWV4MwJhmDEqhdU4BwR8oIOEXPCjGMzcoKDuLmnLwLw6vy9vMCFM6ggIW50umRpIbVW14U29L/QmIZgqDs5cD0JDKwCHFIylReQ51yFpO+XKBwDcjHltbq9801mxdeFzX8inbguoAq1yCWzpH95JuRUJIC0EDPH5nNGtIkkA4GgvROBocpEEKLCCBwVj0BRF/CJHFYhEo9WCbF1TCdgEEgF0A0Ee8NxioIeN97QzQqFMd2tdfIJC3KeK0T3eJYu0J07g6BVbCB0IiDVDNsQ1mFcbNxDCTk6IWEb2ShHfHxUlvAjkfj0mHDhC56GAL4CWMUgQXgEywDxuH0TBAD7gDZuRqtx7KWpnyTbushlJUpytdfnUvoS/pXG880npIYe3wueUdIJoa9HlRgdsYiF5QJv8C2zjIbzXERGQmwH0QylmjJfC4evBB8UUKQZMsAMG2aWMU6nc6s9m7X4Thn0gTfomgnm5d0qwX4v0rQH3GZn4Ajp8F2VeUcTTARpA+FfyLcpc+T05bOemT2fny8EH8Vn4LPFh3htyOtB3jDSJj34IpEQ3HNboUdasWNDQifcA8BfPPkTe6YaWp0nF/IrhQHGW2D5HTO7O2zfTH3+gxip/NioTs9VwUXL7T3AbzTxHa3qSu1e4EZTfZl/QiC2c7UI5jZ/ET938pSH8Z8IPBwU0NopeLgB7h6Kvp0GVCOw72KAjKFA71sPKX7/9g+Js/AmNfj8/o28sqNVdSTVI93p08F3v/75zqw8W79vb0RVaCTrw6aNntrQwCtbzzDKosTRFMjp/WFqtpZUEGxsi6P8L09byvlyrrvUJ6/ZFJR/X32mbUmndlduWjbdnwnY2ZBHo8OIKIVDUJah62hi4aKdSoqZsWypN7d0w6nsAzb12tWrqZOl12+W/W7YyLFxDy/7U369cgFF85PUVevYahz8y/HS9ZGrbv7saR0sn5MfEzhinC2Dizcv5xHycyChG33pcskigbRkvXnDaurRjRuIeDdu4rnSgPQ/L196FHQg6FGs7266c82aTtDT1jU0CqzWoG2Ndf91wRo1g/0wo9b4VPtV+2iwl/fjvxq4f83CBZeYgx6njp8mb7jzou9FfPdwBBpffvyUx6XARoc/1umGwtrl034lryLH/YCEwly/XrrckYHsd+/YWY/u3EGI085rV6RD5+Bw7dqnoAvBjzifw3S3zdaNZL/dRnfz7XZup232DX4VtD6Cn+AzkqFgBq6unr/gwtCDuydN51fk76ocHS/nN25Y/WqMe1fzBRgEQHPEjqE0gIbkR1CKM/zYUukn9ItRVMHwLfuO1kaP2mlUivpAUpbb8f5wZS1eib+cs3/qlD9r8DU2NEccqhPVFos3SRGSKtb4hyJEcX6VZhArj8Y+edgVpHICKD9tt8ddsvuYpNLZfQGoyBiY2CzKm1chkFmHUGwbUityTs70kCCSE2DZZADRaSeo0heYTpdQ3vwIAv0+QagzEKTOQnnOzHzoXTMkrCJYy6q7Wb1GNPO6hLi6keVYaDeqpDDFGarGkqy3sLFRMXFPDjZjqYsD5A6BI4RneUk0sdlwM2w0iqxFEtuwhkTpCLHER0fzWQ+I0ogmcLVPgqkQmBZLrdvC1tMQmfGTE66J3y+HCdoZqUgFBd/Y1TCJTL92VqwoMRVQOUxzpYJTiZd1EHAIyXmskS4RmbCySY4ZpVPEsmRv1QbTIKLoGtgt4kVTI74qM2p4tulMzwFS4qPiUDFxCSSUSGJJKJd2ozFS1kgYmyN1snOnimh0brybVuw0G0WV9iF3xeYjFAg4LcEi4Q692C7TUI8omiJRZAN3M+4ikTLBlosAAAA=")
format("woff2"),
url("iconfont.woff?t=1618544337340") format("woff"),
url("iconfont.ttf?t=1618544337340") format("truetype"),
/* chrome, firefox, opera, Safari, Android, iOS 4.2+ */
url("iconfont.svg?t=1618544337340#iconfont") format("svg"); /* iOS 4.1- */
}
.iconfont {
font-family: "iconfont" !important;
font-size: 16px;
font-style: normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.icon-full-screen-hs::before {
content: "\e656";
}
.icon-watch-hs::before {
content: "\e766";
}
.icon-download-hs::before {
content: "\e6af";
}
.icon-enlarge-hs::before {
content: "\e765";
}
.icon-previous-hs::before {
content: "\e84c";
}
.icon-zoom-out-hs::before {
content: "\e744";
}
.icon-next-step-hs::before {
content: "\e84b";
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,58 @@
{
"id": "2491438",
"name": "liu'c'tu",
"font_family": "iconfont",
"css_prefix_text": "icon-",
"description": "",
"glyphs": [
{
"icon_id": "755619",
"name": "自适应图标",
"font_class": "full-screen-hs",
"unicode": "e656",
"unicode_decimal": 58966
},
{
"icon_id": "14445801",
"name": "查看",
"font_class": "watch-hs",
"unicode": "e766",
"unicode_decimal": 59238
},
{
"icon_id": "9712640",
"name": "下载",
"font_class": "download-hs",
"unicode": "e6af",
"unicode_decimal": 59055
},
{
"icon_id": "1029099",
"name": "放大",
"font_class": "enlarge-hs",
"unicode": "e765",
"unicode_decimal": 59237
},
{
"icon_id": "20017362",
"name": "上一步",
"font_class": "previous-hs",
"unicode": "e84c",
"unicode_decimal": 59468
},
{
"icon_id": "1010015",
"name": "缩小",
"font_class": "zoom-out-hs",
"unicode": "e744",
"unicode_decimal": 59204
},
{
"icon_id": "20017363",
"name": "下一步",
"font_class": "next-step-hs",
"unicode": "e84b",
"unicode_decimal": 59467
}
]
}

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.1 KiB

View File

@@ -0,0 +1,55 @@
export const nodeList = [
{
text: "开始",
type: "start",
class: "node-start"
},
{
text: "矩形",
type: "rect",
class: "node-rect"
},
{
type: "user",
text: "用户",
class: "node-user"
},
{
type: "push",
text: "推送",
class: "node-push"
},
{
type: "download",
text: "位置",
class: "node-download"
},
{
type: "end",
text: "结束",
class: "node-end"
}
];
export const BpmnNode = [
{
type: "bpmn:startEvent",
text: "开始",
class: "bpmn-start"
},
{
type: "bpmn:endEvent",
text: "结束",
class: "bpmn-end"
},
{
type: "bpmn:exclusiveGateway",
text: "网关",
class: "bpmn-exclusiveGateway"
},
{
type: "bpmn:userTask",
text: "用户",
class: "bpmn-user"
}
];

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,15 @@
import iconifyIconOffline from "./src/iconifyIconOffline";
import iconifyIconOnline from "./src/iconifyIconOnline";
import iconSelect from "./src/Select.vue";
import fontIcon from "./src/iconfont";
/** 本地图标组件 */
const IconifyIconOffline = iconifyIconOffline;
/** 在线图标组件 */
const IconifyIconOnline = iconifyIconOnline;
/** `IconSelect`图标选择器组件 */
const IconSelect = iconSelect;
/** `iconfont`组件 */
const FontIcon = fontIcon;
export { IconifyIconOffline, IconifyIconOnline, IconSelect, FontIcon };

View File

@@ -0,0 +1,268 @@
<script setup lang="ts">
import { IconJson } from "@/components/ReIcon/data";
import { cloneDeep, isAllEmpty } from "@pureadmin/utils";
import { ref, computed, CSSProperties, watch } from "vue";
import Search from "@iconify-icons/ri/search-eye-line";
type ParameterCSSProperties = (item?: string) => CSSProperties | undefined;
defineOptions({
name: "IconSelect"
});
const inputValue = defineModel({ type: String });
const iconList = ref(IconJson);
const icon = ref();
const currentActiveType = ref("ep:");
// 深拷贝图标数据,前端做搜索
const copyIconList = cloneDeep(iconList.value);
const totalPage = ref(0);
// 每页显示35个图标
const pageSize = ref(35);
const currentPage = ref(1);
// 搜索条件
const filterValue = ref("");
const tabsList = [
{
label: "Element Plus",
name: "ep:"
},
{
label: "Remix Icon",
name: "ri:"
},
{
label: "Font Awesome 5 Solid",
name: "fa-solid:"
}
];
const pageList = computed(() =>
copyIconList[currentActiveType.value]
.filter(i => i.includes(filterValue.value))
.slice(
(currentPage.value - 1) * pageSize.value,
currentPage.value * pageSize.value
)
);
const iconItemStyle = computed((): ParameterCSSProperties => {
return item => {
if (inputValue.value === currentActiveType.value + item) {
return {
borderColor: "var(--el-color-primary)",
color: "var(--el-color-primary)"
};
}
};
});
function setVal() {
currentActiveType.value = inputValue.value.substring(
0,
inputValue.value.indexOf(":") + 1
);
icon.value = inputValue.value.substring(inputValue.value.indexOf(":") + 1);
}
function onBeforeEnter() {
if (isAllEmpty(icon.value)) return;
setVal();
// 寻找当前图标在第几页
const curIconIndex = copyIconList[currentActiveType.value].findIndex(
i => i === icon.value
);
currentPage.value = Math.ceil((curIconIndex + 1) / pageSize.value);
}
function onAfterLeave() {
filterValue.value = "";
}
function handleClick({ props }) {
currentPage.value = 1;
currentActiveType.value = props.name;
}
function onChangeIcon(item) {
icon.value = item;
inputValue.value = currentActiveType.value + item;
}
function onCurrentChange(page) {
currentPage.value = page;
}
function onClear() {
icon.value = "";
inputValue.value = "";
}
watch(
() => pageList.value,
() =>
(totalPage.value = copyIconList[currentActiveType.value].filter(i =>
i.includes(filterValue.value)
).length),
{ immediate: true }
);
watch(
() => inputValue.value,
val => val && setVal(),
{ immediate: true }
);
watch(
() => filterValue.value,
() => (currentPage.value = 1)
);
</script>
<template>
<div class="selector">
<el-input v-model="inputValue" disabled>
<template #append>
<el-popover
:width="350"
trigger="click"
popper-class="pure-popper"
:popper-options="{
placement: 'auto'
}"
@before-enter="onBeforeEnter"
@after-leave="onAfterLeave"
>
<template #reference>
<div
class="w-[40px] h-[32px] cursor-pointer flex justify-center items-center"
>
<IconifyIconOffline v-if="!icon" :icon="Search" />
<IconifyIconOnline v-else :icon="inputValue" />
</div>
</template>
<el-input
v-model="filterValue"
class="px-2 pt-2"
placeholder="搜索图标"
clearable
/>
<el-tabs v-model="currentActiveType" @tab-click="handleClick">
<el-tab-pane
v-for="(pane, index) in tabsList"
:key="index"
:label="pane.label"
:name="pane.name"
>
<el-scrollbar height="220px">
<ul class="flex flex-wrap px-2 ml-2">
<li
v-for="(item, key) in pageList"
:key="key"
:title="item"
class="icon-item p-2 cursor-pointer mr-2 mt-1 flex justify-center items-center border border-[#e5e7eb]"
:style="iconItemStyle(item)"
@click="onChangeIcon(item)"
>
<IconifyIconOnline
:icon="currentActiveType + item"
width="20px"
height="20px"
/>
</li>
</ul>
<el-empty
v-show="pageList.length === 0"
:description="`${filterValue} 图标不存在`"
:image-size="60"
/>
</el-scrollbar>
</el-tab-pane>
</el-tabs>
<div
class="w-full h-9 flex items-center overflow-auto border-t border-[#e5e7eb]"
>
<el-pagination
class="flex-auto ml-2"
:total="totalPage"
:current-page="currentPage"
:page-size="pageSize"
:pager-count="5"
layout="pager"
background
size="small"
@current-change="onCurrentChange"
/>
<el-button
class="justify-end mr-2 ml-2"
type="danger"
size="small"
text
bg
@click="onClear"
>
清空
</el-button>
</div>
</el-popover>
</template>
</el-input>
</div>
</template>
<style lang="scss" scoped>
.icon-item {
&:hover {
color: var(--el-color-primary);
border-color: var(--el-color-primary);
transition: all 0.4s;
transform: scaleX(1.05);
}
}
:deep(.el-tabs__nav-next) {
font-size: 15px;
line-height: 32px;
box-shadow: -5px 0 5px -6px #ccc;
}
:deep(.el-tabs__nav-prev) {
font-size: 15px;
line-height: 32px;
box-shadow: 5px 0 5px -6px #ccc;
}
:deep(.el-input-group__append) {
padding: 0;
}
:deep(.el-tabs__item) {
height: 30px;
font-size: 12px;
font-weight: normal;
line-height: 30px;
}
:deep(.el-tabs__header),
:deep(.el-tabs__nav-wrap) {
position: static;
margin: 0;
box-shadow: 0 2px 5px rgb(0 0 0 / 6%);
}
:deep(.el-tabs__nav-wrap::after) {
height: 0;
}
:deep(.el-tabs__nav-wrap) {
padding: 0 24px;
}
:deep(.el-tabs__content) {
margin-top: 4px;
}
</style>

View File

@@ -0,0 +1,61 @@
import type { iconType } from "./types";
import { h, defineComponent, type Component } from "vue";
import { IconifyIconOnline, IconifyIconOffline, FontIcon } from "../index";
/**
* 支持 `iconfont`、自定义 `svg` 以及 `iconify` 中所有的图标
* @see 点击查看文档图标篇 {@link https://pure-admin.github.io/pure-admin-doc/pages/icon/}
* @param icon 必传 图标
* @param attrs 可选 iconType 属性
* @returns Component
*/
export function useRenderIcon(icon: any, attrs?: iconType): Component {
// iconfont
const ifReg = /^IF-/;
// typeof icon === "function" 属于SVG
if (ifReg.test(icon)) {
// iconfont
const name = icon.split(ifReg)[1];
const iconName = name.slice(
0,
name.indexOf(" ") == -1 ? name.length : name.indexOf(" ")
);
const iconType = name.slice(name.indexOf(" ") + 1, name.length);
return defineComponent({
name: "FontIcon",
render() {
return h(FontIcon, {
icon: iconName,
iconType,
...attrs
});
}
});
} else if (typeof icon === "function" || typeof icon?.render === "function") {
// svg
return attrs ? h(icon, { ...attrs }) : icon;
} else if (typeof icon === "object") {
return defineComponent({
name: "OfflineIcon",
render() {
return h(IconifyIconOffline, {
icon: icon,
...attrs
});
}
});
} else {
// 通过是否存在 : 符号来判断是在线还是本地图标,存在即是在线图标,反之
return defineComponent({
name: "Icon",
render() {
const IconifyIcon =
icon && icon.includes(":") ? IconifyIconOnline : IconifyIconOffline;
return h(IconifyIcon, {
icon: icon,
...attrs
});
}
});
}
}

View File

@@ -0,0 +1,48 @@
import { h, defineComponent } from "vue";
// 封装iconfont组件默认`font-class`引用模式,支持`unicode`引用、`font-class`引用、`symbol`引用 https://www.iconfont.cn/help/detail?spm=a313x.7781069.1998910419.20&helptype=code
export default defineComponent({
name: "FontIcon",
props: {
icon: {
type: String,
default: ""
}
},
render() {
const attrs = this.$attrs;
if (Object.keys(attrs).includes("uni") || attrs?.iconType === "uni") {
return h(
"i",
{
class: "iconfont",
...attrs
},
this.icon
);
} else if (
Object.keys(attrs).includes("svg") ||
attrs?.iconType === "svg"
) {
return h(
"svg",
{
class: "icon-svg",
"aria-hidden": true
},
{
default: () => [
h("use", {
"xlink:href": `#${this.icon}`
})
]
}
);
} else {
return h("i", {
class: `iconfont ${this.icon}`,
...attrs
});
}
}
});

View File

@@ -0,0 +1,30 @@
import { h, defineComponent } from "vue";
import { Icon as IconifyIcon, addIcon } from "@iconify/vue/dist/offline";
// Iconify Icon在Vue里本地使用用于内网环境
export default defineComponent({
name: "IconifyIconOffline",
components: { IconifyIcon },
props: {
icon: {
default: null
}
},
render() {
if (typeof this.icon === "object") addIcon(this.icon, this.icon);
const attrs = this.$attrs;
return h(
IconifyIcon,
{
icon: this.icon,
style: attrs?.style
? Object.assign(attrs.style, { outline: "none" })
: { outline: "none" },
...attrs
},
{
default: () => []
}
);
}
});

View File

@@ -0,0 +1,30 @@
import { h, defineComponent } from "vue";
import { Icon as IconifyIcon } from "@iconify/vue";
// Iconify Icon在Vue里在线使用用于外网环境
export default defineComponent({
name: "IconifyIconOnline",
components: { IconifyIcon },
props: {
icon: {
type: String,
default: ""
}
},
render() {
const attrs = this.$attrs;
return h(
IconifyIcon,
{
icon: `${this.icon}`,
style: attrs?.style
? Object.assign(attrs.style, { outline: "none" })
: { outline: "none" },
...attrs
},
{
default: () => []
}
);
}
});

View File

@@ -0,0 +1,70 @@
// 这里存放本地图标,在 src/layout/index.vue 文件中加载,避免在首启动加载
import { addIcon } from "@iconify/vue/dist/offline";
// 本地菜单图标,后端在路由的 icon 中返回对应的图标字符串并且前端在此处使用 addIcon 添加即可渲染菜单图标
// @iconify-icons/ep
import Menu from "@iconify-icons/ep/menu";
import Edit from "@iconify-icons/ep/edit";
import SetUp from "@iconify-icons/ep/set-up";
import Guide from "@iconify-icons/ep/guide";
import Monitor from "@iconify-icons/ep/monitor";
import Lollipop from "@iconify-icons/ep/lollipop";
import Histogram from "@iconify-icons/ep/histogram";
import HomeFilled from "@iconify-icons/ep/home-filled";
addIcon("ep:menu", Menu);
addIcon("ep:edit", Edit);
addIcon("ep:set-up", SetUp);
addIcon("ep:guide", Guide);
addIcon("ep:monitor", Monitor);
addIcon("ep:lollipop", Lollipop);
addIcon("ep:histogram", Histogram);
addIcon("ep:home-filled", HomeFilled);
// @iconify-icons/ri
import Tag from "@iconify-icons/ri/bookmark-2-line";
import Ppt from "@iconify-icons/ri/file-ppt-2-line";
import Card from "@iconify-icons/ri/bank-card-line";
import Role from "@iconify-icons/ri/admin-fill";
import Info from "@iconify-icons/ri/file-info-line";
import Dept from "@iconify-icons/ri/git-branch-line";
import Table from "@iconify-icons/ri/table-line";
import Links from "@iconify-icons/ri/links-fill";
import Search from "@iconify-icons/ri/search-line";
import FlUser from "@iconify-icons/ri/admin-line";
import Setting from "@iconify-icons/ri/settings-3-line";
import MindMap from "@iconify-icons/ri/mind-map";
import BarChart from "@iconify-icons/ri/bar-chart-horizontal-line";
import LoginLog from "@iconify-icons/ri/window-line";
import Artboard from "@iconify-icons/ri/artboard-line";
import SystemLog from "@iconify-icons/ri/file-search-line";
import ListCheck from "@iconify-icons/ri/list-check";
import UbuntuFill from "@iconify-icons/ri/ubuntu-fill";
import OnlineUser from "@iconify-icons/ri/user-voice-line";
import EditBoxLine from "@iconify-icons/ri/edit-box-line";
import OperationLog from "@iconify-icons/ri/history-fill";
import InformationLine from "@iconify-icons/ri/information-line";
import TerminalWindowLine from "@iconify-icons/ri/terminal-window-line";
import CheckboxCircleLine from "@iconify-icons/ri/checkbox-circle-line";
addIcon("ri:bookmark-2-line", Tag);
addIcon("ri:file-ppt-2-line", Ppt);
addIcon("ri:bank-card-line", Card);
addIcon("ri:admin-fill", Role);
addIcon("ri:file-info-line", Info);
addIcon("ri:git-branch-line", Dept);
addIcon("ri:links-fill", Links);
addIcon("ri:table-line", Table);
addIcon("ri:search-line", Search);
addIcon("ri:admin-line", FlUser);
addIcon("ri:settings-3-line", Setting);
addIcon("ri:mind-map", MindMap);
addIcon("ri:bar-chart-horizontal-line", BarChart);
addIcon("ri:window-line", LoginLog);
addIcon("ri:file-search-line", SystemLog);
addIcon("ri:artboard-line", Artboard);
addIcon("ri:list-check", ListCheck);
addIcon("ri:ubuntu-fill", UbuntuFill);
addIcon("ri:user-voice-line", OnlineUser);
addIcon("ri:edit-box-line", EditBoxLine);
addIcon("ri:history-fill", OperationLog);
addIcon("ri:information-line", InformationLine);
addIcon("ri:terminal-window-line", TerminalWindowLine);
addIcon("ri:checkbox-circle-line", CheckboxCircleLine);

View File

@@ -0,0 +1,20 @@
export interface iconType {
// iconify (https://docs.iconify.design/icon-components/vue/#properties)
inline?: boolean;
width?: string | number;
height?: string | number;
horizontalFlip?: boolean;
verticalFlip?: boolean;
flip?: string;
rotate?: number | string;
color?: string;
horizontalAlign?: boolean;
verticalAlign?: boolean;
align?: string;
onLoad?: Function;
includes?: Function;
// svg 需要什么SVG属性自行添加
fill?: string;
// all icon
style?: object;
}

View File

@@ -0,0 +1,7 @@
import reImageVerify from "./src/index.vue";
import { withInstall } from "@pureadmin/utils";
/** 图形验证码组件 */
export const ReImageVerify = withInstall(reImageVerify);
export default ReImageVerify;

View File

@@ -0,0 +1,85 @@
import { ref, onMounted } from "vue";
/**
* 绘制图形验证码
* @param width - 图形宽度
* @param height - 图形高度
*/
export const useImageVerify = (width = 120, height = 40) => {
const domRef = ref<HTMLCanvasElement>();
const imgCode = ref("");
function setImgCode(code: string) {
imgCode.value = code;
}
function getImgCode() {
if (!domRef.value) return;
imgCode.value = draw(domRef.value, width, height);
}
onMounted(() => {
getImgCode();
});
return {
domRef,
imgCode,
setImgCode,
getImgCode
};
};
function randomNum(min: number, max: number) {
const num = Math.floor(Math.random() * (max - min) + min);
return num;
}
function randomColor(min: number, max: number) {
const r = randomNum(min, max);
const g = randomNum(min, max);
const b = randomNum(min, max);
return `rgb(${r},${g},${b})`;
}
function draw(dom: HTMLCanvasElement, width: number, height: number) {
let imgCode = "";
const NUMBER_STRING = "0123456789";
const ctx = dom.getContext("2d");
if (!ctx) return imgCode;
ctx.fillStyle = randomColor(180, 230);
ctx.fillRect(0, 0, width, height);
for (let i = 0; i < 4; i += 1) {
const text = NUMBER_STRING[randomNum(0, NUMBER_STRING.length)];
imgCode += text;
const fontSize = randomNum(18, 41);
const deg = randomNum(-30, 30);
ctx.font = `${fontSize}px Simhei`;
ctx.textBaseline = "top";
ctx.fillStyle = randomColor(80, 150);
ctx.save();
ctx.translate(30 * i + 15, 15);
ctx.rotate((deg * Math.PI) / 180);
ctx.fillText(text, -15 + 5, -15);
ctx.restore();
}
for (let i = 0; i < 5; i += 1) {
ctx.beginPath();
ctx.moveTo(randomNum(0, width), randomNum(0, height));
ctx.lineTo(randomNum(0, width), randomNum(0, height));
ctx.strokeStyle = randomColor(180, 230);
ctx.closePath();
ctx.stroke();
}
for (let i = 0; i < 41; i += 1) {
ctx.beginPath();
ctx.arc(randomNum(0, width), randomNum(0, height), 1, 0, 2 * Math.PI);
ctx.closePath();
ctx.fillStyle = randomColor(150, 200);
ctx.fill();
}
return imgCode;
}

View File

@@ -0,0 +1,46 @@
<script setup lang="ts">
import { watch } from "vue";
import { useImageVerify } from "./hooks";
defineOptions({
name: "ReImageVerify"
});
interface Props {
code?: string;
}
interface Emits {
(e: "update:code", code: string): void;
}
const props = withDefaults(defineProps<Props>(), {
code: ""
});
const emit = defineEmits<Emits>();
const { domRef, imgCode, setImgCode, getImgCode } = useImageVerify();
watch(
() => props.code,
newValue => {
setImgCode(newValue);
}
);
watch(imgCode, newValue => {
emit("update:code", newValue);
});
defineExpose({ getImgCode });
</script>
<template>
<canvas
ref="domRef"
width="120"
height="40"
class="cursor-pointer"
@click="getImgCode"
/>
</template>

View File

@@ -0,0 +1,7 @@
import amap from "./src/Amap.vue";
import { withInstall } from "@pureadmin/utils";
/** 高德地图组件 */
export const Amap = withInstall(amap);
export default Amap;

View File

@@ -0,0 +1,136 @@
<script setup lang="ts">
import { reactive, getCurrentInstance, onBeforeMount, onUnmounted } from "vue";
import { deviceDetection } from "@pureadmin/utils";
import AMapLoader from "@amap/amap-jsapi-loader";
import { mapJson } from "@/api/mock";
import car from "@/assets/car.png";
export interface MapConfigureInter {
on: Fn;
destroy?: Fn;
clearEvents?: Fn;
addControl?: Fn;
setCenter?: Fn;
setZoom?: Fn;
plugin?: Fn;
}
defineOptions({
name: "Amap"
});
let MarkerCluster;
let map: MapConfigureInter;
const instance = getCurrentInstance();
const mapSet = reactive({
loading: deviceDetection() ? false : true
});
// 地图创建完成(动画关闭)
const complete = (): void => {
if (map) {
map.on("complete", () => {
mapSet.loading = false;
});
}
};
onBeforeMount(() => {
if (!instance) return;
const { MapConfigure } = instance.appContext.config.globalProperties.$config;
const { options } = MapConfigure;
AMapLoader.load({
key: MapConfigure.amapKey,
version: "2.0",
plugins: ["AMap.MarkerCluster"]
})
.then(AMap => {
// 创建地图实例
map = new AMap.Map(instance.refs.mapview, options);
//地图中添加地图操作ToolBar插件
map.plugin(["AMap.ToolBar", "AMap.MapType"], () => {
map.addControl(new AMap.ToolBar());
//地图类型切换
map.addControl(
new AMap.MapType({
defaultType: 0
})
);
});
MarkerCluster = new AMap.MarkerCluster(map, [], {
// 聚合网格像素大小
gridSize: 80,
maxZoom: 14,
renderMarker(ctx) {
const { marker, data } = ctx;
if (Array.isArray(data) && data[0]) {
const { driver, plateNumber, orientation } = data[0];
const content = `<img style="transform: scale(1) rotate(${
360 - Number(orientation)
}deg);" src='${car}' />`;
marker.setContent(content);
marker.setLabel({
direction: "bottom",
//设置文本标注偏移量
offset: new AMap.Pixel(-4, 0),
//设置文本标注内容
content: `<div> ${plateNumber}(${driver})</div>`
});
marker.setOffset(new AMap.Pixel(-18, -10));
marker.on("click", ({ lnglat }) => {
map.setZoom(13); //设置地图层级
map.setCenter(lnglat);
});
}
}
});
// 获取模拟车辆信息
mapJson()
.then(({ data }) => {
const points: object = data.map(v => {
return {
lnglat: [v.lng, v.lat],
...v
};
});
if (MarkerCluster) MarkerCluster.setData(points);
})
.catch(err => {
console.log("err:", err);
});
complete();
})
.catch(() => {
mapSet.loading = false;
throw "地图加载失败,请重新加载";
});
});
onUnmounted(() => {
if (map) {
// 销毁地图实例
map.destroy() && map.clearEvents("click");
}
});
</script>
<template>
<div id="mapview" ref="mapview" v-loading="mapSet.loading" />
</template>
<style lang="scss" scoped>
#mapview {
height: calc(100vh - 86px);
}
:deep(.amap-marker-label) {
border: none !important;
}
</style>

View File

@@ -0,0 +1,5 @@
import perms from "./src/perms";
const Perms = perms;
export { Perms };

View File

@@ -0,0 +1,20 @@
import { defineComponent, Fragment } from "vue";
import { hasPerms } from "@/utils/auth";
export default defineComponent({
name: "Perms",
props: {
value: {
type: undefined,
default: []
}
},
setup(props, { slots }) {
return () => {
if (!slots) return null;
return hasPerms(props.value) ? (
<Fragment>{slots.default?.()}</Fragment>
) : null;
};
}
});

View File

@@ -0,0 +1,5 @@
import pureTableBar from "./src/bar";
import { withInstall } from "@pureadmin/utils";
/** 配合 `@pureadmin/table` 实现快速便捷的表格操作 https://github.com/pure-admin/pure-admin-table */
export const PureTableBar = withInstall(pureTableBar);

View File

@@ -0,0 +1,393 @@
import Sortable from "sortablejs";
import { transformI18n } from "@/plugins/i18n";
import { useEpThemeStoreHook } from "@/store/modules/epTheme";
import {
type PropType,
ref,
unref,
computed,
nextTick,
defineComponent,
getCurrentInstance
} from "vue";
import {
delay,
cloneDeep,
isBoolean,
isFunction,
getKeyList
} from "@pureadmin/utils";
import Fullscreen from "@iconify-icons/ri/fullscreen-fill";
import ExitFullscreen from "@iconify-icons/ri/fullscreen-exit-fill";
import DragIcon from "@/assets/table-bar/drag.svg?component";
import ExpandIcon from "@/assets/table-bar/expand.svg?component";
import RefreshIcon from "@/assets/table-bar/refresh.svg?component";
import SettingIcon from "@/assets/table-bar/settings.svg?component";
import CollapseIcon from "@/assets/table-bar/collapse.svg?component";
const props = {
/** 头部最左边的标题 */
title: {
type: String,
default: "列表"
},
/** 对于树形表格如果想启用展开和折叠功能传入当前表格的ref即可 */
tableRef: {
type: Object as PropType<any>
},
/** 需要展示的列 */
columns: {
type: Array as PropType<TableColumnList>,
default: () => []
},
isExpandAll: {
type: Boolean,
default: true
},
tableKey: {
type: [String, Number] as PropType<string | number>,
default: "0"
}
};
export default defineComponent({
name: "PureTableBar",
props,
emits: ["refresh"],
setup(props, { emit, slots, attrs }) {
const size = ref("default");
const loading = ref(false);
const checkAll = ref(true);
const isFullscreen = ref(false);
const isIndeterminate = ref(false);
const instance = getCurrentInstance()!;
const isExpandAll = ref(props.isExpandAll);
const filterColumns = cloneDeep(props?.columns).filter(column =>
isBoolean(column?.hide)
? !column.hide
: !(isFunction(column?.hide) && column?.hide())
);
let checkColumnList = getKeyList(cloneDeep(props?.columns), "label");
const checkedColumns = ref(getKeyList(cloneDeep(filterColumns), "label"));
const dynamicColumns = ref(cloneDeep(props?.columns));
const getDropdownItemStyle = computed(() => {
return s => {
return {
background:
s === size.value ? useEpThemeStoreHook().epThemeColor : "",
color: s === size.value ? "#fff" : "var(--el-text-color-primary)"
};
};
});
const iconClass = computed(() => {
return [
"text-black",
"dark:text-white",
"duration-100",
"hover:!text-primary",
"cursor-pointer",
"outline-none"
];
});
const topClass = computed(() => {
return [
"flex",
"justify-between",
"pt-[3px]",
"px-[11px]",
"border-b-[1px]",
"border-solid",
"border-[#dcdfe6]",
"dark:border-[#303030]"
];
});
function onReFresh() {
loading.value = true;
emit("refresh");
delay(500).then(() => (loading.value = false));
}
function onExpand() {
isExpandAll.value = !isExpandAll.value;
toggleRowExpansionAll(props.tableRef.data, isExpandAll.value);
}
function toggleRowExpansionAll(data, isExpansion) {
data.forEach(item => {
props.tableRef.toggleRowExpansion(item, isExpansion);
if (item.children !== undefined && item.children !== null) {
toggleRowExpansionAll(item.children, isExpansion);
}
});
}
function handleCheckAllChange(val: boolean) {
checkedColumns.value = val ? checkColumnList : [];
isIndeterminate.value = false;
dynamicColumns.value.map(column =>
val ? (column.hide = false) : (column.hide = true)
);
}
function handleCheckedColumnsChange(value: string[]) {
checkedColumns.value = value;
const checkedCount = value.length;
checkAll.value = checkedCount === checkColumnList.length;
isIndeterminate.value =
checkedCount > 0 && checkedCount < checkColumnList.length;
}
function handleCheckColumnListChange(val: boolean, label: string) {
dynamicColumns.value.filter(
item => transformI18n(item.label) === transformI18n(label)
)[0].hide = !val;
}
async function onReset() {
checkAll.value = true;
isIndeterminate.value = false;
dynamicColumns.value = cloneDeep(props?.columns);
checkColumnList = [];
checkColumnList = await getKeyList(cloneDeep(props?.columns), "label");
checkedColumns.value = getKeyList(cloneDeep(filterColumns), "label");
}
const dropdown = {
dropdown: () => (
<el-dropdown-menu class="translation">
<el-dropdown-item
style={getDropdownItemStyle.value("large")}
onClick={() => (size.value = "large")}
>
</el-dropdown-item>
<el-dropdown-item
style={getDropdownItemStyle.value("default")}
onClick={() => (size.value = "default")}
>
</el-dropdown-item>
<el-dropdown-item
style={getDropdownItemStyle.value("small")}
onClick={() => (size.value = "small")}
>
</el-dropdown-item>
</el-dropdown-menu>
)
};
/** 列展示拖拽排序 */
const rowDrop = (event: { preventDefault: () => void }) => {
event.preventDefault();
nextTick(() => {
const wrapper: HTMLElement = (
instance?.proxy?.$refs[`GroupRef${unref(props.tableKey)}`] as any
).$el.firstElementChild;
Sortable.create(wrapper, {
animation: 300,
handle: ".drag-btn",
onEnd: ({ newIndex, oldIndex, item }) => {
const targetThElem = item;
const wrapperElem = targetThElem.parentNode as HTMLElement;
const oldColumn = dynamicColumns.value[oldIndex];
const newColumn = dynamicColumns.value[newIndex];
if (oldColumn?.fixed || newColumn?.fixed) {
// 当前列存在fixed属性 则不可拖拽
const oldThElem = wrapperElem.children[oldIndex] as HTMLElement;
if (newIndex > oldIndex) {
wrapperElem.insertBefore(targetThElem, oldThElem);
} else {
wrapperElem.insertBefore(
targetThElem,
oldThElem ? oldThElem.nextElementSibling : oldThElem
);
}
return;
}
const currentRow = dynamicColumns.value.splice(oldIndex, 1)[0];
dynamicColumns.value.splice(newIndex, 0, currentRow);
}
});
});
};
const isFixedColumn = (label: string) => {
return dynamicColumns.value.filter(
item => transformI18n(item.label) === transformI18n(label)
)[0].fixed
? true
: false;
};
const rendTippyProps = (content: string) => {
// https://vue-tippy.netlify.app/props
return {
content,
offset: [0, 18],
duration: [300, 0],
followCursor: true,
hideOnClick: "toggle"
};
};
const reference = {
reference: () => (
<SettingIcon
class={["w-[16px]", iconClass.value]}
v-tippy={rendTippyProps("列设置")}
/>
)
};
return () => (
<>
<div
{...attrs}
class={[
"w-[99/100]",
"px-2",
"pb-2",
"bg-bg_color",
isFullscreen.value
? ["!w-full", "!h-full", "z-[2002]", "fixed", "inset-0"]
: "mt-2"
]}
>
<div class="flex justify-between w-full h-[60px] p-4">
{slots?.title ? (
slots.title()
) : (
<p class="font-bold truncate">{props.title}</p>
)}
<div class="flex items-center justify-around">
{slots?.buttons ? (
<div class="flex mr-4">{slots.buttons()}</div>
) : null}
{props.tableRef?.size ? (
<>
<ExpandIcon
class={["w-[16px]", iconClass.value]}
style={{
transform: isExpandAll.value ? "none" : "rotate(-90deg)"
}}
v-tippy={rendTippyProps(
isExpandAll.value ? "折叠" : "展开"
)}
onClick={() => onExpand()}
/>
<el-divider direction="vertical" />
</>
) : null}
<RefreshIcon
class={[
"w-[16px]",
iconClass.value,
loading.value ? "animate-spin" : ""
]}
v-tippy={rendTippyProps("刷新")}
onClick={() => onReFresh()}
/>
<el-divider direction="vertical" />
<el-dropdown
v-slots={dropdown}
trigger="click"
v-tippy={rendTippyProps("密度")}
>
<CollapseIcon class={["w-[16px]", iconClass.value]} />
</el-dropdown>
<el-divider direction="vertical" />
<el-popover
v-slots={reference}
placement="bottom-start"
popper-style={{ padding: 0 }}
width="200"
trigger="click"
>
<div class={[topClass.value]}>
<el-checkbox
class="!-mr-1"
label="列展示"
v-model={checkAll.value}
indeterminate={isIndeterminate.value}
onChange={value => handleCheckAllChange(value)}
/>
<el-button type="primary" link onClick={() => onReset()}>
</el-button>
</div>
<div class="pt-[6px] pl-[11px]">
<el-scrollbar max-height="36vh">
<el-checkbox-group
ref={`GroupRef${unref(props.tableKey)}`}
modelValue={checkedColumns.value}
onChange={value => handleCheckedColumnsChange(value)}
>
<el-space
direction="vertical"
alignment="flex-start"
size={0}
>
{checkColumnList.map((item, index) => {
return (
<div class="flex items-center">
<DragIcon
class={[
"drag-btn w-[16px] mr-2",
isFixedColumn(item)
? "!cursor-no-drop"
: "!cursor-grab"
]}
onMouseenter={(event: {
preventDefault: () => void;
}) => rowDrop(event)}
/>
<el-checkbox
key={index}
label={item}
value={item}
onChange={value =>
handleCheckColumnListChange(value, item)
}
>
<span
title={transformI18n(item)}
class="inline-block w-[120px] truncate hover:text-text_color_primary"
>
{transformI18n(item)}
</span>
</el-checkbox>
</div>
);
})}
</el-space>
</el-checkbox-group>
</el-scrollbar>
</div>
</el-popover>
<el-divider direction="vertical" />
<iconifyIconOffline
class={["w-[16px]", iconClass.value]}
icon={isFullscreen.value ? ExitFullscreen : Fullscreen}
v-tippy={isFullscreen.value ? "退出全屏" : "全屏"}
onClick={() => (isFullscreen.value = !isFullscreen.value)}
/>
</div>
</div>
{slots.default({
size: size.value,
dynamicColumns: dynamicColumns.value
})}
</div>
</>
);
}
});

View File

@@ -0,0 +1,7 @@
import reQrcode from "./src/index";
import { withInstall } from "@pureadmin/utils";
/** 二维码组件 */
export const ReQrcode = withInstall(reQrcode);
export default ReQrcode;

View File

@@ -0,0 +1,9 @@
.qrcode {
&--disabled {
background: rgb(255 255 255 / 95%);
& > div {
transform: translate(-50%, -50%);
}
}
}

View File

@@ -0,0 +1,261 @@
import {
type PropType,
ref,
unref,
watch,
nextTick,
computed,
defineComponent
} from "vue";
import "./index.scss";
import propTypes from "@/utils/propTypes";
import { isString, cloneDeep } from "@pureadmin/utils";
import QRCode, { type QRCodeRenderersOptions } from "qrcode";
import RefreshRight from "@iconify-icons/ep/refresh-right";
interface QrcodeLogo {
src?: string;
logoSize?: number;
bgColor?: string;
borderSize?: number;
crossOrigin?: string;
borderRadius?: number;
logoRadius?: number;
}
const props = {
// img 或者 canvas,img不支持logo嵌套
tag: propTypes.string
.validate((v: string) => ["canvas", "img"].includes(v))
.def("canvas"),
// 二维码内容
text: {
type: [String, Array] as PropType<string | Recordable[]>,
default: null
},
// qrcode.js配置项
options: {
type: Object as PropType<QRCodeRenderersOptions>,
default: (): QRCodeRenderersOptions => ({})
},
// 宽度
width: propTypes.number.def(200),
// logo
logo: {
type: [String, Object] as PropType<Partial<QrcodeLogo> | string>,
default: (): QrcodeLogo | string => ""
},
// 是否过期
disabled: propTypes.bool.def(false),
// 过期提示内容
disabledText: propTypes.string.def("")
};
export default defineComponent({
name: "ReQrcode",
props,
emits: ["done", "click", "disabled-click"],
setup(props, { emit }) {
const { toCanvas, toDataURL } = QRCode;
const loading = ref(true);
const wrapRef = ref<Nullable<HTMLCanvasElement | HTMLImageElement>>(null);
const renderText = computed(() => String(props.text));
const wrapStyle = computed(() => {
return {
width: props.width + "px",
height: props.width + "px"
};
});
const initQrcode = async () => {
await nextTick();
const options = cloneDeep(props.options || {});
if (props.tag === "canvas") {
// 容错率,默认对内容少的二维码采用高容错率,内容多的二维码采用低容错率
options.errorCorrectionLevel =
options.errorCorrectionLevel ||
getErrorCorrectionLevel(unref(renderText));
const _width: number = await getOriginWidth(unref(renderText), options);
options.scale =
props.width === 0 ? undefined : (props.width / _width) * 4;
const canvasRef: any = await toCanvas(
unref(wrapRef) as HTMLCanvasElement,
unref(renderText),
options
);
if (props.logo) {
const url = await createLogoCode(canvasRef);
emit("done", url);
loading.value = false;
} else {
emit("done", canvasRef.toDataURL());
loading.value = false;
}
} else {
const url = await toDataURL(renderText.value, {
errorCorrectionLevel: "H",
width: props.width,
...options
});
(unref(wrapRef) as any).src = url;
emit("done", url);
loading.value = false;
}
};
watch(
() => renderText.value,
val => {
if (!val) return;
initQrcode();
},
{
deep: true,
immediate: true
}
);
const createLogoCode = (canvasRef: HTMLCanvasElement) => {
const canvasWidth = canvasRef.width;
const logoOptions: QrcodeLogo = Object.assign(
{
logoSize: 0.15,
bgColor: "#ffffff",
borderSize: 0.05,
crossOrigin: "anonymous",
borderRadius: 8,
logoRadius: 0
},
isString(props.logo) ? {} : props.logo
);
const {
logoSize = 0.15,
bgColor = "#ffffff",
borderSize = 0.05,
crossOrigin = "anonymous",
borderRadius = 8,
logoRadius = 0
} = logoOptions;
const logoSrc = isString(props.logo) ? props.logo : props.logo.src;
const logoWidth = canvasWidth * logoSize;
const logoXY = (canvasWidth * (1 - logoSize)) / 2;
const logoBgWidth = canvasWidth * (logoSize + borderSize);
const logoBgXY = (canvasWidth * (1 - logoSize - borderSize)) / 2;
const ctx = canvasRef.getContext("2d");
if (!ctx) return;
// logo 底色
canvasRoundRect(ctx)(
logoBgXY,
logoBgXY,
logoBgWidth,
logoBgWidth,
borderRadius
);
ctx.fillStyle = bgColor;
ctx.fill();
// logo
const image = new Image();
if (crossOrigin || logoRadius) {
image.setAttribute("crossOrigin", crossOrigin);
}
(image as any).src = logoSrc;
// 使用image绘制可以避免某些跨域情况
const drawLogoWithImage = (image: HTMLImageElement) => {
ctx.drawImage(image, logoXY, logoXY, logoWidth, logoWidth);
};
// 使用canvas绘制以获得更多的功能
const drawLogoWithCanvas = (image: HTMLImageElement) => {
const canvasImage = document.createElement("canvas");
canvasImage.width = logoXY + logoWidth;
canvasImage.height = logoXY + logoWidth;
const imageCanvas = canvasImage.getContext("2d");
if (!imageCanvas || !ctx) return;
imageCanvas.drawImage(image, logoXY, logoXY, logoWidth, logoWidth);
canvasRoundRect(ctx)(logoXY, logoXY, logoWidth, logoWidth, logoRadius);
if (!ctx) return;
const fillStyle = ctx.createPattern(canvasImage, "no-repeat");
if (fillStyle) {
ctx.fillStyle = fillStyle;
ctx.fill();
}
};
// 将 logo绘制到 canvas上
return new Promise((resolve: any) => {
image.onload = () => {
logoRadius ? drawLogoWithCanvas(image) : drawLogoWithImage(image);
resolve(canvasRef.toDataURL());
};
});
};
// 得到原QrCode的大小以便缩放得到正确的QrCode大小
const getOriginWidth = async (
content: string,
options: QRCodeRenderersOptions
) => {
const _canvas = document.createElement("canvas");
await toCanvas(_canvas, content, options);
return _canvas.width;
};
// 对于内容少的QrCode增大容错率
const getErrorCorrectionLevel = (content: string) => {
if (content.length > 36) {
return "M";
} else if (content.length > 16) {
return "Q";
} else {
return "H";
}
};
// 用于绘制圆角
const canvasRoundRect = (ctx: CanvasRenderingContext2D) => {
return (x: number, y: number, w: number, h: number, r: number) => {
const minSize = Math.min(w, h);
if (r > minSize / 2) {
r = minSize / 2;
}
ctx.beginPath();
ctx.moveTo(x + r, y);
ctx.arcTo(x + w, y, x + w, y + h, r);
ctx.arcTo(x + w, y + h, x, y + h, r);
ctx.arcTo(x, y + h, x, y, r);
ctx.arcTo(x, y, x + w, y, r);
ctx.closePath();
return ctx;
};
};
const clickCode = () => {
emit("click");
};
const disabledClick = () => {
emit("disabled-click");
};
return () => (
<>
<div
v-loading={unref(loading)}
class="qrcode relative inline-block"
style={unref(wrapStyle)}
>
{props.tag === "canvas" ? (
<canvas ref={wrapRef} onClick={clickCode}></canvas>
) : (
<img ref={wrapRef} onClick={clickCode}></img>
)}
{props.disabled && (
<div
class="qrcode--disabled absolute top-0 left-0 flex w-full h-full items-center justify-center"
onClick={disabledClick}
>
<div class="absolute top-[50%] left-[50%] font-bold">
<iconify-icon-offline
class="cursor-pointer"
icon={RefreshRight}
width="30"
color="var(--el-color-primary)"
/>
<div>{props.disabledText}</div>
</div>
</div>
)}
</div>
</>
);
}
});

View File

@@ -0,0 +1,7 @@
import reSeamlessScroll from "./src/index.vue";
import { withInstall } from "@pureadmin/utils";
/** 无缝滚动组件 */
export const ReSeamlessScroll = withInstall(reSeamlessScroll);
export default ReSeamlessScroll;

View File

@@ -0,0 +1,538 @@
<script setup lang="ts">
import {
type PropType,
type CSSProperties,
ref,
unref,
nextTick,
computed
} from "vue";
import {
tryOnMounted,
tryOnUnmounted,
templateRef,
useDebounceFn
} from "@vueuse/core";
import * as utilsMethods from "./utils";
const { animationFrame, copyObj } = utilsMethods;
animationFrame();
defineOptions({
name: "ReSeamlessScroll"
});
const props = defineProps({
data: {
type: Array as PropType<unknown>
},
classOption: {
type: Object as PropType<unknown>
}
});
const emit = defineEmits<{
(e: "scrollEnd"): void;
}>();
const xPos = ref<number>(0);
const yPos = ref<number>(0);
const delay = ref<number>(0);
const height = ref<number>(0);
// 外容器宽度
const width = ref<number>(0);
// 内容实际宽度
const realBoxWidth = ref<number>(0);
const realBoxHeight = ref<number>(0);
const copyHtml = ref("");
// single 单步滚动的定时器
let singleWaitTime = null;
// move动画的animationFrame定时器
let reqFrame = null;
let startPos = null;
//记录touchStart时候的posY
let startPosY = null;
//记录touchStart时候的posX
let startPosX = null;
// mouseenter mouseleave 控制scrollMove()的开关
let isHover = false;
let ease = "ease-in";
if (props.classOption["key"] === undefined) {
// eslint-disable-next-line vue/no-mutating-props
props.classOption["key"] = 0;
}
const wrap = templateRef<HTMLElement | null>(
`wrap${props.classOption["key"]}`,
null
);
const slotList = templateRef<HTMLElement | null>(
`slotList${props.classOption["key"]}`,
null
);
const realBox = templateRef<HTMLElement | null>(
`realBox${props.classOption["key"]}`,
null
);
const leftSwitchState = computed(() => {
return unref(xPos) < 0;
});
const rightSwitchState = computed(() => {
return Math.abs(unref(xPos)) < unref(realBoxWidth) - unref(width);
});
const defaultOption = computed(() => {
return {
//步长
step: 1,
//启动无缝滚动最小数据数
limitMoveNum: 5,
//是否启用鼠标hover控制
hoverStop: true,
// bottom 往下 top 往上(默认) left 向左 right 向右
direction: "top",
//开启移动端touch
openTouch: true,
//单条数据高度有值hoverStop关闭
singleHeight: 0,
//单条数据宽度有值hoverStop关闭
singleWidth: 0,
//单步停止等待时间
waitTime: 1000,
switchOffset: 30,
autoPlay: true,
navigation: false,
switchSingleStep: 134,
switchDelay: 400,
switchDisabledClass: "disabled",
// singleWidth/singleHeight 是否开启rem度量
isSingleRemUnit: false
};
});
const options = computed(() => {
// @ts-expect-error
return copyObj({}, unref(defaultOption), props.classOption);
});
const leftSwitchClass = computed(() => {
return unref(leftSwitchState) ? "" : unref(options).switchDisabledClass;
});
const rightSwitchClass = computed(() => {
return unref(rightSwitchState) ? "" : unref(options).switchDisabledClass;
});
const leftSwitch = computed((): CSSProperties => {
return {
position: "absolute",
margin: `${unref(height) / 2}px 0 0 -${unref(options).switchOffset}px`,
transform: "translate(-100%,-50%)"
};
});
const rightSwitch = computed((): CSSProperties => {
return {
position: "absolute",
margin: `${unref(height) / 2}px 0 0 ${
unref(width) + unref(options).switchOffset
}px`,
transform: "translateY(-50%)"
};
});
const isHorizontal = computed(() => {
return (
unref(options).direction !== "bottom" && unref(options).direction !== "top"
);
});
const float = computed((): CSSProperties => {
return unref(isHorizontal)
? { float: "left", overflow: "hidden" }
: { overflow: "hidden" };
});
const pos = computed(() => {
return {
transform: `translate(${unref(xPos)}px,${unref(yPos)}px)`,
transition: `all ${ease} ${unref(delay)}ms`,
overflow: "hidden"
};
});
const navigation = computed(() => {
return unref(options).navigation;
});
const autoPlay = computed(() => {
if (unref(navigation)) return false;
return unref(options).autoPlay;
});
const scrollSwitch = computed(() => {
// 从 props 解构出来的 属性 不再具有响应性.
return (props.data as any).length >= unref(options).limitMoveNum;
});
const hoverStopSwitch = computed(() => {
return unref(options).hoverStop && unref(autoPlay) && unref(scrollSwitch);
});
const canTouchScroll = computed(() => {
return unref(options).openTouch;
});
const baseFontSize = computed(() => {
return unref(options).isSingleRemUnit
? parseInt(window.getComputedStyle(document.documentElement, null).fontSize)
: 1;
});
const realSingleStopWidth = computed(() => {
return unref(options).singleWidth * unref(baseFontSize);
});
const realSingleStopHeight = computed(() => {
return unref(options).singleHeight * unref(baseFontSize);
});
const step = computed(() => {
let singleStep;
const step = unref(options).step;
if (unref(isHorizontal)) {
singleStep = unref(realSingleStopWidth);
} else {
singleStep = unref(realSingleStopHeight);
}
if (singleStep > 0 && singleStep % step > 0) {
throw "如果设置了单步滚动step需是单步大小的约数否则无法保证单步滚动结束的位置是否准确";
}
return step;
});
function reset() {
xPos.value = 0;
yPos.value = 0;
scrollCancle();
scrollInitMove();
}
function leftSwitchClick() {
if (!unref(leftSwitchState)) return;
// 小于单步距离
if (Math.abs(unref(xPos)) < unref(options).switchSingleStep) {
xPos.value = 0;
return;
}
xPos.value += unref(options).switchSingleStep;
}
function rightSwitchClick() {
if (!unref(rightSwitchState)) return;
// 小于单步距离
if (
unref(realBoxWidth) - unref(width) + unref(xPos) <
unref(options).switchSingleStep
) {
xPos.value = unref(width) - unref(realBoxWidth);
return;
}
xPos.value -= unref(options).switchSingleStep;
}
function scrollCancle() {
cancelAnimationFrame(reqFrame || "");
}
function touchStart(e) {
if (!unref(canTouchScroll)) return;
let timer;
//touches数组对象获得屏幕上所有的touch取第一个touch
const touch = e.targetTouches[0];
const { waitTime, singleHeight, singleWidth } = unref(options);
//取第一个touch的坐标值
startPos = {
x: touch.pageX,
y: touch.pageY
};
//记录touchStart时候的posY
startPosY = unref(yPos);
//记录touchStart时候的posX
startPosX = unref(xPos);
if (!!singleHeight && !!singleWidth) {
if (timer) clearTimeout(timer);
timer = setTimeout(() => {
scrollCancle();
}, waitTime + 20);
} else {
scrollCancle();
}
}
function touchMove(e) {
//当屏幕有多个touch或者页面被缩放过就不执行move操作
if (
!unref(canTouchScroll) ||
e.targetTouches.length > 1 ||
(e.scale && e.scale !== 1)
)
return;
const touch = e.targetTouches[0];
const { direction } = unref(options);
const endPos = {
x: touch.pageX - startPos.x,
y: touch.pageY - startPos.y
};
//阻止触摸事件的默认行为,即阻止滚屏
e.preventDefault();
//dir1表示纵向滑动0为横向滑动
const dir = Math.abs(endPos.x) < Math.abs(endPos.y) ? 1 : 0;
if (
(dir === 1 && direction === "bottom") ||
(dir === 1 && direction === "top")
) {
// 表示纵向滑动 && 运动方向为上下
yPos.value = startPosY + endPos.y;
} else if (
(dir === 0 && direction === "left") ||
(dir === 0 && direction === "right")
) {
// 为横向滑动 && 运动方向为左右
xPos.value = startPosX + endPos.x;
}
}
function touchEnd() {
if (!unref(canTouchScroll)) return;
let timer: any;
const direction = unref(options).direction;
delay.value = 50;
if (direction === "top") {
if (unref(yPos) > 0) yPos.value = 0;
} else if (direction === "bottom") {
const h = (unref(realBoxHeight) / 2) * -1;
if (unref(yPos) < h) yPos.value = h;
} else if (direction === "left") {
if (unref(xPos) > 0) xPos.value = 0;
} else if (direction === "right") {
const w = unref(realBoxWidth) * -1;
if (unref(xPos) < w) xPos.value = w;
}
if (timer) clearTimeout(timer);
timer = setTimeout(() => {
delay.value = 0;
scrollMove();
}, unref(delay));
}
function enter() {
if (unref(hoverStopSwitch)) scrollStopMove();
}
function leave() {
if (unref(hoverStopSwitch)) scrollStartMove();
}
function scrollMove() {
// 鼠标移入时拦截scrollMove()
if (isHover) return;
//进入move立即先清除动画 防止频繁touchMove导致多动画同时进行
// scrollCancle();
reqFrame = requestAnimationFrame(function () {
//实际高度
const h = unref(realBoxHeight) / 2;
//宽度
const w = unref(realBoxWidth) / 2;
const { direction, waitTime } = unref(options);
if (direction === "top") {
// 上
if (Math.abs(unref(yPos)) >= h) {
emit("scrollEnd");
yPos.value = 0;
}
yPos.value -= step.value;
} else if (direction === "bottom") {
// 下
if (unref(yPos) >= 0) {
emit("scrollEnd");
yPos.value = h * -1;
}
yPos.value += step.value;
} else if (direction === "left") {
// 左
if (Math.abs(unref(xPos)) >= w) {
emit("scrollEnd");
xPos.value = 0;
}
xPos.value -= step.value;
} else if (direction === "right") {
// 右
if (unref(xPos) >= 0) {
emit("scrollEnd");
xPos.value = w * -1;
}
xPos.value += step.value;
}
if (singleWaitTime) clearTimeout(singleWaitTime);
if (unref(realSingleStopHeight)) {
//是否启动了单行暂停配置
if (Math.abs(unref(yPos)) % unref(realSingleStopHeight) < unref(step)) {
// 符合条件暂停waitTime
singleWaitTime = setTimeout(() => {
scrollMove();
}, waitTime);
} else {
scrollMove();
}
} else if (unref(realSingleStopWidth)) {
if (Math.abs(unref(xPos)) % unref(realSingleStopWidth) < unref(step)) {
// 符合条件暂停waitTime
singleWaitTime = setTimeout(() => {
scrollMove();
}, waitTime);
} else {
scrollMove();
}
} else {
scrollMove();
}
});
}
function scrollInitMove() {
nextTick(() => {
const { switchDelay } = unref(options);
//清空copy
copyHtml.value = "";
if (unref(isHorizontal)) {
height.value = unref(wrap).offsetHeight;
width.value = unref(wrap).offsetWidth;
let slotListWidth = unref(slotList).offsetWidth;
// 水平滚动设置warp width
if (unref(autoPlay)) {
// 修正offsetWidth四舍五入
slotListWidth = slotListWidth * 2 + 1;
}
unref(realBox).style.width = slotListWidth + "px";
realBoxWidth.value = slotListWidth;
}
if (unref(autoPlay)) {
ease = "ease-in";
delay.value = 0;
} else {
ease = "linear";
delay.value = switchDelay;
return;
}
// 是否可以滚动判断
if (unref(scrollSwitch)) {
let timer;
if (timer) clearTimeout(timer);
copyHtml.value = unref(slotList).innerHTML;
setTimeout(() => {
realBoxHeight.value = unref(realBox)?.offsetHeight;
scrollMove();
}, 0);
} else {
scrollCancle();
yPos.value = xPos.value = 0;
}
});
}
function scrollStartMove() {
//开启scrollMove
isHover = false;
scrollMove();
}
function scrollStopMove() {
//关闭scrollMove
isHover = true;
// 防止频频hover进出单步滚动,导致定时器乱掉
if (singleWaitTime) clearTimeout(singleWaitTime);
scrollCancle();
}
// 鼠标滚轮事件
function wheel(e) {
if (
unref(options).direction === "left" ||
unref(options).direction === "right"
)
return;
useDebounceFn(() => {
e.deltaY > 0 ? (yPos.value -= step.value) : (yPos.value += step.value);
}, 50)();
}
// watchEffect(() => {
// const watchData = data;
// if (!watchData) return;
// nextTick(() => {
// reset();
// });
// const watchAutoPlay = unref(autoPlay);
// if (watchAutoPlay) {
// reset();
// } else {
// scrollStopMove();
// }
// });
tryOnMounted(() => {
scrollInitMove();
});
tryOnUnmounted(() => {
scrollCancle();
clearTimeout(singleWaitTime);
});
defineExpose({
reset
});
</script>
<template>
<div :ref="'wrap' + classOption['key']">
<div
v-if="navigation"
:style="leftSwitch"
:class="leftSwitchClass"
@click="leftSwitchClick"
>
<slot name="left-switch" />
</div>
<div
v-if="navigation"
:style="rightSwitch"
:class="rightSwitchClass"
@click="rightSwitchClick"
>
<slot name="right-switch" />
</div>
<div
:ref="'realBox' + classOption['key']"
:style="pos"
@mouseenter="enter"
@mouseleave="leave"
@touchstart.passive="touchStart"
@touchmove.passive="touchMove"
@touchend="touchEnd"
@mousewheel.passive="wheel"
>
<div :ref="'slotList' + classOption['key']" :style="float">
<slot />
</div>
<div :style="float" v-html="copyHtml" />
</div>
</div>
</template>

View File

@@ -0,0 +1,115 @@
/**
* @desc AnimationFrame简单兼容hack
*/
export const animationFrame = () => {
window.cancelAnimationFrame = (() => {
return (
window.cancelAnimationFrame ||
window.webkitCancelAnimationFrame ||
window.mozCancelAnimationFrame ||
window.oCancelAnimationFrame ||
window.msCancelAnimationFrame ||
function (id) {
return window.clearTimeout(id);
}
);
})();
window.requestAnimationFrame = (function () {
return (
window.requestAnimationFrame ||
window.webkitRequestAnimationFrame ||
window.mozRequestAnimationFrame ||
window.oRequestAnimationFrame ||
window.msRequestAnimationFrame ||
function (callback) {
return window.setTimeout(callback, 1000 / 60);
}
);
})();
};
/**
* @desc 判断数组是否相等
* @return {Boolean}
* @param arr1
* @param arr2
*/
export const arrayEqual = (arr1: Array<any>, arr2: Array<any>) => {
if (arr1 === arr2) return true;
if (arr1.length !== arr2.length) return false;
for (let i = 0; i < arr1.length; ++i) {
if (arr1[i] !== arr2[i]) return false;
}
return true;
};
/**
* @desc 深浅合并拷贝
*/
export function copyObj() {
if (!Array.isArray) {
// @ts-expect-error
Array.isArray = function (arg) {
return Object.prototype.toString.call(arg) === "[object Array]";
};
}
let name,
options,
src,
copy,
copyIsArray,
clone,
i = 1,
target = arguments[0] || {}, // 使用||运算符排除隐式强制类型转换为false的数据类型
deep = false,
len = arguments.length;
if (typeof target === "boolean") {
deep = target;
target = arguments[1] || {};
i++;
}
if (typeof target !== "object" && typeof target !== "function") {
target = {};
}
// 如果arguments.length === 1 或typeof arguments[0] === 'boolean',且存在arguments[1]则直接返回target对象
if (i === len) {
return target;
}
for (; i < len; i++) {
//所以如果源对象中数据类型为Undefined或Null那么就会跳过本次循环接着循环下一个源对象
if ((options = arguments[i]) != null) {
// 如果遇到源对象的数据类型为Boolean, Number for in循环会被跳过不执行for in循环// src用于判断target对象是否存在name属性
for (name in options) {
// src用于判断target对象是否存在name属性
src = target[name];
// 需要复制的属性当前源对象的name属性
copy = options[name];
// 判断copy是否是数组
copyIsArray = Array.isArray(copy);
// 如果是深复制且copy是一个对象或数组则需要递归直到copy成为一个基本数据类型为止
if (deep && copy && (typeof copy === "object" || copyIsArray)) {
if (copyIsArray) {
copyIsArray = false;
// 如果目标对象存在name属性且是一个数组
// 则使用目标对象的name属性否则重新创建一个数组用于复制
clone = src && Array.isArray(src) ? src : [];
} else {
// 如果目标对象存在name属性且是一个对象则使用目标对象的name属性否则重新创建一个对象用于复制
clone = src && typeof src === "object" ? src : {};
}
// 深复制所以递归调用copyObject函数
// 返回值为target对象即clone对象
// copy是一个源对象
// @ts-expect-error
target[name] = copyObj(deep, clone, copy);
} else if (copy !== undefined) {
// 浅复制直接复制到target对象上
target[name] = copy;
}
}
}
}
return target;
}

View File

@@ -0,0 +1,8 @@
import reSegmented from "./src/index";
import { withInstall } from "@pureadmin/utils";
/** 分段控制器组件 */
export const ReSegmented = withInstall(reSegmented);
export default ReSegmented;
export type { OptionsType } from "./src/type";

View File

@@ -0,0 +1,157 @@
.pure-segmented {
--pure-control-padding-horizontal: 12px;
--pure-control-padding-horizontal-sm: 8px;
--pure-segmented-track-padding: 2px;
--pure-segmented-line-width: 1px;
--pure-segmented-border-radius-small: 4px;
--pure-segmented-border-radius-base: 6px;
--pure-segmented-border-radius-large: 8px;
box-sizing: border-box;
display: inline-block;
padding: var(--pure-segmented-track-padding);
font-size: var(--el-font-size-base);
color: rgba(0, 0, 0, 0.65);
background-color: rgb(0 0 0 / 4%);
border-radius: var(--pure-segmented-border-radius-base);
}
.pure-segmented-block {
display: flex;
}
.pure-segmented-block .pure-segmented-item {
flex: 1;
min-width: 0;
}
.pure-segmented-block .pure-segmented-item > .pure-segmented-item-label > span {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
/* small */
.pure-segmented.pure-segmented--small {
border-radius: var(--pure-segmented-border-radius-small);
}
.pure-segmented.pure-segmented--small .pure-segmented-item {
border-radius: var(--el-border-radius-small);
}
.pure-segmented.pure-segmented--small .pure-segmented-item > div {
min-height: calc(
var(--el-component-size-small) - var(--pure-segmented-track-padding) * 2
);
line-height: calc(
var(--el-component-size-small) - var(--pure-segmented-track-padding) * 2
);
padding: 0
calc(
var(--pure-control-padding-horizontal-sm) -
var(--pure-segmented-line-width)
);
}
/* large */
.pure-segmented.pure-segmented--large {
border-radius: var(--pure-segmented-border-radius-large);
}
.pure-segmented.pure-segmented--large .pure-segmented-item {
border-radius: calc(
var(--el-border-radius-base) + var(--el-border-radius-small)
);
}
.pure-segmented.pure-segmented--large .pure-segmented-item > div {
min-height: calc(
var(--el-component-size-large) - var(--pure-segmented-track-padding) * 2
);
line-height: calc(
var(--el-component-size-large) - var(--pure-segmented-track-padding) * 2
);
padding: 0
calc(
var(--pure-control-padding-horizontal) - var(--pure-segmented-line-width)
);
font-size: var(--el-font-size-medium);
}
/* default */
.pure-segmented-item {
position: relative;
text-align: center;
cursor: pointer;
border-radius: var(--el-border-radius-base);
transition: all 0.1s cubic-bezier(0.645, 0.045, 0.355, 1);
}
.pure-segmented .pure-segmented-item > div {
min-height: calc(
var(--el-component-size) - var(--pure-segmented-track-padding) * 2
);
line-height: calc(
var(--el-component-size) - var(--pure-segmented-track-padding) * 2
);
padding: 0
calc(
var(--pure-control-padding-horizontal) - var(--pure-segmented-line-width)
);
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
transition: 0.1s;
}
.pure-segmented-group {
position: relative;
display: flex;
align-items: stretch;
justify-items: flex-start;
width: 100%;
}
.pure-segmented-item-selected {
position: absolute;
top: 0;
left: 0;
box-sizing: border-box;
display: none;
width: 0;
height: 100%;
padding: 4px 0;
background-color: #fff;
border-radius: 4px;
box-shadow:
0 2px 8px -2px rgb(0 0 0 / 5%),
0 1px 4px -1px rgb(0 0 0 / 7%),
0 0 1px rgb(0 0 0 / 7%);
transition:
transform 0.5s cubic-bezier(0.645, 0.045, 0.355, 1),
width 0.5s cubic-bezier(0.645, 0.045, 0.355, 1);
will-change: transform, width;
}
.pure-segmented-item > input {
position: absolute;
inset-block-start: 0;
inset-inline-start: 0;
width: 0;
height: 0;
opacity: 0;
pointer-events: none;
}
.pure-segmented-item-label {
display: flex;
align-items: center;
justify-content: center;
}
.pure-segmented-item-icon svg {
width: 16px;
height: 16px;
}
.pure-segmented-item-disabled {
color: rgba(0, 0, 0, 0.25);
cursor: not-allowed;
}

View File

@@ -0,0 +1,216 @@
import "./index.css";
import type { OptionsType } from "./type";
import { useRenderIcon } from "@/components/ReIcon/src/hooks";
import {
useDark,
isNumber,
isFunction,
useResizeObserver
} from "@pureadmin/utils";
import {
type PropType,
h,
ref,
toRef,
watch,
nextTick,
defineComponent,
getCurrentInstance
} from "vue";
const props = {
options: {
type: Array<OptionsType>,
default: () => []
},
/** 默认选中,按照第一个索引为 `0` 的模式,可选(`modelValue`只有传`number`类型时才为响应式) */
modelValue: {
type: undefined,
require: false,
default: "0"
},
/** 将宽度调整为父元素宽度 */
block: {
type: Boolean,
default: false
},
/** 控件尺寸 */
size: {
type: String as PropType<"small" | "default" | "large">
},
/** 是否全局禁用,默认 `false` */
disabled: {
type: Boolean,
default: false
},
/** 当内容发生变化时,设置 `resize` 可使其自适应容器位置 */
resize: {
type: Boolean,
default: false
}
};
export default defineComponent({
name: "ReSegmented",
props,
emits: ["change", "update:modelValue"],
setup(props, { emit }) {
const width = ref(0);
const translateX = ref(0);
const { isDark } = useDark();
const initStatus = ref(false);
const curMouseActive = ref(-1);
const segmentedItembg = ref("");
const instance = getCurrentInstance()!;
const curIndex = isNumber(props.modelValue)
? toRef(props, "modelValue")
: ref(0);
function handleChange({ option, index }, event: Event) {
if (props.disabled || option.disabled) return;
event.preventDefault();
isNumber(props.modelValue)
? emit("update:modelValue", index)
: (curIndex.value = index);
segmentedItembg.value = "";
emit("change", { index, option });
}
function handleMouseenter({ option, index }, event: Event) {
if (props.disabled) return;
event.preventDefault();
curMouseActive.value = index;
if (option.disabled || curIndex.value === index) {
segmentedItembg.value = "";
} else {
segmentedItembg.value = isDark.value
? "#1f1f1f"
: "rgba(0, 0, 0, 0.06)";
}
}
function handleMouseleave(_, event: Event) {
if (props.disabled) return;
event.preventDefault();
curMouseActive.value = -1;
}
function handleInit(index = curIndex.value) {
nextTick(() => {
const curLabelRef = instance?.proxy?.$refs[`labelRef${index}`] as ElRef;
if (!curLabelRef) return;
width.value = curLabelRef.clientWidth;
translateX.value = curLabelRef.offsetLeft;
initStatus.value = true;
});
}
function handleResizeInit() {
useResizeObserver(".pure-segmented", () => {
nextTick(() => {
handleInit(curIndex.value);
});
});
}
(props.block || props.resize) && handleResizeInit();
watch(
() => curIndex.value,
index => {
nextTick(() => {
handleInit(index);
});
},
{
immediate: true
}
);
watch(() => props.size, handleResizeInit, {
immediate: true
});
const rendLabel = () => {
return props.options.map((option, index) => {
return (
<label
ref={`labelRef${index}`}
class={[
"pure-segmented-item",
(props.disabled || option?.disabled) &&
"pure-segmented-item-disabled"
]}
style={{
background:
curMouseActive.value === index ? segmentedItembg.value : "",
color: props.disabled
? null
: !option.disabled &&
(curIndex.value === index || curMouseActive.value === index)
? isDark.value
? "rgba(255, 255, 255, 0.85)"
: "rgba(0,0,0,.88)"
: ""
}}
onMouseenter={event => handleMouseenter({ option, index }, event)}
onMouseleave={event => handleMouseleave({ option, index }, event)}
onClick={event => handleChange({ option, index }, event)}
>
<input type="radio" name="segmented" />
<div
class="pure-segmented-item-label"
v-tippy={{
content: option?.tip,
zIndex: 41000
}}
>
{option.icon && !isFunction(option.label) ? (
<span
class="pure-segmented-item-icon"
style={{ marginRight: option.label ? "6px" : 0 }}
>
{h(
useRenderIcon(option.icon, {
...option?.iconAttrs
})
)}
</span>
) : null}
{option.label ? (
isFunction(option.label) ? (
h(option.label)
) : (
<span>{option.label}</span>
)
) : null}
</div>
</label>
);
});
};
return () => (
<div
class={{
"pure-segmented": true,
"pure-segmented-block": props.block,
"pure-segmented--large": props.size === "large",
"pure-segmented--small": props.size === "small"
}}
>
<div class="pure-segmented-group">
<div
class="pure-segmented-item-selected"
style={{
width: `${width.value}px`,
transform: `translateX(${translateX.value}px)`,
display: initStatus.value ? "block" : "none"
}}
></div>
{rendLabel()}
</div>
</div>
);
}
});

View File

@@ -0,0 +1,20 @@
import type { VNode, Component } from "vue";
import type { iconType } from "@/components/ReIcon/src/types.ts";
export interface OptionsType {
/** 文字 */
label?: string | (() => VNode | Component);
/**
* @description 图标,采用平台内置的 `useRenderIcon` 函数渲染
* @see {@link 用法参考 https://pure-admin.github.io/pure-admin-doc/pages/icon/#%E9%80%9A%E7%94%A8%E5%9B%BE%E6%A0%87-userendericon-hooks }
*/
icon?: string | Component;
/** 图标属性、样式配置 */
iconAttrs?: iconType;
/** 值 */
value?: any;
/** 是否禁用 */
disabled?: boolean;
/** `tooltip` 提示 */
tip?: string;
}

View File

@@ -0,0 +1,7 @@
import reSelector from "./src";
import { withInstall } from "@pureadmin/utils";
/** 选择器组件 */
export const ReSelector = withInstall(reSelector);
export default ReSelector;

View File

@@ -0,0 +1,28 @@
.hs-rate__icon {
font-size: 18px;
transition: 0.3s;
}
.hs-item {
width: 30px;
height: 30px;
box-sizing: border-box;
line-height: 30px;
}
.hs-on {
background-color: #409eff;
border-radius: 50%;
}
.hs-range {
background-color: #f2f6fc;
}
.both-left-sides {
border-radius: 50% 0 0 50%;
}
.both-right-sides {
border-radius: 0 50% 50% 0;
}

View File

@@ -0,0 +1,327 @@
import "./index.css";
import {
unref,
computed,
nextTick,
onBeforeMount,
defineComponent,
getCurrentInstance
} from "vue";
import { addClass, removeClass, toggleClass } from "@pureadmin/utils";
const stayClass = "stay"; //鼠标点击
const activeClass = "hs-on"; //鼠标移动上去
const voidClass = "hs-off"; //鼠标移开
const inRange = "hs-range"; //当前选中的两个元素之间的背景
const bothLeftSides = "both-left-sides";
const bothRightSides = "both-right-sides";
let selectedDirection = "right"; //默认从左往右,索引变大
let overList = [];
// 存放第一个选中的元素和最后一个选中元素,只能存放这两个元素
let selectedList = [];
const props = {
HsKey: {
type: Number || String,
default: 0
},
disabled: {
type: Boolean,
default: false
},
value: {
type: Number,
default: 0
},
max: {
type: Array,
default() {
return [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
}
},
// 回显数据的索引长度必须是2
echo: {
type: Array,
default() {
return [];
}
}
};
export default defineComponent({
name: "ReSelector",
props,
emits: ["selectedVal"],
setup(props, { emit }) {
const instance = getCurrentInstance();
const currentValue = props.value;
const rateDisabled = computed(() => {
return props.disabled;
});
const classes = computed(() => {
const result = [];
let i = 0;
let threshold = currentValue;
if (currentValue !== Math.floor(currentValue)) {
threshold--;
}
for (; i < threshold; i++) {
result.push(activeClass);
}
for (; i < props.max.length; i++) {
result.push(voidClass);
}
return result;
});
// 鼠标移入
const setCurrentValue = index => {
if (props.disabled) return;
// 当选中一个元素后,开始添加背景色
if (selectedList.length === 1) {
if (overList.length < 1) overList.push({ index });
let firstIndex = overList[0].index;
// 往右走,索引变大
if (index > firstIndex) {
selectedDirection = "right";
toggleClass(
false,
bothRightSides,
document.querySelector(".hs-select__item" + selectedList[0].index)
);
while (index >= firstIndex) {
addClass(
document.querySelector(".hs-select__item" + firstIndex),
inRange
);
firstIndex++;
}
} else {
selectedDirection = "left";
toggleClass(
true,
bothRightSides,
document.querySelector(".hs-select__item" + selectedList[0].index)
);
while (index <= firstIndex) {
addClass(
document.querySelector(".hs-select__item" + firstIndex),
inRange
);
firstIndex--;
}
}
}
addClass(document.querySelector("." + voidClass + index), activeClass);
};
// 鼠标离开
const resetCurrentValue = index => {
if (props.disabled) return;
// 移除先检查是否选中 选中则返回false 不移除
const currentHsDom = document.querySelector("." + voidClass + index);
if (currentHsDom.className.includes(stayClass)) {
return false;
} else {
removeClass(currentHsDom, activeClass);
}
// 当选中一个元素后,开始移除背景色
if (selectedList.length === 1) {
const firstIndex = overList[0].index;
if (index >= firstIndex) {
for (let i = 0; i <= index; i++) {
removeClass(
document.querySelector(".hs-select__item" + i),
inRange
);
}
} else {
while (index <= firstIndex) {
removeClass(
document.querySelector(".hs-select__item" + index),
inRange
);
index++;
}
}
}
};
// 鼠标点击
const selectValue = (index, item) => {
if (props.disabled) return;
const len = selectedList.length;
if (len < 2) {
selectedList.push({ item, index });
addClass(document.querySelector("." + voidClass + index), stayClass);
addClass(
document.querySelector(".hs-select__item" + selectedList[0].index),
bothLeftSides
);
if (selectedList[1]) {
if (selectedDirection === "right") {
addClass(
document.querySelector(
".hs-select__item" + selectedList[1].index
),
bothRightSides
);
} else {
addClass(
document.querySelector(
".hs-select__item" + selectedList[1].index
),
bothLeftSides
);
}
}
if (len === 1) {
// 顺时针排序
if (selectedDirection === "right") {
emit("selectedVal", {
left: selectedList[0].item,
right: selectedList[1].item,
whole: selectedList
});
} else {
emit("selectedVal", {
left: selectedList[1].item,
right: selectedList[0].item,
whole: selectedList
});
}
}
} else {
nextTick(() => {
selectedList.forEach(v => {
removeClass(
document.querySelector("." + voidClass + v.index),
activeClass,
stayClass
);
removeClass(
document.querySelector(".hs-select__item" + v.index),
bothLeftSides,
bothRightSides
);
});
selectedList = [];
overList = [];
for (let i = 0; i <= props.max.length; i++) {
const currentDom = document.querySelector(".hs-select__item" + i);
if (currentDom) {
removeClass(currentDom, inRange);
}
}
selectedList.push({ item, index });
addClass(document.querySelector("." + voidClass + index), stayClass);
addClass(
document.querySelector(".hs-select__item" + selectedList[0].index),
bothLeftSides
);
});
}
};
// 回显数据
const echoView = item => {
if (item.length === 0) return;
if (item.length > 2 || item.length === 1) {
throw "传入的数组长度必须是2";
}
item.sort((a, b) => {
return a - b;
});
addClass(
instance.refs["hsdiv" + props.HsKey + item[0]] as Element,
activeClass,
stayClass
);
addClass(
instance.refs["hstd" + props.HsKey + item[0]] as Element,
bothLeftSides
);
addClass(
instance.refs["hsdiv" + props.HsKey + item[1]] as Element,
activeClass,
stayClass
);
addClass(
instance.refs["hstd" + props.HsKey + item[1]] as Element,
bothRightSides
);
while (item[1] >= item[0]) {
addClass(
instance.refs["hstd" + props.HsKey + item[0]] as Element,
inRange
);
item[0]++;
}
};
onBeforeMount(() => {
nextTick(() => {
echoView(props.echo);
});
});
return () => (
<>
<table cellspacing="0" cellpadding="0">
<tbody>
<tr>
{props.max.map((item, key) => {
return (
<td
data-index={props.HsKey}
ref={`hstd${props.HsKey}${key}`}
class={`hs-select__item${key}`}
onMousemove={() => setCurrentValue(key)}
onMouseleave={() => resetCurrentValue(key)}
onClick={() => selectValue(key, item)}
style={{
cursor: unref(rateDisabled) ? "auto" : "pointer",
textAlign: "center"
}}
key={key}
>
<div
ref={`hsdiv${props.HsKey}${key}`}
class={`hs-item ${[unref(classes)[key] + key]}`}
>
<span>{item}</span>
</div>
</td>
);
})}
</tr>
</tbody>
</table>
</>
);
}
});

View File

@@ -0,0 +1,23 @@
@font-face {
font-family: "iconfont"; /* Project id 3268330 */
src:
url("iconfont.woff2?t=1647939915215") format("woff2"),
url("iconfont.woff?t=1647939915215") format("woff"),
url("iconfont.ttf?t=1647939915215") format("truetype");
}
.iconfont {
font-family: "iconfont" !important;
font-size: 16px;
font-style: normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.icon-tuozhuai1:before {
content: "\e647";
}
.icon-tuozhuai1-copy:before {
content: "\eda3";
}

View File

@@ -0,0 +1,65 @@
!(function (e) {
var t,
n,
c,
o,
s,
i =
'<svg><symbol id="icon-tuozhuai1" viewBox="0 0 1024 1024"><path d="M576 896c0-35.2 28.8-64 64-64s64 28.8 64 64-28.8 64-64 64-64-28.8-64-64z m-256 0c0-35.2 28.8-64 64-64s64 28.8 64 64-28.8 64-64 64-64-28.8-64-64z m256-192c0-35.2 28.8-64 64-64s64 28.8 64 64-28.8 64-64 64-64-28.8-64-64z m-256 0c0-35.2 28.8-64 64-64s64 28.8 64 64-28.8 64-64 64-64-28.8-64-64z m256-192c0-35.2 28.8-64 64-64s64 28.8 64 64-28.8 64-64 64-64-28.8-64-64z m-256 0c0-35.2 28.8-64 64-64s64 28.8 64 64-28.8 64-64 64-64-28.8-64-64z m256-192c0-35.2 28.8-64 64-64s64 28.8 64 64-28.8 64-64 64-64-28.8-64-64z m-256 0c0-35.2 28.8-64 64-64s64 28.8 64 64-28.8 64-64 64-64-28.8-64-64z m256-192c0-35.2 28.8-64 64-64s64 28.8 64 64-28.8 64-64 64-64-28.8-64-64z m-256 0c0-35.2 28.8-64 64-64s64 28.8 64 64-28.8 64-64 64-64-28.8-64-64z" fill="#2c2c2c" ></path></symbol><symbol id="icon-tuozhuai1-copy" viewBox="0 0 1024 1024"><path d="M128 576c35.2 0 64 28.8 64 64s-28.8 64-64 64-64-28.8-64-64 28.8-64 64-64z m0-256c35.2 0 64 28.8 64 64s-28.8 64-64 64-64-28.8-64-64 28.8-64 64-64z m192 256c35.2 0 64 28.8 64 64s-28.8 64-64 64-64-28.8-64-64 28.8-64 64-64z m0-256c35.2 0 64 28.8 64 64s-28.8 64-64 64-64-28.8-64-64 28.8-64 64-64z m192 256.00000001c35.2 0 64 28.8 64 63.99999999s-28.8 64-64 64-64-28.8-64-64 28.8-64 64-63.99999999z m0-256.00000001c35.2 0 64 28.8 64 64s-28.8 64-64 63.99999999-64-28.8-64-63.99999999 28.8-64 64-64z m192 256c35.2 0 64 28.8 64 64s-28.8 64-64 64-64-28.8-64-64 28.8-64 64-64z m0-256c35.2 0 64 28.8 64 64s-28.8 64-64 64-64-28.8-64-64 28.8-64 64-64z m192 256c35.2 0 64 28.8 64 64s-28.8 64-64 64-64-28.8-64-64 28.8-64 64-64z m0-256c35.2 0 64 28.8 64 64s-28.8 64-64 64-64-28.8-64-64 28.8-64 64-64z" fill="#2c2c2c" ></path></symbol></svg>',
d = (d = document.getElementsByTagName("script"))[
d.length - 1
].getAttribute("data-injectcss"),
m = function (e, t) {
t.parentNode.insertBefore(e, t);
};
if (d && !e.__iconfont__svg__cssinject__) {
e.__iconfont__svg__cssinject__ = !0;
try {
document.write(
"<style>.svgfont {display: inline-block;width: 1em;height: 1em;fill: currentColor;vertical-align: -0.1em;font-size:16px;}</style>"
);
} catch (e) {
console && console.log(e);
}
}
function l() {
s || ((s = !0), c());
}
function a() {
try {
o.documentElement.doScroll("left");
} catch (e) {
return void setTimeout(a, 50);
}
l();
}
(t = function () {
var e,
t = document.createElement("div");
(t.innerHTML = i),
(i = null),
(t = t.getElementsByTagName("svg")[0]) &&
(t.setAttribute("aria-hidden", "true"),
(t.style.position = "absolute"),
(t.style.width = 0),
(t.style.height = 0),
(t.style.overflow = "hidden"),
(t = t),
(e = document.body).firstChild ? m(t, e.firstChild) : e.appendChild(t));
}),
document.addEventListener
? ~["complete", "loaded", "interactive"].indexOf(document.readyState)
? setTimeout(t, 0)
: ((n = function () {
document.removeEventListener("DOMContentLoaded", n, !1), t();
}),
document.addEventListener("DOMContentLoaded", n, !1))
: document.attachEvent &&
((c = t),
(o = e.document),
(s = !1),
a(),
(o.onreadystatechange = function () {
"complete" == o.readyState && ((o.onreadystatechange = null), l());
}));
})(window);

View File

@@ -0,0 +1,23 @@
{
"id": "3268330",
"name": "split",
"font_family": "iconfont",
"css_prefix_text": "icon-",
"description": "",
"glyphs": [
{
"icon_id": "22378774",
"name": "拖拽",
"font_class": "tuozhuai1",
"unicode": "e647",
"unicode_decimal": 58951
},
{
"icon_id": "23570521",
"name": "拖拽",
"font_class": "tuozhuai1-copy",
"unicode": "eda3",
"unicode_decimal": 60835
}
]
}

View File

@@ -0,0 +1,49 @@
.clearfix::after {
visibility: hidden;
display: block;
font-size: 0;
content: " ";
clear: both;
height: 0;
}
.vue-splitter-container {
height: 100%;
position: relative;
}
.vue-splitter-container-mask {
z-index: 9999;
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
}
.splitter-pane.vertical.splitter-paneL {
position: absolute;
left: 0;
height: 100%;
padding-right: 3px;
}
.splitter-pane.vertical.splitter-paneR {
position: absolute;
right: 0;
height: 100%;
padding-left: 3px;
}
.splitter-pane.horizontal.splitter-paneL {
position: absolute;
top: 0;
width: 100%;
}
.splitter-pane.horizontal.splitter-paneR {
position: absolute;
bottom: 0;
width: 100%;
padding-top: 3px;
}

View File

@@ -0,0 +1,136 @@
import "./index.css";
import resizer from "./resizer";
import { type PropType, defineComponent, ref, unref, computed } from "vue";
export interface ContextProps {
minPercent: number;
defaultPercent: number;
split: string;
}
/** 切割面板组件 */
export default defineComponent({
name: "SplitPane",
components: { resizer },
props: {
splitSet: {
type: Object as PropType<ContextProps>,
require: true
}
},
emits: ["resize"],
setup(props, ctx) {
const active = ref(false);
const hasMoved = ref(false);
const percent = ref(props.splitSet?.defaultPercent);
const type = props.splitSet?.split === "vertical" ? "width" : "height";
const resizeType = props.splitSet?.split === "vertical" ? "left" : "top";
const leftClass = ref([
"splitter-pane splitter-paneL",
props.splitSet?.split
]);
const rightClass = ref([
"splitter-pane splitter-paneR",
props.splitSet?.split
]);
const cursor = computed(() => {
return active.value
? props.splitSet?.split === "vertical"
? { cursor: "col-resize" }
: { cursor: "row-resize" }
: { cursor: "default" };
});
const onClick = (): void => {
if (!hasMoved.value) {
percent.value = 50;
ctx.emit("resize", percent.value);
}
};
const onMouseDown = (): void => {
active.value = true;
hasMoved.value = false;
};
const onMouseUp = (): void => {
active.value = false;
};
const onMouseMove = (e: any): void => {
if (e.buttons === 0 || e.which === 0) {
active.value = false;
}
if (active.value) {
let offset = 0;
let target = e.currentTarget;
if (props.splitSet?.split === "vertical") {
while (target) {
offset += target.offsetLeft;
target = target.offsetParent;
}
} else {
while (target) {
offset += target.offsetTop;
target = target.offsetParent;
}
}
const currentPage =
props.splitSet?.split === "vertical" ? e.pageX : e.pageY;
const targetOffset =
props.splitSet?.split === "vertical"
? e.currentTarget.offsetWidth
: e.currentTarget.offsetHeight;
const percents =
Math.floor(((currentPage - offset) / targetOffset) * 10000) / 100;
if (
percents > props.splitSet?.minPercent &&
percents < 100 - props.splitSet?.minPercent
) {
percent.value = percents;
}
ctx.emit("resize", percent.value);
hasMoved.value = true;
}
};
return () => (
<>
<div
class="vue-splitter-container clearfix"
style={unref(cursor)}
onMouseup={() => onMouseUp()}
onMousemove={() => onMouseMove(event)}
>
<div
class={unref(leftClass)}
style={{ [unref(type)]: unref(percent) + "%" }}
>
{ctx.slots.paneL()}
</div>
<resizer
style={`${unref([resizeType])}:${unref(percent)}%`}
split={props.splitSet?.split}
onMousedown={() => onMouseDown()}
onClick={() => onClick()}
></resizer>
<div
class={unref(rightClass)}
style={{ [unref(type)]: 100 - unref(percent) + "%" }}
>
{ctx.slots.paneR()}
</div>
<div v-show={unref(active)} class="vue-splitter-container-mask"></div>
</div>
</>
);
}
});

View File

@@ -0,0 +1,47 @@
@import "./iconfont/iconfont.css";
.splitter-pane-resizer {
box-sizing: border-box;
background: #000;
position: absolute;
opacity: 0.2;
z-index: 1;
background-clip: padding;
background-clip: padding-box;
}
.splitter-pane-resizer.horizontal {
height: 6px;
width: 100%;
background: #e5e6eb;
cursor: row-resize;
}
.splitter-pane-resizer.horizontal:before {
content: "\eda3";
font-family: "iconfont";
font-size: 13px;
color: #000;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.splitter-pane-resizer.vertical {
width: 6px;
height: 100%;
background: #e5e6eb;
cursor: col-resize;
}
.splitter-pane-resizer.vertical:before {
content: "\e647";
font-family: "iconfont";
font-size: 13px;
color: #000;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}

View File

@@ -0,0 +1,23 @@
import "./resizer.css";
import { computed, unref, defineComponent } from "vue";
export default defineComponent({
name: "Resizer",
props: {
split: {
type: String,
required: true
},
className: {
type: String,
default: ""
}
},
setup(props) {
const classes = computed(() => {
return ["splitter-pane-resizer", props.split, props.className].join(" ");
});
return () => <div class={unref(classes)}></div>;
}
});

View File

@@ -0,0 +1,7 @@
import reText from "./src/index.vue";
import { withInstall } from "@pureadmin/utils";
/** 支持`Tooltip`提示的文本省略组件 */
export const ReText = withInstall(reText);
export default ReText;

Some files were not shown because too many files have changed in this diff Show More