refactor(projects): 精简版+动态路由权限初步

This commit is contained in:
Soybean
2022-01-03 22:20:10 +08:00
parent 7a0648dba5
commit de2057f141
354 changed files with 2053 additions and 22117 deletions

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -1,3 +0,0 @@
import SvgRadiusBg from './SvgRadiusBg.vue';
export { SvgRadiusBg };

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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 };

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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 };