mirror of
https://github.com/m-xlsea/ruoyi-plus-soybean.git
synced 2025-09-24 07:49:47 +08:00
feat(projects): 添加常用组件、composables函数
This commit is contained in:
37
src/components/custom/ImageVerify/index.vue
Normal file
37
src/components/custom/ImageVerify/index.vue
Normal file
@ -0,0 +1,37 @@
|
||||
<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>
|
290
src/components/custom/SGraph/components/DragScaleSvg.vue
Normal file
290
src/components/custom/SGraph/components/DragScaleSvg.vue
Normal file
@ -0,0 +1,290 @@
|
||||
<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>
|
124
src/components/custom/SGraph/components/ScaleSlider.vue
Normal file
124
src/components/custom/SGraph/components/ScaleSlider.vue
Normal file
@ -0,0 +1,124 @@
|
||||
<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>
|
80
src/components/custom/SGraph/components/SvgEdge.vue
Normal file
80
src/components/custom/SGraph/components/SvgEdge.vue
Normal file
@ -0,0 +1,80 @@
|
||||
<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>
|
28
src/components/custom/SGraph/components/SvgNode.vue
Normal file
28
src/components/custom/SGraph/components/SvgNode.vue
Normal file
@ -0,0 +1,28 @@
|
||||
<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>
|
6
src/components/custom/SGraph/components/index.ts
Normal file
6
src/components/custom/SGraph/components/index.ts
Normal file
@ -0,0 +1,6 @@
|
||||
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 };
|
123
src/components/custom/SGraph/index.vue
Normal file
123
src/components/custom/SGraph/index.vue
Normal file
@ -0,0 +1,123 @@
|
||||
<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>
|
@ -6,5 +6,6 @@ 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 };
|
||||
export { CountTo, IconClose, ButtonTab, ChromeTab, BetterScroll, WebSiteLink, GithubLink, ThemeSwitch, ImageVerify };
|
||||
|
Reference in New Issue
Block a user