mirror of
https://github.com/m-xlsea/ruoyi-plus-soybean.git
synced 2025-09-24 07:49:47 +08:00
refactor(projects): 精简版+动态路由权限初步
This commit is contained in:
@ -1,45 +0,0 @@
|
||||
<template>
|
||||
<div ref="scrollbar" class="h-full text-left">
|
||||
<div ref="scrollbarContent" class="inline-block" :class="{ 'h-full': !isScrollY }">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch, onMounted } from 'vue';
|
||||
import BScroll from '@better-scroll/core';
|
||||
import type { Options } from '@better-scroll/core';
|
||||
import { useElementSize } from '@vueuse/core';
|
||||
|
||||
interface Props {
|
||||
/** better-scroll的配置: https://better-scroll.github.io/docs/zh-CN/guide/base-scroll-options.html */
|
||||
options: Options;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const scrollbar = ref<HTMLElement | null>(null);
|
||||
const bsInstance = ref<BScroll | null>(null);
|
||||
const scrollbarContent = ref<HTMLElement | null>(null);
|
||||
const isScrollY = computed(() => Boolean(props.options.scrollY));
|
||||
|
||||
function initBetterScroll() {
|
||||
bsInstance.value = new BScroll(scrollbar.value!, props.options);
|
||||
}
|
||||
|
||||
// 滚动元素发生变化,刷新BS
|
||||
const { width, height } = useElementSize(scrollbarContent);
|
||||
watch([() => width.value, () => height.value], () => {
|
||||
if (bsInstance.value) {
|
||||
bsInstance.value.refresh();
|
||||
}
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
initBetterScroll();
|
||||
});
|
||||
|
||||
defineExpose({ bsInstance });
|
||||
</script>
|
||||
<style scoped></style>
|
@ -1,66 +0,0 @@
|
||||
<template>
|
||||
<div
|
||||
class="relative flex-center h-30px pl-14px border-1px border-[#e5e7eb] dark:border-[#ffffff3d] rounded-2px transition-border-color duration-300 ease-in-out cursor-pointer"
|
||||
:class="[closable ? 'pr-6px' : 'pr-14px']"
|
||||
:style="buttonStyle"
|
||||
@mouseenter="setTrue"
|
||||
@mouseleave="setFalse"
|
||||
>
|
||||
<span class="whitespace-nowrap">
|
||||
<slot></slot>
|
||||
</span>
|
||||
<div v-if="closable" class="pl-10px">
|
||||
<icon-close :is-primary="isActive || isHover" :primary-color="primaryColor" @click="handleClose" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
import { useBoolean } from '@/hooks';
|
||||
import { IconClose } from '@/components';
|
||||
import { addColorAlpha } from '@/utils';
|
||||
|
||||
interface Props {
|
||||
/** 激活状态 */
|
||||
isActive?: boolean;
|
||||
/** 主题颜色 */
|
||||
primaryColor?: string;
|
||||
/** 是否显示关闭图标 */
|
||||
closable?: boolean;
|
||||
/** 暗黑模式 */
|
||||
darkMode?: boolean;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
isActive: false,
|
||||
primaryColor: '#409EFF',
|
||||
closable: true,
|
||||
darkMode: false
|
||||
});
|
||||
const emit = defineEmits<{
|
||||
/** 点击关闭图标 */
|
||||
(e: 'close'): void;
|
||||
}>();
|
||||
|
||||
const { bool: isHover, setTrue, setFalse } = useBoolean();
|
||||
|
||||
function handleClose(e: MouseEvent) {
|
||||
e.stopPropagation();
|
||||
emit('close');
|
||||
}
|
||||
|
||||
const buttonStyle = computed(() => {
|
||||
const style: { [key: string]: string } = {};
|
||||
if (props.isActive || isHover.value) {
|
||||
style.color = props.primaryColor;
|
||||
style.borderColor = addColorAlpha(props.primaryColor, 0.3);
|
||||
if (props.isActive) {
|
||||
const alpha = props.darkMode ? 0.15 : 0.1;
|
||||
style.backgroundColor = addColorAlpha(props.primaryColor, alpha);
|
||||
}
|
||||
}
|
||||
return style;
|
||||
});
|
||||
</script>
|
||||
<style scoped></style>
|
@ -1,79 +0,0 @@
|
||||
<template>
|
||||
<svg>
|
||||
<defs>
|
||||
<symbol id="geometry-left" viewBox="0 0 214 36">
|
||||
<path d="M17 0h197v36H0v-2c4.5 0 9-3.5 9-8V8c0-4.5 3.5-8 8-8z"></path>
|
||||
</symbol>
|
||||
<symbol id="geometry-right" viewBox="0 0 214 36">
|
||||
<use xlink:href="#geometry-left"></use>
|
||||
</symbol>
|
||||
<clipPath>
|
||||
<rect width="100%" height="100%" x="0"></rect>
|
||||
</clipPath>
|
||||
</defs>
|
||||
<svg width="52%" height="100%">
|
||||
<use
|
||||
xlink:href="#geometry-left"
|
||||
width="214"
|
||||
height="36"
|
||||
:fill="fill"
|
||||
class="transition-fill duration-300 ease-in-out"
|
||||
></use>
|
||||
</svg>
|
||||
<g transform="scale(-1, 1)">
|
||||
<svg width="52%" height="100%" x="-100%" y="0">
|
||||
<use
|
||||
xlink:href="#geometry-right"
|
||||
width="214"
|
||||
height="36"
|
||||
:fill="fill"
|
||||
class="transition-fill duration-300 ease-in-out"
|
||||
></use>
|
||||
</svg>
|
||||
</g>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { mixColor } from '@/utils';
|
||||
|
||||
interface Props {
|
||||
/** 激活状态 */
|
||||
isActive?: boolean;
|
||||
/** 鼠标悬浮状态 */
|
||||
isHover?: boolean;
|
||||
/** 主题颜色 */
|
||||
primaryColor?: string;
|
||||
/** 暗黑模式 */
|
||||
darkMode?: boolean;
|
||||
}
|
||||
|
||||
/** 填充的背景颜色: [默认颜色, 暗黑主题颜色] */
|
||||
type FillColor = [string, string];
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
isActive: false,
|
||||
isHover: false,
|
||||
primaryColor: '#409EFF',
|
||||
darkMode: false
|
||||
});
|
||||
|
||||
const defaultColor: FillColor = ['#fff', '#18181c'];
|
||||
const hoverColor: FillColor = ['#dee1e6', '#3f3c37'];
|
||||
const mixColors: FillColor = ['#ffffff', '#000000'];
|
||||
|
||||
const fill = computed(() => {
|
||||
const index = Number(props.darkMode);
|
||||
let color = defaultColor[index];
|
||||
if (props.isHover) {
|
||||
color = hoverColor[index];
|
||||
}
|
||||
if (props.isActive) {
|
||||
const alpha = props.darkMode ? 0.1 : 0.15;
|
||||
color = mixColor(mixColors[index], props.primaryColor, alpha);
|
||||
}
|
||||
return color;
|
||||
});
|
||||
</script>
|
||||
<style scoped></style>
|
@ -1,3 +0,0 @@
|
||||
import SvgRadiusBg from './SvgRadiusBg.vue';
|
||||
|
||||
export { SvgRadiusBg };
|
@ -1,66 +0,0 @@
|
||||
<template>
|
||||
<div
|
||||
class="relative flex-y-center h-34px px-24px cursor-pointer"
|
||||
:class="{ '-mr-18px': !isLast, 'z-10': isActive, 'z-9': isHover }"
|
||||
@mouseenter="setTrue"
|
||||
@mouseleave="setFalse"
|
||||
>
|
||||
<div class="absolute-lb wh-full overflow-hidden">
|
||||
<svg-radius-bg
|
||||
class="wh-full"
|
||||
:is-active="isActive"
|
||||
:is-hover="isHover"
|
||||
:dark-mode="darkMode"
|
||||
:primary-color="primaryColor"
|
||||
/>
|
||||
</div>
|
||||
<span class="relative whitespace-nowrap z-2">
|
||||
<slot></slot>
|
||||
</span>
|
||||
<div v-if="closable" class="pl-18px">
|
||||
<icon-close :is-primary="isActive" :primary-color="primaryColor" @click="handleClose" />
|
||||
</div>
|
||||
<n-divider v-if="!isHover && !isActive" :vertical="true" class="absolute right-0 !bg-[#a4abb8] z-2" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { NDivider } from 'naive-ui';
|
||||
import { useBoolean } from '@/hooks';
|
||||
import IconClose from '../IconClose/index.vue';
|
||||
import { SvgRadiusBg } from './components';
|
||||
|
||||
interface Props {
|
||||
/** 激活状态 */
|
||||
isActive?: boolean;
|
||||
/** 主题颜色 */
|
||||
primaryColor?: string;
|
||||
/** 是否显示关闭图标 */
|
||||
closable?: boolean;
|
||||
/** 暗黑模式 */
|
||||
darkMode?: boolean;
|
||||
/** 是否是最后一个 */
|
||||
isLast: boolean;
|
||||
}
|
||||
|
||||
withDefaults(defineProps<Props>(), {
|
||||
isActive: false,
|
||||
primaryColor: '#409EFF',
|
||||
closable: true,
|
||||
darkMode: false,
|
||||
isLast: false
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
/** 点击关闭图标 */
|
||||
(e: 'close'): void;
|
||||
}>();
|
||||
|
||||
const { bool: isHover, setTrue, setFalse } = useBoolean();
|
||||
|
||||
function handleClose(e: MouseEvent) {
|
||||
e.stopPropagation();
|
||||
emit('close');
|
||||
}
|
||||
</script>
|
||||
<style scoped></style>
|
@ -1,106 +0,0 @@
|
||||
<template>
|
||||
<span>{{ value }}</span>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, onMounted, watch, watchEffect } from 'vue';
|
||||
import { useTransition, TransitionPresets } from '@vueuse/core';
|
||||
import { isNumber } from '@/utils';
|
||||
|
||||
interface Props {
|
||||
/** 初始值 */
|
||||
startValue?: number;
|
||||
/** 结束值 */
|
||||
endValue?: number;
|
||||
/** 动画时长 */
|
||||
duration?: number;
|
||||
/** 自动动画 */
|
||||
autoplay?: boolean;
|
||||
/** 进制 */
|
||||
decimals?: number;
|
||||
/** 前缀 */
|
||||
prefix?: string;
|
||||
/** 后缀 */
|
||||
suffix?: string;
|
||||
/** 分割符号 */
|
||||
separator?: string;
|
||||
/** 小数点 */
|
||||
decimal?: string;
|
||||
/** 使用缓冲动画函数 */
|
||||
useEasing?: boolean;
|
||||
/** 缓冲动画函数类型 */
|
||||
transition?: string;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
startValue: 0,
|
||||
endValue: 2021,
|
||||
duration: 1500,
|
||||
autoplay: true,
|
||||
decimals: 0,
|
||||
prefix: '',
|
||||
suffix: '',
|
||||
separator: ',',
|
||||
decimal: '.',
|
||||
useEasing: true,
|
||||
transition: 'linear'
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'on-started'): void;
|
||||
(e: 'on-finished'): void;
|
||||
}>();
|
||||
|
||||
const source = ref(props.startValue);
|
||||
let outputValue = useTransition(source);
|
||||
const value = computed(() => formatNumber(outputValue.value));
|
||||
const disabled = ref(false);
|
||||
|
||||
function run() {
|
||||
outputValue = useTransition(source, {
|
||||
disabled,
|
||||
duration: props.duration,
|
||||
onStarted: () => emit('on-started'),
|
||||
onFinished: () => emit('on-finished'),
|
||||
...(props.useEasing ? { transition: TransitionPresets[props.transition] } : {})
|
||||
});
|
||||
}
|
||||
|
||||
function start() {
|
||||
run();
|
||||
source.value = props.endValue;
|
||||
}
|
||||
|
||||
function formatNumber(num: number | string) {
|
||||
if (!num) {
|
||||
return '';
|
||||
}
|
||||
const { decimals, decimal, separator, suffix, prefix } = props;
|
||||
let number = Number(num).toFixed(decimals);
|
||||
number += '';
|
||||
|
||||
const x = number.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;
|
||||
}
|
||||
|
||||
watch([() => props.startValue, () => props.endValue], () => {
|
||||
if (props.autoplay) {
|
||||
start();
|
||||
}
|
||||
});
|
||||
watchEffect(() => {
|
||||
source.value = props.startValue;
|
||||
});
|
||||
onMounted(() => {
|
||||
if (props.autoplay) {
|
||||
start();
|
||||
}
|
||||
});
|
||||
</script>
|
@ -1,15 +0,0 @@
|
||||
<template>
|
||||
<web-site-link label="github地址:" :link="link" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import WebSiteLink from '../WebSiteLink/index.vue';
|
||||
|
||||
interface Props {
|
||||
/** github链接 */
|
||||
link: string;
|
||||
}
|
||||
|
||||
defineProps<Props>();
|
||||
</script>
|
||||
<style scoped></style>
|
@ -1,35 +0,0 @@
|
||||
<template>
|
||||
<div
|
||||
class="relative flex-center w-18px h-18px text-14px"
|
||||
:style="{ color: isPrimary ? primaryColor : defaultColor }"
|
||||
@mouseenter="setTrue"
|
||||
@mouseleave="setFalse"
|
||||
>
|
||||
<transition name="transition-opacity">
|
||||
<icon-mdi:close-circle v-if="isHover" key="hover" class="absolute" />
|
||||
<icon-mdi:close v-else key="unhover" class="absolute" />
|
||||
</transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useBoolean } from '@/hooks';
|
||||
|
||||
interface Props {
|
||||
/** 激活状态 */
|
||||
isPrimary?: boolean;
|
||||
/** 主题颜色 */
|
||||
primaryColor?: string;
|
||||
/** 默认颜色 */
|
||||
defaultColor?: string;
|
||||
}
|
||||
|
||||
withDefaults(defineProps<Props>(), {
|
||||
isPrimary: false,
|
||||
primaryColor: '#409EFF',
|
||||
defaultColor: '#9ca3af'
|
||||
});
|
||||
|
||||
const { bool: isHover, setTrue, setFalse } = useBoolean();
|
||||
</script>
|
||||
<style scoped></style>
|
@ -1,37 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<canvas ref="domRef" width="152" height="40" class="cursor-pointer" @click="getImgCode"></canvas>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { watch } from 'vue';
|
||||
import { useImageVerify } from '@/hooks';
|
||||
|
||||
interface Props {
|
||||
code: string;
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:code', code: string): void;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
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>
|
||||
<style scoped></style>
|
@ -1,290 +0,0 @@
|
||||
<template>
|
||||
<div ref="mousewheelRef" class="h-full cursor-move">
|
||||
<svg ref="svgRef" class="w-full h-full select-none" @mousedown="dragStart">
|
||||
<g :style="{ transform }">
|
||||
<slot></slot>
|
||||
</g>
|
||||
</svg>
|
||||
<slot name="absolute"></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { reactive, ref, computed, watch, onMounted, onBeforeUnmount } from 'vue';
|
||||
import type { SScaleRange, STranslate, SPosition, SCoord, SNodeSize } from '@/interface';
|
||||
|
||||
interface Props {
|
||||
/** 缩放比例 */
|
||||
scale?: number;
|
||||
/** 缩放范围 */
|
||||
scaleRange?: SScaleRange;
|
||||
/** g标签相对于svg标签的左上角的偏移量 */
|
||||
translate?: STranslate;
|
||||
/** 节点尺寸 */
|
||||
nodeSize?: SNodeSize;
|
||||
/** 是否开启按坐标居中画布 */
|
||||
centerSvg?: boolean;
|
||||
/** 居中的坐标 */
|
||||
centerCoord?: SCoord;
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:scale', scale: number): void;
|
||||
(e: 'update:translate', translate: STranslate): void;
|
||||
}
|
||||
|
||||
interface SvgConfig {
|
||||
/** 距离可视区左边的距离 */
|
||||
left: number;
|
||||
/** 距离可视区顶部的距离 */
|
||||
top: number;
|
||||
/** svg画布宽 */
|
||||
width: number;
|
||||
/** svg画布高 */
|
||||
height: number;
|
||||
}
|
||||
|
||||
interface DragScale {
|
||||
/** 画布缩放比例 */
|
||||
scale: number;
|
||||
/** 画布缩放比例取值范围 */
|
||||
scaleRange: SScaleRange;
|
||||
/** 画布移动距离 */
|
||||
translate: STranslate;
|
||||
/** 是否在拖动 */
|
||||
isDragging: boolean;
|
||||
/** 拖动前的鼠标距离可视区左边和上边的距离 */
|
||||
lastPosition: SPosition;
|
||||
/** svg的属性 */
|
||||
svgConfig: SvgConfig;
|
||||
}
|
||||
|
||||
interface WheelDelta {
|
||||
wheelDelta: number;
|
||||
wheelDeltaX: number;
|
||||
wheelDeltaY: number;
|
||||
}
|
||||
|
||||
type DOMWheelEvent = WheelEvent & WheelDelta;
|
||||
|
||||
type WheelDirection = 'up' | 'down';
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
scale: 1,
|
||||
scaleRange: () => [0.2, 3],
|
||||
translate: () => ({ x: 0, y: 0 }),
|
||||
nodeSize: () => ({ w: 100, h: 100 }),
|
||||
centerSvg: false,
|
||||
centerCoord: () => ({ x: 0, y: 0 })
|
||||
});
|
||||
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
/** 最外层容器,用于鼠标滚轮事件 */
|
||||
const mousewheelRef = ref<HTMLElement>();
|
||||
|
||||
/** 基本属性 */
|
||||
const dragScale = reactive<DragScale>({
|
||||
scale: props.scale,
|
||||
scaleRange: [...props.scaleRange],
|
||||
translate: { ...props.translate },
|
||||
isDragging: false,
|
||||
lastPosition: {
|
||||
x: 0,
|
||||
y: 0
|
||||
},
|
||||
svgConfig: {
|
||||
left: 0,
|
||||
top: 0,
|
||||
width: 1000,
|
||||
height: 500
|
||||
}
|
||||
});
|
||||
function setDragScale(data: Partial<DragScale>) {
|
||||
Object.assign(dragScale, data);
|
||||
}
|
||||
|
||||
/** svg dom */
|
||||
const svgRef = ref<SVGElement | null>(null);
|
||||
function initSvgConfig() {
|
||||
if (svgRef.value) {
|
||||
const { left, top, width, height } = svgRef.value.getBoundingClientRect();
|
||||
setDragScale({ svgConfig: { left, top, width, height } });
|
||||
}
|
||||
}
|
||||
|
||||
/** 缩放和平移样式 */
|
||||
const transform = computed(() => {
|
||||
const { scale, translate } = dragScale;
|
||||
const { x, y } = translate;
|
||||
return `translate(${x}px, ${y}px) scale(${scale})`;
|
||||
});
|
||||
|
||||
/**
|
||||
* 更新偏移量
|
||||
* @param delta - 偏移量的增量
|
||||
*/
|
||||
function updateTranslate(delta: STranslate) {
|
||||
const { x, y } = dragScale.translate;
|
||||
const update = { x: x + delta.x, y: y + delta.y };
|
||||
setDragScale({ translate: update });
|
||||
}
|
||||
|
||||
/**
|
||||
* 缩放后将视图移动到鼠标的位置
|
||||
* @param mouseX - 鼠标x坐标
|
||||
* @param mouseY - 鼠标y坐标
|
||||
* @param oldScale - 缩放前的缩放比例
|
||||
*/
|
||||
function correctTranslate(mouseX: number, mouseY: number, oldScale: number) {
|
||||
const { scale, translate } = dragScale;
|
||||
const { x, y } = translate;
|
||||
const sourceCoord = {
|
||||
x: (mouseX - x) / oldScale,
|
||||
y: (mouseY - y) / oldScale
|
||||
};
|
||||
const sourceTranslate = {
|
||||
x: sourceCoord.x * (1 - scale),
|
||||
y: sourceCoord.y * (1 - scale)
|
||||
};
|
||||
const update = {
|
||||
x: sourceTranslate.x - (sourceCoord.x - mouseX),
|
||||
y: sourceTranslate.y - (sourceCoord.y - mouseY)
|
||||
};
|
||||
setDragScale({ translate: update });
|
||||
}
|
||||
|
||||
// 拖拽事件
|
||||
/** 拖拽开始 */
|
||||
function dragStart(e: MouseEvent) {
|
||||
if (e.button !== 0) {
|
||||
// 只允许鼠标点击左键拖动
|
||||
return;
|
||||
}
|
||||
const { clientX: x, clientY: y } = e;
|
||||
setDragScale({ isDragging: true, lastPosition: { x, y } });
|
||||
}
|
||||
/** 拖拽中 */
|
||||
function dragMove(e: MouseEvent) {
|
||||
if (dragScale.isDragging) {
|
||||
const { clientX: x, clientY: y } = e; // 当前鼠标的位置
|
||||
const { x: lX, y: lY } = dragScale.lastPosition; // 上一次鼠标的位置
|
||||
const delta = { x: x - lX, y: y - lY }; // 鼠标的偏移量
|
||||
updateTranslate(delta);
|
||||
setDragScale({ lastPosition: { x, y } });
|
||||
}
|
||||
}
|
||||
/** 拖拽结束 */
|
||||
function dragEnd() {
|
||||
setDragScale({ isDragging: false });
|
||||
}
|
||||
|
||||
/** 缩放事件 */
|
||||
function handleScale(e: WheelEvent, direction: WheelDirection) {
|
||||
const { clientX, clientY } = e;
|
||||
const { left, top } = dragScale.svgConfig;
|
||||
const mouseX = clientX - left;
|
||||
const mouseY = clientY - top;
|
||||
const { scale: oldScale, scaleRange } = dragScale;
|
||||
const [min, max] = scaleRange;
|
||||
const scaleParam = 0.045;
|
||||
const updateParam = direction === 'up' ? 1 + scaleParam : 1 - scaleParam;
|
||||
const newScale = oldScale * updateParam;
|
||||
if (newScale >= min && newScale <= max) {
|
||||
dragScale.scale = newScale;
|
||||
} else {
|
||||
dragScale.scale = newScale < min ? min : max;
|
||||
}
|
||||
correctTranslate(mouseX, mouseY, oldScale);
|
||||
}
|
||||
/** 鼠标滚轮缩放事件 */
|
||||
function handleMousewheel(e: WheelEvent) {
|
||||
e.preventDefault();
|
||||
const direction: WheelDirection = (e as DOMWheelEvent).wheelDeltaY > 0 ? 'up' : 'down';
|
||||
handleScale(e, direction);
|
||||
}
|
||||
|
||||
/** 监听拖拽事件 */
|
||||
function initDragEventListener() {
|
||||
window.addEventListener('mousemove', dragMove);
|
||||
window.addEventListener('mouseup', dragEnd);
|
||||
}
|
||||
|
||||
/** 监听鼠标滚轮事件 */
|
||||
function initMousewheelEventListener() {
|
||||
if (mousewheelRef.value) {
|
||||
mousewheelRef.value.addEventListener('wheel', handleMousewheel);
|
||||
}
|
||||
}
|
||||
|
||||
/** 卸载监听事件 */
|
||||
function destroyEventListener() {
|
||||
window.removeEventListener('mousemove', dragMove);
|
||||
window.removeEventListener('mouseup', dragEnd);
|
||||
if (mousewheelRef.value) {
|
||||
mousewheelRef.value.removeEventListener('wheel', handleMousewheel);
|
||||
}
|
||||
}
|
||||
|
||||
// 根据指定坐标居中布局
|
||||
function handleCenterSvg() {
|
||||
const { x, y } = props.centerCoord;
|
||||
const isCoordValid = !Number.isNaN(x) && !Number.isNaN(y);
|
||||
if (props.centerSvg && isCoordValid) {
|
||||
const { w, h } = props.nodeSize;
|
||||
const { width, height } = dragScale.svgConfig;
|
||||
const translate = { x: width / 2 - x - w / 2, y: height / 2 - y - h / 2 };
|
||||
setDragScale({ translate });
|
||||
} else {
|
||||
setDragScale({ translate: { x: 0, y: 0 } });
|
||||
}
|
||||
}
|
||||
|
||||
// 将scale和translate进行双向数据绑定
|
||||
watch(
|
||||
() => props.scale,
|
||||
newValue => {
|
||||
setDragScale({ scale: newValue });
|
||||
}
|
||||
);
|
||||
watch(
|
||||
() => props.translate,
|
||||
newValue => {
|
||||
setDragScale({ translate: newValue });
|
||||
}
|
||||
);
|
||||
watch(
|
||||
() => dragScale.scale,
|
||||
newValue => {
|
||||
emit('update:scale', newValue);
|
||||
}
|
||||
);
|
||||
watch(
|
||||
() => dragScale.translate,
|
||||
newValue => {
|
||||
emit('update:translate', newValue);
|
||||
}
|
||||
);
|
||||
// 监听centerCoord,居中画布
|
||||
watch([() => props.centerSvg, () => props.centerCoord], () => {
|
||||
handleCenterSvg();
|
||||
});
|
||||
|
||||
/** 初始化 */
|
||||
function init() {
|
||||
initDragEventListener();
|
||||
initMousewheelEventListener();
|
||||
initSvgConfig();
|
||||
handleCenterSvg();
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
init();
|
||||
});
|
||||
|
||||
// 卸载监听事件
|
||||
onBeforeUnmount(() => {
|
||||
destroyEventListener();
|
||||
});
|
||||
</script>
|
||||
<style scoped></style>
|
@ -1,124 +0,0 @@
|
||||
<template>
|
||||
<div class="flex-col-center select-none">
|
||||
<icon-mdi-plus-circle
|
||||
class="text-20px cursor-pointer"
|
||||
:style="{ color: themeColor }"
|
||||
@click="handleSliderValue('plus')"
|
||||
/>
|
||||
<div class="h-120px pr-4px">
|
||||
<n-slider
|
||||
v-model:value="sliderValue"
|
||||
:vertical="true"
|
||||
:tooltip="false"
|
||||
:style="`--rail-color: #efefef;--fill-color:${themeColor};--fill-color-hover:${themeColor}`"
|
||||
/>
|
||||
</div>
|
||||
<div class="absolute -right-40px h-20px" :style="{ bottom: sliderLabelBottom }">
|
||||
{{ sliderLabel }}
|
||||
</div>
|
||||
<icon-mdi-minus-circle
|
||||
class="text-20px cursor-pointer"
|
||||
:style="{ color: themeColor }"
|
||||
@click="handleSliderValue('minus')"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch } from 'vue';
|
||||
import { NSlider } from 'naive-ui';
|
||||
import type { SScaleRange, STranslate } from '@/interface';
|
||||
|
||||
interface Props {
|
||||
/** 主题颜色 */
|
||||
themeColor: string;
|
||||
/** 缩放比例 */
|
||||
scale?: number;
|
||||
/** 缩放范围 */
|
||||
scaleRange?: SScaleRange;
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:scale', scale: number): void;
|
||||
(e: 'update:translate', translate: STranslate): void;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
scale: 1,
|
||||
scaleRange: () => [0.2, 3]
|
||||
});
|
||||
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
const sliderValue = ref(getSliderValue());
|
||||
const sliderLabel = computed(() => formatSlider(sliderValue.value));
|
||||
const sliderLabelBottom = computed(() => getSliderLabelBottom(sliderValue.value));
|
||||
|
||||
function getSliderValue() {
|
||||
const {
|
||||
scale,
|
||||
scaleRange: [min, max]
|
||||
} = props;
|
||||
let value = 50;
|
||||
if (scale - 1 >= 0) {
|
||||
value = ((scale - 1) / (Number(max) - 1)) * 50 + 50;
|
||||
} else {
|
||||
value = ((scale - Number(min)) / (1 - Number(min))) * 50;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function getScale(sliderValue: number) {
|
||||
const [min, max] = props.scaleRange;
|
||||
let scale = 1;
|
||||
if (sliderValue >= 50) {
|
||||
scale = ((sliderValue - 50) / 50) * (Number(max) - 1) + 1;
|
||||
} else {
|
||||
scale = (sliderValue / 50) * (1 - Number(min)) + Number(min);
|
||||
}
|
||||
return scale;
|
||||
}
|
||||
|
||||
function handleSliderValue(type: 'plus' | 'minus') {
|
||||
let step = 10;
|
||||
if (sliderValue.value >= 50) {
|
||||
step = 5;
|
||||
}
|
||||
if (type === 'minus') {
|
||||
step *= -1;
|
||||
}
|
||||
const newValue = sliderValue.value + step;
|
||||
if (newValue >= 0 && newValue <= 100) {
|
||||
sliderValue.value = newValue;
|
||||
} else {
|
||||
sliderValue.value = newValue < 0 ? 0 : 100;
|
||||
}
|
||||
}
|
||||
|
||||
function formatSlider(sliderValue: number) {
|
||||
const scale = getScale(sliderValue);
|
||||
const percent = `${Math.round(scale * 100)}%`;
|
||||
return percent;
|
||||
}
|
||||
|
||||
function getSliderLabelBottom(sliderValue: number) {
|
||||
return `${19 + (102 * sliderValue) / 100}px`;
|
||||
}
|
||||
|
||||
watch(sliderValue, newValue => {
|
||||
const updateScale = getScale(newValue);
|
||||
emit('update:scale', updateScale);
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.scale,
|
||||
() => {
|
||||
sliderValue.value = getSliderValue();
|
||||
}
|
||||
);
|
||||
</script>
|
||||
<style scoped>
|
||||
:deep(.n-slider-rail) {
|
||||
width: 4px;
|
||||
}
|
||||
</style>
|
@ -1,80 +0,0 @@
|
||||
<template>
|
||||
<g>
|
||||
<path :d="line" style="fill: none" :style="lineStyle"></path>
|
||||
<path :d="arrow" style="stroke-width: 0" :style="arrowStyle"></path>
|
||||
</g>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import type { SCoord } from '@/interface';
|
||||
|
||||
interface Props {
|
||||
/** 边的起始坐标 */
|
||||
sourceCoord: SCoord;
|
||||
/** 边的终点坐标 */
|
||||
targetCoord: SCoord;
|
||||
/** 边的线宽 */
|
||||
width?: number;
|
||||
/** 填充颜色 */
|
||||
color?: string;
|
||||
/** 是否高亮 */
|
||||
highlight?: boolean;
|
||||
/** 高亮的颜色 */
|
||||
highlightColor?: string;
|
||||
/** 是否显示终点箭头 */
|
||||
showArrow?: boolean;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
width: 2,
|
||||
color: '#000',
|
||||
highlight: false,
|
||||
highlightColor: '#f00',
|
||||
showArrow: true
|
||||
});
|
||||
|
||||
const line = computed(() => {
|
||||
const {
|
||||
sourceCoord: { x: sX, y: sY },
|
||||
targetCoord: { x: tX, y: tY },
|
||||
showArrow
|
||||
} = props;
|
||||
const horizontalGap = Math.abs(sX - tX);
|
||||
const start = `M${sX} ${sY}`;
|
||||
const end = showArrow ? `${tX - 5} ${tY}` : `${tX} ${tY}`;
|
||||
const control1 = `C${sX + (horizontalGap * 2) / 3} ${sY}`;
|
||||
const control2 = `${tX - (horizontalGap * 2) / 3} ${tY}`;
|
||||
return `${start} ${control1} ${control2} ${end}`;
|
||||
});
|
||||
|
||||
const arrow = computed(() => {
|
||||
const { x, y } = props.targetCoord;
|
||||
const M = `M${x - 10} ${y}`;
|
||||
const L1 = `L ${x - 10} ${y - 5 + 0.2472}`;
|
||||
const A1 = `A 4 4 0 0 1 ${x - 10 + 0.178885} ${y - 5 + 0.08944}`;
|
||||
const L2 = `L ${x - 0.8944} ${y - 0.4472}`;
|
||||
const A2 = `A 5 5 0 0 1 ${x - 0.8944} ${y + 0.4472}`;
|
||||
const L3 = `L ${x - 10 + 0.178885} ${y + 5 - 0.08944}`;
|
||||
const A3 = `A 4 4 0 0 1 ${x - 10} ${y + 5 - 0.2472}`;
|
||||
return `${M} ${L1} ${A1} ${L2} ${A2} ${L3} ${A3}`;
|
||||
});
|
||||
|
||||
const lineStyle = computed(() => {
|
||||
const { highlight, highlightColor, color } = props;
|
||||
const stroke = highlight ? highlightColor : color;
|
||||
return {
|
||||
stroke,
|
||||
strokeWidth: props.width
|
||||
};
|
||||
});
|
||||
|
||||
const arrowStyle = computed(() => {
|
||||
const { highlight, highlightColor, color } = props;
|
||||
const fill = highlight ? highlightColor : color;
|
||||
return {
|
||||
fill
|
||||
};
|
||||
});
|
||||
</script>
|
||||
<style scoped></style>
|
@ -1,28 +0,0 @@
|
||||
<template>
|
||||
<g :transform="transform">
|
||||
<foreignObject :width="props.size.w" :height="props.size.h">
|
||||
<slot></slot>
|
||||
</foreignObject>
|
||||
</g>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import type { SNodeSize } from '@/interface';
|
||||
|
||||
interface Props {
|
||||
/** 节点尺寸 */
|
||||
size?: SNodeSize;
|
||||
/** 节点坐标 */
|
||||
x: number;
|
||||
/** 节点坐标 */
|
||||
y: number;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
size: () => ({ w: 100, h: 100 })
|
||||
});
|
||||
|
||||
const transform = computed(() => `translate(${props.x}, ${props.y})`);
|
||||
</script>
|
||||
<style scoped></style>
|
@ -1,6 +0,0 @@
|
||||
import DragScaleSvg from './DragScaleSvg.vue';
|
||||
import SvgNode from './SvgNode.vue';
|
||||
import SvgEdge from './SvgEdge.vue';
|
||||
import ScaleSlider from './ScaleSlider.vue';
|
||||
|
||||
export { DragScaleSvg, SvgNode, SvgEdge, ScaleSlider };
|
@ -1,123 +0,0 @@
|
||||
<template>
|
||||
<drag-scale-svg
|
||||
v-model:scale="dragScaleConfig.scale"
|
||||
v-model:translate="dragScaleConfig.translate"
|
||||
:scale-range="dragScaleConfig.scaleRange"
|
||||
:center-svg="centerSvg"
|
||||
:center-coord="centerCoord"
|
||||
>
|
||||
<g>
|
||||
<svg-edge
|
||||
v-for="(edge, index) in allEdges.unhighlights"
|
||||
:key="`unhighlight${index}`"
|
||||
v-bind="edge"
|
||||
:color="edgeColor"
|
||||
/>
|
||||
<svg-edge
|
||||
v-for="(edge, index) in allEdges.highlights"
|
||||
:key="`highlight${index}`"
|
||||
v-bind="edge"
|
||||
:color="edgeColor"
|
||||
:highlight="true"
|
||||
:highlight-color="highlightColor"
|
||||
/>
|
||||
</g>
|
||||
<g>
|
||||
<svg-node v-for="node in nodes" :key="node.id" :x="node.x" :y="node.y" :size="nodeSize">
|
||||
<slot name="node" v-bind="node"></slot>
|
||||
</svg-node>
|
||||
</g>
|
||||
<template #absolute>
|
||||
<scale-slider
|
||||
v-model:scale="dragScaleConfig.scale"
|
||||
v-model:translate="dragScaleConfig.translate"
|
||||
:theme-color="sliderColor"
|
||||
class="absolute bottom-56px transition-right duration-300 ease-in-out"
|
||||
:style="{ right: sliderRight + 'px' }"
|
||||
/>
|
||||
</template>
|
||||
</drag-scale-svg>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { reactive, computed } from 'vue';
|
||||
import type { SScaleRange, STranslate, SGraphNode, SGraphEdge, SNodeSize, SCoord } from '@/interface';
|
||||
import { DragScaleSvg, SvgNode, SvgEdge, ScaleSlider } from './components';
|
||||
|
||||
interface Props {
|
||||
/** 图的节点 */
|
||||
nodes: SGraphNode[];
|
||||
/** 图的关系线 */
|
||||
edges: SGraphEdge[];
|
||||
/** 节点尺寸 */
|
||||
nodeSize?: SNodeSize;
|
||||
/** 边的填充颜色 */
|
||||
edgeColor?: string;
|
||||
/** 高亮颜色 */
|
||||
highlightColor?: string;
|
||||
/** 需要高亮关系线的节点坐标 */
|
||||
highlightCoord?: SCoord;
|
||||
/** 锁放条的颜色 */
|
||||
sliderColor?: string;
|
||||
/** 缩放条距离父元素右边的距离 */
|
||||
sliderRight?: number;
|
||||
/** 是否开启按坐标居中画布 */
|
||||
centerSvg?: boolean;
|
||||
/** 居中的坐标 */
|
||||
centerCoord?: SCoord;
|
||||
}
|
||||
|
||||
/** 可缩放拖拽容器的配置属性 */
|
||||
interface GragScaleConfig {
|
||||
/** 缩放比例 */
|
||||
scale: number;
|
||||
/** 缩放范围 */
|
||||
scaleRange: SScaleRange;
|
||||
/** g标签相对于svg标签的左上角的偏移量 */
|
||||
translate: STranslate;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
nodeSize: () => ({ w: 100, h: 100 }),
|
||||
edgeColor: '#000',
|
||||
highlightColor: '#fadb14',
|
||||
highlightCoord: () => ({ x: NaN, y: NaN }),
|
||||
sliderColor: '#000',
|
||||
sliderRight: 48,
|
||||
centerSvg: false,
|
||||
centerCoord: () => ({ x: 0, y: 0 })
|
||||
});
|
||||
|
||||
const dragScaleConfig = reactive<GragScaleConfig>({
|
||||
scale: 1,
|
||||
scaleRange: [0.2, 3],
|
||||
translate: { x: 0, y: 0 }
|
||||
});
|
||||
|
||||
/** 区分是否高亮的边 */
|
||||
const allEdges = computed(() => {
|
||||
const {
|
||||
edges,
|
||||
highlightCoord: { x: hX, y: hY },
|
||||
nodeSize: { w, h }
|
||||
} = props;
|
||||
const highlights: SGraphEdge[] = [];
|
||||
const unhighlights: SGraphEdge[] = [];
|
||||
edges.forEach(edge => {
|
||||
const { x: sX, y: sY } = edge.sourceCoord;
|
||||
const { x: tX, y: tY } = edge.targetCoord;
|
||||
const isSourceHighlight = hX === sX - w && hY + h / 2 === sY;
|
||||
const isTargetHighlight = hX === tX && hY + h / 2 === tY;
|
||||
if (isSourceHighlight || isTargetHighlight) {
|
||||
highlights.push(edge);
|
||||
} else {
|
||||
unhighlights.push(edge);
|
||||
}
|
||||
});
|
||||
return {
|
||||
highlights,
|
||||
unhighlights
|
||||
};
|
||||
});
|
||||
</script>
|
||||
<style scoped></style>
|
@ -1,40 +0,0 @@
|
||||
<template>
|
||||
<hover-container class="w-40px h-full text-14px text-[#999] hover:text-primary" @click="toggleDarkMode">
|
||||
<icon-mdi-moon-waning-crescent v-if="dark" />
|
||||
<icon-mdi-white-balance-sunny v-else />
|
||||
</hover-container>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { watch } from 'vue';
|
||||
import { HoverContainer } from '../../common';
|
||||
import { useBoolean } from '@/hooks';
|
||||
|
||||
interface Props {
|
||||
/** 暗黑模式 */
|
||||
dark?: boolean;
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update', darkMode: boolean): void;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
dark: false
|
||||
});
|
||||
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
const { bool: darkMode, setBool: setDarkMode, toggle: toggleDarkMode } = useBoolean(props.dark);
|
||||
|
||||
watch(
|
||||
() => props.dark,
|
||||
newValue => {
|
||||
setDarkMode(newValue);
|
||||
}
|
||||
);
|
||||
watch(darkMode, newValue => {
|
||||
emit('update', newValue);
|
||||
});
|
||||
</script>
|
||||
<style scoped></style>
|
@ -1,20 +0,0 @@
|
||||
<template>
|
||||
<p>
|
||||
<span>{{ label }}</span>
|
||||
<a class="text-blue-500" :href="link" target="_blank">
|
||||
{{ link }}
|
||||
</a>
|
||||
</p>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
interface Props {
|
||||
/** 网址名称 */
|
||||
label: string;
|
||||
/** 网址链接 */
|
||||
link: string;
|
||||
}
|
||||
|
||||
defineProps<Props>();
|
||||
</script>
|
||||
<style scoped></style>
|
@ -1,11 +0,0 @@
|
||||
import CountTo from './CountTo/index.vue';
|
||||
import IconClose from './IconClose/index.vue';
|
||||
import ButtonTab from './ButtonTab/index.vue';
|
||||
import ChromeTab from './ChromeTab/index.vue';
|
||||
import BetterScroll from './BetterScroll/index.vue';
|
||||
import WebSiteLink from './WebSiteLink/index.vue';
|
||||
import GithubLink from './GithubLink/index.vue';
|
||||
import ThemeSwitch from './ThemeSwitch/index.vue';
|
||||
import ImageVerify from './ImageVerify/index.vue';
|
||||
|
||||
export { CountTo, IconClose, ButtonTab, ChromeTab, BetterScroll, WebSiteLink, GithubLink, ThemeSwitch, ImageVerify };
|
Reference in New Issue
Block a user