mirror of
https://github.com/soybeanjs/soybean-admin.git
synced 2025-09-22 03:26:38 +08:00
feat(projects): 添加常用组件、composables函数
This commit is contained in:
parent
e755caabf2
commit
230a50a4cf
8
.env
8
.env
@ -1,5 +1,9 @@
|
|||||||
# 变量需要以VITE开头
|
# 变量需要以VITE开头
|
||||||
|
|
||||||
VITE_APP_TITLE=SoybeanAdmin
|
|
||||||
VITE_APP_TITLE_LABEL=SoybeanAdmin
|
|
||||||
VITE_BASE_URL=/
|
VITE_BASE_URL=/
|
||||||
|
|
||||||
|
VITE_APP_NAME=SoybeanAdmin
|
||||||
|
|
||||||
|
VITE_APP_TITLE=SoybeanAdmin
|
||||||
|
|
||||||
|
VITE_APP_DESC=中后台管理系统模版
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
#请求的环境
|
#请求的环境
|
||||||
VITE_HTTP_ENV=DEV
|
VITE_HTTP_ENV=DEV
|
||||||
|
|
||||||
#请求地址
|
#请求地址
|
||||||
VITE_HTTP_URL=https://test.aisuit.com.cn
|
VITE_HTTP_URL=https://test.aisuit.com.cn
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
#请求的环境 正式环境
|
#请求的环境 正式环境
|
||||||
VITE_HTTP_ENV=PROD
|
VITE_HTTP_ENV=PROD
|
||||||
|
|
||||||
#请求地址
|
#请求地址
|
||||||
VITE_HTTP_URL=http://192.168.100.43:8201
|
VITE_HTTP_URL=http://192.168.100.43:8201
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
VITE_HTTP_ENV=STAGING
|
VITE_HTTP_ENV=STAGING
|
||||||
|
|
||||||
#请求地址
|
#请求地址
|
||||||
VITE_HTTP_URL=http://192.168.100.43:8201
|
VITE_HTTP_URL=http://192.168.100.43:8201
|
||||||
|
@ -12,3 +12,4 @@ lib
|
|||||||
/docs
|
/docs
|
||||||
.vscode
|
.vscode
|
||||||
.local
|
.local
|
||||||
|
index.html
|
||||||
|
@ -5,8 +5,8 @@ export default [
|
|||||||
minifyHtml(),
|
minifyHtml(),
|
||||||
injectHtml({
|
injectHtml({
|
||||||
injectData: {
|
injectData: {
|
||||||
title: viteEnv.VITE_APP_TITLE,
|
appName: viteEnv.VITE_APP_NAME,
|
||||||
appName: viteEnv.VITE_APP_TITLE_LABEL
|
appTitle: viteEnv.VITE_APP_TITLE
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
];
|
];
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" href="/favicon.ico" />
|
<link rel="icon" href="/favicon.ico" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title><%= title %></title>
|
<title><%= appName %></title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="appProvider" style="display: none"></div>
|
<div id="appProvider" style="display: none"></div>
|
||||||
@ -20,7 +20,7 @@
|
|||||||
<i class="right bottom delay-1200"></i>
|
<i class="right bottom delay-1200"></i>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<h2 class="app-loading_title"><%= appName %></h2>
|
<h2 class="app-loading_title"><%= appTitle %></h2>
|
||||||
<style>
|
<style>
|
||||||
@import '/resource/loading.css';
|
@import '/resource/loading.css';
|
||||||
</style>
|
</style>
|
||||||
|
44
package.json
44
package.json
@ -12,11 +12,11 @@
|
|||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"dev:prod": "vite --mode production",
|
"dev:prod": "vite --mode production",
|
||||||
"dev:staging": "vite --mode staging",
|
"dev:staging": "vite --mode staging",
|
||||||
"build": "vue-tsc --noEmit --skipLibCheck && vite build",
|
|
||||||
"build:dev": "vue-tsc --noEmit --skipLibCheck && vite build --mode development",
|
|
||||||
"build:staging": "vue-tsc --noEmit --skipLibCheck && vite build --mode staging",
|
|
||||||
"serve": "vite preview",
|
|
||||||
"vtsc": "vue-tsc --noEmit --skipLibCheck",
|
"vtsc": "vue-tsc --noEmit --skipLibCheck",
|
||||||
|
"build": "npm run vtsc && vite build",
|
||||||
|
"build:dev": "npm run vtsc && vite build --mode development",
|
||||||
|
"build:staging": "npm run vtsc && vite build --mode staging",
|
||||||
|
"serve": "vite preview",
|
||||||
"lint": "eslint ./src --ext .vue,.js,jsx,.ts,tsx",
|
"lint": "eslint ./src --ext .vue,.js,jsx,.ts,tsx",
|
||||||
"lint:fix": "eslint --fix ./src --ext .vue,.js,jsx,.ts,tsx",
|
"lint:fix": "eslint --fix ./src --ext .vue,.js,jsx,.ts,tsx",
|
||||||
"prepare": "husky install",
|
"prepare": "husky install",
|
||||||
@ -31,21 +31,21 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@antv/g2plot": "^2.3.40",
|
"@antv/g2plot": "^2.4.0",
|
||||||
"@better-scroll/core": "^2.4.2",
|
"@better-scroll/core": "^2.4.2",
|
||||||
"@vueuse/core": "^7.1.2",
|
"@vueuse/core": "^7.3.0",
|
||||||
"axios": "^0.24.0",
|
"axios": "^0.24.0",
|
||||||
"chroma-js": "^2.1.2",
|
"chroma-js": "^2.1.2",
|
||||||
"clipboard": "^2.0.8",
|
"clipboard": "^2.0.8",
|
||||||
"dayjs": "^1.10.7",
|
"dayjs": "^1.10.7",
|
||||||
"form-data": "^4.0.0",
|
"form-data": "^4.0.0",
|
||||||
"naive-ui": "^2.21.3",
|
"naive-ui": "^2.21.5",
|
||||||
"pinia": "^2.0.6",
|
"pinia": "^2.0.6",
|
||||||
"print-js": "^1.6.0",
|
"print-js": "^1.6.0",
|
||||||
"qs": "^6.10.1",
|
"qs": "^6.10.2",
|
||||||
"swiper": "^7.3.1",
|
"swiper": "^7.3.1",
|
||||||
"vditor": "^3.8.7",
|
"vditor": "^3.8.8",
|
||||||
"vue": "^3.2.23",
|
"vue": "^3.2.25",
|
||||||
"vue-router": "^4.0.12",
|
"vue-router": "^4.0.12",
|
||||||
"wangeditor": "^4.7.10",
|
"wangeditor": "^4.7.10",
|
||||||
"xgplayer": "^2.31.4"
|
"xgplayer": "^2.31.4"
|
||||||
@ -54,41 +54,41 @@
|
|||||||
"@amap/amap-jsapi-types": "^0.0.8",
|
"@amap/amap-jsapi-types": "^0.0.8",
|
||||||
"@commitlint/cli": "^15.0.0",
|
"@commitlint/cli": "^15.0.0",
|
||||||
"@commitlint/config-conventional": "^15.0.0",
|
"@commitlint/config-conventional": "^15.0.0",
|
||||||
"@iconify/json": "^1.1.438",
|
"@iconify/json": "^1.1.441",
|
||||||
"@iconify/vue": "^3.1.1",
|
"@iconify/vue": "^3.1.1",
|
||||||
"@types/bmapgl": "^0.0.4",
|
"@types/bmapgl": "^0.0.4",
|
||||||
"@types/chroma-js": "^2.1.3",
|
"@types/chroma-js": "^2.1.3",
|
||||||
"@types/qs": "^6.9.7",
|
"@types/qs": "^6.9.7",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.5.0",
|
"@typescript-eslint/eslint-plugin": "^5.6.0",
|
||||||
"@typescript-eslint/parser": "^5.5.0",
|
"@typescript-eslint/parser": "^5.6.0",
|
||||||
"@vitejs/plugin-vue": "^1.10.1",
|
"@vitejs/plugin-vue": "^1.10.2",
|
||||||
"@vue/compiler-sfc": "^3.2.23",
|
"@vue/compiler-sfc": "^3.2.25",
|
||||||
"@vue/eslint-config-prettier": "^6.0.0",
|
"@vue/eslint-config-prettier": "^6.0.0",
|
||||||
"@vue/eslint-config-typescript": "^9.1.0",
|
"@vue/eslint-config-typescript": "^9.1.0",
|
||||||
"commitizen": "^4.2.4",
|
"commitizen": "^4.2.4",
|
||||||
"cz-conventional-changelog": "^3.3.0",
|
"cz-conventional-changelog": "^3.3.0",
|
||||||
"cz-customizable": "^6.3.0",
|
"cz-customizable": "^6.3.0",
|
||||||
"dotenv": "^10.0.0",
|
"dotenv": "^10.0.0",
|
||||||
"eslint": "^8.4.0",
|
"eslint": "^8.4.1",
|
||||||
"eslint-config-airbnb-base": "^15.0.0",
|
"eslint-config-airbnb-base": "^15.0.0",
|
||||||
"eslint-config-prettier": "^8.3.0",
|
"eslint-config-prettier": "^8.3.0",
|
||||||
"eslint-plugin-import": "^2.25.3",
|
"eslint-plugin-import": "^2.25.3",
|
||||||
"eslint-plugin-prettier": "^4.0.0",
|
"eslint-plugin-prettier": "^4.0.0",
|
||||||
"eslint-plugin-vue": "^8.1.1",
|
"eslint-plugin-vue": "^8.2.0",
|
||||||
"husky": "^7.0.4",
|
"husky": "^7.0.4",
|
||||||
"lint-staged": "^12.1.2",
|
"lint-staged": "^12.1.2",
|
||||||
"patch-package": "^6.4.7",
|
"patch-package": "^6.4.7",
|
||||||
"postinstall-postinstall": "^2.1.0",
|
"postinstall-postinstall": "^2.1.0",
|
||||||
"prettier": "^2.5.1",
|
"prettier": "^2.5.1",
|
||||||
"rollup-plugin-visualizer": "^5.5.2",
|
"rollup-plugin-visualizer": "^5.5.2",
|
||||||
"sass": "^1.44.0",
|
"sass": "^1.45.0",
|
||||||
"typescript": "^4.5.2",
|
"typescript": "^4.5.3",
|
||||||
"unplugin-icons": "^0.12.22",
|
"unplugin-icons": "^0.12.23",
|
||||||
"unplugin-vue-components": "^0.17.3",
|
"unplugin-vue-components": "^0.17.8",
|
||||||
"vite": "~2.5.10",
|
"vite": "~2.5.10",
|
||||||
"vite-plugin-html": "^2.1.1",
|
"vite-plugin-html": "^2.1.1",
|
||||||
"vite-plugin-windicss": "^1.5.4",
|
"vite-plugin-windicss": "^1.5.4",
|
||||||
"vue-tsc": "~0.29.6",
|
"vue-tsc": "^0.29.8",
|
||||||
"vueuc": "^0.4.18",
|
"vueuc": "^0.4.18",
|
||||||
"windicss": "^3.2.1"
|
"windicss": "^3.2.1"
|
||||||
},
|
},
|
||||||
|
3530
pnpm-lock.yaml
3530
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
@ -1,5 +0,0 @@
|
|||||||
import SvgNoPermission from './SvgNoPermission.vue';
|
|
||||||
import SvgNotFound from './SvgNotFound.vue';
|
|
||||||
import SvgServiceError from './SvgServiceError.vue';
|
|
||||||
|
|
||||||
export { SvgNoPermission, SvgNotFound, SvgServiceError };
|
|
@ -1,24 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="w-400px h-400px" :style="{ color }">
|
|
||||||
<svg-no-permission v-if="type === '403'" />
|
|
||||||
<svg-not-found v-if="type === '404'" />
|
|
||||||
<svg-service-error v-if="type === '500'" />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { SvgNoPermission, SvgNotFound, SvgServiceError } from './components';
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
/** 异常类型 */
|
|
||||||
type?: '403' | '404' | '500';
|
|
||||||
/** 主题颜色 */
|
|
||||||
color?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
withDefaults(defineProps<Props>(), {
|
|
||||||
type: '404',
|
|
||||||
color: '#409eff'
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
<style scoped></style>
|
|
93
src/components/common/LoadingEmptyWrapper/index.vue
Normal file
93
src/components/common/LoadingEmptyWrapper/index.vue
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
<template>
|
||||||
|
<div v-if="reloadFlag" class="relative">
|
||||||
|
<slot></slot>
|
||||||
|
<div v-show="showPlaceholder" class="absolute-lt w-full h-full" :class="placeholderClass">
|
||||||
|
<div v-show="loading" class="absolute-center">
|
||||||
|
<n-spin :show="true" :size="loadingSize" />
|
||||||
|
</div>
|
||||||
|
<div v-show="isEmpty" class="absolute-center">
|
||||||
|
<div class="relative text-primary" :class="emptyNetworkClass">
|
||||||
|
<svg-empty-data />
|
||||||
|
<p class="absolute left-0 bottom-[20%] w-full text-center">{{ emptyDesc }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-show="!network" class="absolute-center">
|
||||||
|
<div
|
||||||
|
class="relative text-primary"
|
||||||
|
:class="[{ 'cursor-pointer': showNetworkReload }, emptyNetworkClass]"
|
||||||
|
@click="handleReload"
|
||||||
|
>
|
||||||
|
<svg-network-error />
|
||||||
|
<p class="absolute-lb bottom-[20%] w-full text-center">{{ networkErrorDesc }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, watch, nextTick } from 'vue';
|
||||||
|
import { NSpin } from 'naive-ui';
|
||||||
|
import { NETWORK_ERROR_MSG } from '@/config';
|
||||||
|
import { useBoolean } from '@/hooks';
|
||||||
|
import { SvgEmptyData, SvgNetworkError } from '../../svg';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
/** 是否加载 */
|
||||||
|
loading: boolean;
|
||||||
|
/** 是否为空 */
|
||||||
|
empty?: boolean;
|
||||||
|
/** 加载图标的大小 */
|
||||||
|
loadingSize?: 'small' | 'medium' | 'large';
|
||||||
|
/** 中间占位符的class */
|
||||||
|
placeholderClass?: string;
|
||||||
|
/** 空数据描述文本 */
|
||||||
|
emptyDesc?: string;
|
||||||
|
/** 空数据和网络异常占位class */
|
||||||
|
emptyNetworkClass?: string;
|
||||||
|
/** 显示网络异常的重试点击按钮 */
|
||||||
|
showNetworkReload?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
loading: false,
|
||||||
|
empty: false,
|
||||||
|
loadingSize: 'medium',
|
||||||
|
placeholderClass: 'bg-white',
|
||||||
|
emptyDesc: '暂无数据',
|
||||||
|
emptyNetworkClass: 'w-320px h-320px text-16px text-[#666]',
|
||||||
|
showNetworkReload: false
|
||||||
|
});
|
||||||
|
|
||||||
|
// 网络状态
|
||||||
|
const { bool: network, setBool: setNetwork } = useBoolean(window.navigator.onLine);
|
||||||
|
const { bool: reloadFlag, setBool: setReload } = useBoolean(true);
|
||||||
|
|
||||||
|
// 数据是否为空
|
||||||
|
const isEmpty = computed(() => props.empty && !props.loading && network.value);
|
||||||
|
|
||||||
|
const showPlaceholder = computed(() => props.loading || isEmpty.value || !network.value);
|
||||||
|
|
||||||
|
const networkErrorDesc = computed(() =>
|
||||||
|
props.showNetworkReload ? `${NETWORK_ERROR_MSG}, 点击重试` : NETWORK_ERROR_MSG
|
||||||
|
);
|
||||||
|
|
||||||
|
function handleReload() {
|
||||||
|
if (!props.showNetworkReload) return;
|
||||||
|
setReload(false);
|
||||||
|
nextTick(() => {
|
||||||
|
setReload(true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.loading,
|
||||||
|
newValue => {
|
||||||
|
// 结束加载判断一下网络状态
|
||||||
|
if (!newValue) {
|
||||||
|
setNetwork(window.navigator.onLine);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
<style scoped></style>
|
@ -1,8 +1,8 @@
|
|||||||
import NaiveProvider from './NaiveProvider/index.vue';
|
import NaiveProvider from './NaiveProvider/index.vue';
|
||||||
import SystemLogo from './SystemLogo/index.vue';
|
import SystemLogo from './SystemLogo/index.vue';
|
||||||
import ExceptionSvg from './ExceptionSvg/index.vue';
|
|
||||||
import LoginBg from './LoginBg/index.vue';
|
import LoginBg from './LoginBg/index.vue';
|
||||||
import BannerSvg from './BannerSvg/index.vue';
|
import BannerSvg from './BannerSvg/index.vue';
|
||||||
import HoverContainer from './HoverContainer/index.vue';
|
import HoverContainer from './HoverContainer/index.vue';
|
||||||
|
import LoadingEmptyWrapper from './LoadingEmptyWrapper/index.vue';
|
||||||
|
|
||||||
export { NaiveProvider, SystemLogo, ExceptionSvg, LoginBg, BannerSvg, HoverContainer };
|
export { NaiveProvider, SystemLogo, LoginBg, BannerSvg, HoverContainer, LoadingEmptyWrapper };
|
||||||
|
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 WebSiteLink from './WebSiteLink/index.vue';
|
||||||
import GithubLink from './GithubLink/index.vue';
|
import GithubLink from './GithubLink/index.vue';
|
||||||
import ThemeSwitch from './ThemeSwitch/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 };
|
||||||
|
@ -1,2 +1,3 @@
|
|||||||
export * from './common';
|
export * from './common';
|
||||||
export * from './custom';
|
export * from './custom';
|
||||||
|
export * from './svg';
|
||||||
|
1447
src/components/svg/SvgEmptyData.vue
Normal file
1447
src/components/svg/SvgEmptyData.vue
Normal file
File diff suppressed because one or more lines are too long
408
src/components/svg/SvgNetworkError.vue
Normal file
408
src/components/svg/SvgNetworkError.vue
Normal file
@ -0,0 +1,408 @@
|
|||||||
|
<template>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 500">
|
||||||
|
<g id="freepik--background-simple--inject-163">
|
||||||
|
<path
|
||||||
|
d="M464.5,113.47q-2.38-4-5-7.9C436.33,71.22,396.44,47.73,357.2,37.11a231.87,231.87,0,0,0-52.71-7.69c-116.62-4-163.07,88-201.61,111.67S5,205.33,9,290.32s65.22,165,183.81,170,126.5-48.42,192.71-68.19c43.82-13.08,75-57.26,90.2-98.09C496.5,238.38,495.52,166.11,464.5,113.47Z"
|
||||||
|
style="fill: currentColor"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
d="M464.5,113.47q-2.38-4-5-7.9C436.33,71.22,396.44,47.73,357.2,37.11a231.87,231.87,0,0,0-52.71-7.69c-116.62-4-163.07,88-201.61,111.67S5,205.33,9,290.32s65.22,165,183.81,170,126.5-48.42,192.71-68.19c43.82-13.08,75-57.26,90.2-98.09C496.5,238.38,495.52,166.11,464.5,113.47Z"
|
||||||
|
style="fill: #fff; opacity: 0.7000000000000001"
|
||||||
|
></path>
|
||||||
|
</g>
|
||||||
|
<g id="freepik--Window--inject-163">
|
||||||
|
<polygon
|
||||||
|
points="438.56 121.49 438.56 118.2 377.7 118.2 377.7 69.42 375.72 69.42 375.72 118.2 315.29 118.2 315.29 121.49 375.72 121.49 375.72 166.65 315.29 166.65 315.29 169.95 375.72 169.95 375.72 215.11 315.29 215.11 315.29 218.4 375.72 218.4 375.72 265.85 377.7 265.85 377.7 218.4 438.56 218.4 438.56 215.11 377.7 215.11 377.7 169.95 438.56 169.95 438.56 166.65 377.7 166.65 377.7 121.49 438.56 121.49"
|
||||||
|
style="fill: #263238; stroke: #263238; stroke-miterlimit: 10"
|
||||||
|
></polygon>
|
||||||
|
<path
|
||||||
|
d="M311.12,65.46V271.13H442.3V65.46ZM437.87,264.18H315.55V72.4H437.87Z"
|
||||||
|
style="fill: #263238; stroke: #263238; stroke-miterlimit: 10"
|
||||||
|
></path>
|
||||||
|
</g>
|
||||||
|
<g id="freepik--Plant--inject-163">
|
||||||
|
<path
|
||||||
|
d="M254,323.92h0a.1.1,0,0,0-.06-.13.1.1,0,0,0-.13.06,132,132,0,0,0-6.94,21.75A202,202,0,0,0,243,368.15l-.54,5.7c-.08.95-.19,1.9-.26,2.85l-.15,2.86c-.08,1.91-.2,3.82-.26,5.73l.06,5.73.06,2.86c0,.95.11,1.91.18,2.86.14,1.9.26,3.81.43,5.71.37,3.8.94,7.57,1.52,11.34a.11.11,0,0,0,.1.08.09.09,0,0,0,.1-.1h0c0-1.91-.09-3.8-.13-5.71s-.22-3.78-.19-5.68,0-3.8-.06-5.69l-.06-2.84,0-2.84,0-5.68.15-5.67.06-2.84c0-.95.11-1.89.16-2.84l.35-5.67a200.9,200.9,0,0,1,3.21-22.49A131.72,131.72,0,0,1,254,323.92Z"
|
||||||
|
style="fill: #263238"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
d="M254.81,353.6a.1.1,0,0,0-.06-.12.1.1,0,0,0-.13.06,132.44,132.44,0,0,0-6.43,29.24c-.5,5-1.06,9.94-1.13,14.94a127.13,127.13,0,0,0,.53,15,.11.11,0,0,0,.09.08.09.09,0,0,0,.11-.08c.58-5,.93-9.94,1.28-14.9s.4-9.93.72-14.89a148.32,148.32,0,0,1,5-29.31Z"
|
||||||
|
style="fill: #263238"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
d="M271.27,364.05a.1.1,0,0,0-.14,0,50.06,50.06,0,0,0-11,9.59,44.64,44.64,0,0,0-4.3,6l-.88,1.67-.7,1.74a15.71,15.71,0,0,0-.6,1.75c-.18.6-.38,1.18-.54,1.78a71,71,0,0,0-2.27,14.52,84,84,0,0,0,.54,14.62.11.11,0,0,0,.1.08.09.09,0,0,0,.1-.09l1.26-14.47a127.64,127.64,0,0,1,1.83-14.27c.13-.59.3-1.15.45-1.73a16,16,0,0,1,.5-1.71l.59-1.67.76-1.58a43.25,43.25,0,0,1,3.94-6,49.72,49.72,0,0,1,10.33-10.05h0A.11.11,0,0,0,271.27,364.05Z"
|
||||||
|
style="fill: #263238"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
d="M263.3,382.64a16.06,16.06,0,0,0-5,6,35.33,35.33,0,0,0-2.81,7.37c-.19.63-.35,1.27-.49,1.91l-.45,1.92a34.52,34.52,0,0,0-.49,3.91c0,1.32-.05,2.63,0,3.94a34.57,34.57,0,0,0,.51,3.91.13.13,0,0,0,.08.08.1.1,0,0,0,.12-.08,35.91,35.91,0,0,0,.7-3.85c.2-1.28.4-2.54.55-3.81s.35-2.52.45-3.79.27-2.54.55-3.78a34.19,34.19,0,0,1,2.14-7.3,16.18,16.18,0,0,1,4.2-6.27l0,0a.09.09,0,0,0,0-.13A.11.11,0,0,0,263.3,382.64Z"
|
||||||
|
style="fill: #263238"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
d="M240.22,397.14c-.54-6.42-1.6-12.79-2.6-19.14s-2.12-12.7-3.28-19-2.46-12.63-3.83-18.91a.1.1,0,1,0-.19,0c1.1,6.34,2.09,12.69,3,19.05s1.87,12.71,2.68,19.08,1.4,12.77,2.19,19.13l2.35,19.13a.12.12,0,0,0,.1.09.11.11,0,0,0,.11-.1c.07-1.61.07-3.22.11-4.84s-.07-3.22-.09-4.83C240.74,403.57,240.46,400.36,240.22,397.14Z"
|
||||||
|
style="fill: #263238"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
d="M236.37,397.05c-.35-2.55-.81-5.08-1.3-7.6l-.9-3.75c-.3-1.25-.58-2.5-.94-3.74a105.84,105.84,0,0,0-4.92-14.61l-.77-1.77-.85-1.73c-.55-1.16-1.2-2.27-1.83-3.4a28.58,28.58,0,0,0-4.64-6.14.09.09,0,0,0-.13,0,.1.1,0,0,0,0,.14h0a28,28,0,0,1,4.24,6.29c.56,1.14,1.15,2.26,1.63,3.44l.76,1.75.67,1.79a105.45,105.45,0,0,1,4.28,14.61c.58,2.47,1,5,1.45,7.49l.6,3.76c.22,1.25.49,2.5.66,3.76.4,2.51.87,5,1.3,7.53s.85,5,1.39,7.55a.11.11,0,0,0,.09.09.11.11,0,0,0,.11-.1q0-3.85-.18-7.7C236.94,402.15,236.63,399.6,236.37,397.05Z"
|
||||||
|
style="fill: #263238"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
d="M231.05,397.38c-1.19-3.38-2.57-6.68-4.07-9.91a86.8,86.8,0,0,0-5.12-9.38.1.1,0,1,0-.17.1,87.62,87.62,0,0,1,4.37,9.68c1.3,3.28,2.48,6.62,3.46,10s1.73,6.83,2.63,10.23,1.71,6.83,2.76,10.27a.1.1,0,0,0,.09.07.11.11,0,0,0,.11-.1,51.83,51.83,0,0,0-1-10.69A66.72,66.72,0,0,0,231.05,397.38Z"
|
||||||
|
style="fill: #263238"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
d="M230.83,408.61a23.15,23.15,0,0,0-2.83-5.94,24.44,24.44,0,0,0-4.12-5.11,19.36,19.36,0,0,0-2.62-2,10.92,10.92,0,0,0-3-1.34.1.1,0,0,0-.11.06.1.1,0,0,0,0,.13h0a14.89,14.89,0,0,1,4.93,3.83,23.66,23.66,0,0,1,3.43,5.2c1,1.84,1.48,3.88,2.33,5.79.37,1,.74,1.95,1.18,2.93a23.09,23.09,0,0,0,1.29,3h0a.11.11,0,0,0,.09.05.09.09,0,0,0,.1-.09,22.57,22.57,0,0,0-.12-3.29C231.31,410.74,231.09,409.67,230.83,408.61Z"
|
||||||
|
style="fill: #263238"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
d="M231.23,349.25a18.29,18.29,0,0,1-13-13.76c-2.43-11.34,5.4-6.48-1.62-24.3s-5.4-21.87-3-27,3.78-3.51,5.94,3.24,10,17.28,13,37S231.23,349.25,231.23,349.25Z"
|
||||||
|
style="fill: currentColor"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
d="M252,330.09s-7-2.43-7.29-15.66,5.13-10.53,5.94-27,1.89-28.08,4.86-28.35,10.8,8.1,13.5,26.46-3.51,30.51-8.91,35.91S252,330.09,252,330.09Z"
|
||||||
|
style="fill: currentColor"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
d="M227.45,365.18s-1.62-9.72-4.86-14.31-10.26-8.64-14.85-17.54-9.72-13.23-9.45-.54,5.94,19.16,13,23.48S227.45,365.18,227.45,365.18Z"
|
||||||
|
style="fill: currentColor"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
d="M224.48,381.92s-6.48-12.42-15.93-21.6-17-13.77-14.31-9.18,5.94,4.32,10.53,12.42,3.24,14,9.72,18.63S224.48,381.92,224.48,381.92Z"
|
||||||
|
style="fill: currentColor"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
d="M223.67,397.85c-.27-.81-.54-6.75-9.72-11.61s-17.82-3-16.74-.54,6.75,6.48,12.42,8.91S223.67,397.85,223.67,397.85Z"
|
||||||
|
style="fill: currentColor"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
d="M251.47,365.72s-4.05-5.13-1.89-15.93,7-8.1,12.42-13.77,4.32-6.47,9.72-12.41,10.26-9.18,12.42-6.75-1.08,10-4.32,14.31S271.45,337.64,269,343s-1.35,14-7.56,18.09S251.47,365.72,251.47,365.72Z"
|
||||||
|
style="fill: currentColor"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
d="M265,368.69s9.72-11.07,16.74-19.17,9.72-14.3,10-8.91,2.43,9.72-3.24,16.74S265,368.69,265,368.69Z"
|
||||||
|
style="fill: currentColor"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
d="M259.84,386.51s-.27-4.32,15.12-14.31,21.87-11.88,20.25-7.83-8.37,8.1-15.66,15.39S259.84,386.51,259.84,386.51Z"
|
||||||
|
style="fill: currentColor"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
d="M231.87,348.1s-3.61-19.36-7.39-33.13-6.21-22.14-6.21-22.14"
|
||||||
|
style="fill: none; stroke: #263238; stroke-linecap: round; stroke-linejoin: round"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
d="M251.39,330.29s6.56-21,8.72-33.68-.27-22.14-1.35-27.81"
|
||||||
|
style="fill: none; stroke: #263238; stroke-linecap: round; stroke-linejoin: round"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
d="M226.6,364.78a101.76,101.76,0,0,0-9.95-14.18c-5.94-7-10.53-10.8-13.23-17.54"
|
||||||
|
style="fill: none; stroke: #263238; stroke-linecap: round; stroke-linejoin: round"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
d="M251.47,365.72s2.16-13,7.29-19.71,15.39-17.27,19.44-24"
|
||||||
|
style="fill: none; stroke: #263238; stroke-linecap: round; stroke-linejoin: round"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
d="M223,397.55s-9.08-5.91-15.56-9.15"
|
||||||
|
style="fill: none; stroke: #263238; stroke-linecap: round; stroke-linejoin: round"
|
||||||
|
></path>
|
||||||
|
<polygon
|
||||||
|
points="252.89 454.81 234.13 454.81 224.75 407.84 262.27 407.84 252.89 454.81"
|
||||||
|
style="fill: #263238; stroke: #263238; stroke-linecap: round; stroke-linejoin: round"
|
||||||
|
></polygon>
|
||||||
|
</g>
|
||||||
|
<g id="freepik--Floor--inject-163">
|
||||||
|
<line
|
||||||
|
x1="64.8"
|
||||||
|
y1="454.81"
|
||||||
|
x2="489.98"
|
||||||
|
y2="454.81"
|
||||||
|
style="fill: none; stroke: #263238; stroke-miterlimit: 10"
|
||||||
|
></line>
|
||||||
|
<line
|
||||||
|
x1="31.18"
|
||||||
|
y1="454.81"
|
||||||
|
x2="57.55"
|
||||||
|
y2="454.81"
|
||||||
|
style="fill: none; stroke: #263238; stroke-miterlimit: 10"
|
||||||
|
></line>
|
||||||
|
</g>
|
||||||
|
<g id="freepik--Device--inject-163">
|
||||||
|
<path
|
||||||
|
d="M216.14,167v-31.5h-2.85V95.19a24.85,24.85,0,0,0-24.85-24.86H54.92A24.86,24.86,0,0,0,30.06,95.19V412.62a24.86,24.86,0,0,0,24.86,24.86H188.44a24.85,24.85,0,0,0,24.85-24.86V193.85h2.85V175.24h-2.85V167Z"
|
||||||
|
style="fill: #707070; stroke: #263238; stroke-linejoin: round"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
d="M184.08,434.05H59.27a26.33,26.33,0,0,1-26.33-26.33V100.09c0-14.55,7.46-26.34,26.33-26.34H184.08c17.31-.23,26.34,11.79,26.34,26.34V407.72A26.33,26.33,0,0,1,184.08,434.05Z"
|
||||||
|
style="fill: #263238; stroke: #263238; stroke-linejoin: round"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
d="M131.34,77.8l-.05.19c-1.46,5.44-4.82,9-8.54,9h-2.14c-3.72,0-7.08-3.52-8.54-9l0-.19-52.08-.18A21.92,21.92,0,0,0,38,99.53l-.07,304.94c0,14.21,11.12,25.73,24.85,25.73H180.62c13.73,0,24.86-11.52,24.86-25.73l0-307.13a19,19,0,0,0-18.78-19Z"
|
||||||
|
style="fill: #fff; stroke: #263238; stroke-miterlimit: 10"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
d="M205.46,102.88V97.34a19,19,0,0,0-18.78-19l-55.34-.57-.05.19c-1.46,5.44-4.82,9-8.54,9h-2.14c-3.72,0-7.08-3.52-8.54-9l0-.19-52.08-.18A21.92,21.92,0,0,0,38,99.53v3.35Z"
|
||||||
|
style="fill: #dbdbdb; stroke: #263238; stroke-miterlimit: 10"
|
||||||
|
></path>
|
||||||
|
<path d="M123.5,77.2a2.34,2.34,0,1,0-2.33,2.34A2.33,2.33,0,0,0,123.5,77.2Z" style="fill: #707070"></path>
|
||||||
|
<path
|
||||||
|
d="M54.4,83.14c0,2.37-2.65,3.22-2.66,5v.12h2.57v.79H50.88V88.4c0-2.53,2.65-3,2.65-5.22,0-.81-.27-1.23-.92-1.23s-.92.46-.92,1.15v.69h-.81v-.63c0-1.2.54-2,1.75-2S54.4,82,54.4,83.14Z"
|
||||||
|
style="fill: #263238"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
d="M58.69,83.13v.2a1.57,1.57,0,0,1-1,1.62,1.55,1.55,0,0,1,1,1.62v.6c0,1.2-.56,2-1.77,2s-1.76-.78-1.76-2v-.53H56v.59c0,.7.29,1.13.92,1.13s.91-.42.91-1.21v-.6c0-.78-.32-1.15-1-1.17h-.47V84.6h.51a.94.94,0,0,0,.94-1.08v-.35c0-.81-.28-1.22-.91-1.22s-.92.43-.92,1.14v.4h-.82v-.36c0-1.19.56-2,1.76-2S58.69,81.94,58.69,83.13Z"
|
||||||
|
style="fill: #263238"
|
||||||
|
></path>
|
||||||
|
<path d="M60.38,83.73v1.08h-.84V83.73Zm0,4.26v1.08h-.84V88Z" style="fill: #263238"></path>
|
||||||
|
<path
|
||||||
|
d="M62.14,84.43a1.28,1.28,0,0,1,1.17-.65c1,0,1.45.74,1.45,1.86v1.53c0,1.2-.57,2-1.76,2s-1.76-.78-1.76-2v-.52h.81v.58c0,.7.3,1.13.92,1.13s.92-.43.92-1.13V85.71c0-.71-.29-1.13-.92-1.13a.84.84,0,0,0-.88.84v.17h-.82l.21-4.35h3.09V82h-2.3Z"
|
||||||
|
style="fill: #263238"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
d="M69.12,83.12v.15H68.3v-.2c0-.71-.29-1.12-.93-1.12s-1,.42-1,1.26V85a1.26,1.26,0,0,1,1.26-.84c1,0,1.46.73,1.46,1.86v1.18c0,1.2-.59,2-1.79,2s-1.8-.78-1.8-2v-4c0-1.24.56-2,1.8-2S69.12,81.92,69.12,83.12Zm-2.7,2.93v1.18c0,.7.29,1.13.93,1.13s.93-.43.93-1.13V86.05c0-.7-.3-1.13-.93-1.13S66.42,85.35,66.42,86.05Z"
|
||||||
|
style="fill: #263238"
|
||||||
|
></path>
|
||||||
|
<rect x="152.41" y="83.47" width="2.21" height="5.53" style="fill: #263238"></rect>
|
||||||
|
<rect x="155.36" y="82.36" width="2.21" height="6.64" style="fill: #263238"></rect>
|
||||||
|
<rect x="158.31" y="81.57" width="2.21" height="7.42" style="fill: #263238"></rect>
|
||||||
|
<rect x="161.25" y="80.2" width="2.21" height="8.79" style="fill: #263238"></rect>
|
||||||
|
<path d="M190.83,88.84H177.56V80.73h13.27Zm-12.53-.73h11.79V81.47H178.3Z" style="fill: #263238"></path>
|
||||||
|
<rect x="179.41" y="82.27" width="7" height="5.03" style="fill: #263238"></rect>
|
||||||
|
<rect x="190.37" y="83.83" width="1.67" height="1.91" style="fill: #263238"></rect>
|
||||||
|
<path
|
||||||
|
d="M172.7,87.08a.48.48,0,0,1-.26-.07c-2.7-1.61-4.48-.12-4.56-.05a.49.49,0,0,1-.69,0,.5.5,0,0,1,0-.7c.1-.08,2.38-2,5.72,0a.5.5,0,0,1-.25.92Z"
|
||||||
|
style="fill: #263238"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
d="M173.7,85.41a.48.48,0,0,1-.26-.07c-3.9-2.32-6.53-.07-6.55,0a.5.5,0,0,1-.66-.74s3.2-2.74,7.72-.06a.49.49,0,0,1,.17.68A.48.48,0,0,1,173.7,85.41Z"
|
||||||
|
style="fill: #263238"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
d="M174.6,83.48a.47.47,0,0,1-.25-.07c-4.94-2.94-8.23-.17-8.37,0a.49.49,0,0,1-.7,0,.48.48,0,0,1,.05-.69s3.94-3.38,9.53-.06a.5.5,0,0,1,.17.68A.49.49,0,0,1,174.6,83.48Z"
|
||||||
|
style="fill: #263238"
|
||||||
|
></path>
|
||||||
|
<path d="M171.21,87.91A1.25,1.25,0,1,1,170,86.67,1.24,1.24,0,0,1,171.21,87.91Z" style="fill: #263238"></path>
|
||||||
|
<path
|
||||||
|
d="M121.53,217.66c22,.34,40.33,8.68,54.78,25.36a5.08,5.08,0,0,1-.47,7.33,5,5,0,0,1-7.15-.66,62.94,62.94,0,0,0-18.47-14.9,61.21,61.21,0,0,0-68.4,7.45,92.38,92.38,0,0,0-7.42,7.39,5.17,5.17,0,0,1-7.32.66,5.1,5.1,0,0,1-.36-7.33,71.44,71.44,0,0,1,45.7-24.87c1.44-.19,2.89-.35,4.34-.41S119.94,217.66,121.53,217.66Z"
|
||||||
|
style="fill: #263238"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
d="M123.35,238.05c13.9.36,27,6.29,37.24,18.17a5,5,0,0,1-.46,7.3,5,5,0,0,1-7.22-.7,39.62,39.62,0,0,0-19.8-12.9C117,245.56,103,249.54,91.3,261.49c-.44.45-.84.93-1.25,1.39a5.08,5.08,0,1,1-7.68-6.65,51.11,51.11,0,0,1,16.38-12.85C105.94,239.81,113.57,238.08,123.35,238.05Z"
|
||||||
|
style="fill: #263238"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
d="M146.21,272.81a5.08,5.08,0,0,1-3.25,4.76,5,5,0,0,1-5.66-1.42,21.34,21.34,0,0,0-7.76-5.77C121.2,266.84,112,269,105.79,276a4.92,4.92,0,0,1-5.43,1.63A5.07,5.07,0,0,1,98,269.52a30.52,30.52,0,0,1,47,.08,14.27,14.27,0,0,1,1.5,3.09Z"
|
||||||
|
style="fill: #263238"
|
||||||
|
></path>
|
||||||
|
<path d="M121.52,299.5a10.2,10.2,0,1,1,10.17-10.15A10.14,10.14,0,0,1,121.52,299.5Z" style="fill: #263238"></path>
|
||||||
|
<path
|
||||||
|
d="M121.58,182a72.72,72.72,0,1,0,72.71,72.71A72.8,72.8,0,0,0,121.58,182ZM58.86,254.75A62.68,62.68,0,0,1,164,208.56L75.39,297.13A62.52,62.52,0,0,1,58.86,254.75Zm62.72,62.72a62.4,62.4,0,0,1-38.85-13.54l88-88a62.67,62.67,0,0,1-49.18,101.57Z"
|
||||||
|
style="fill: currentColor"
|
||||||
|
></path>
|
||||||
|
</g>
|
||||||
|
<g id="freepik--Sofa--inject-163">
|
||||||
|
<polygon
|
||||||
|
points="427.5 454.81 418.02 454.81 415.99 432.25 429.53 432.25 427.5 454.81"
|
||||||
|
style="fill: #263238; stroke: #263238; stroke-miterlimit: 10"
|
||||||
|
></polygon>
|
||||||
|
<polygon
|
||||||
|
points="466.98 454.81 457.5 454.81 455.47 432.25 469.01 432.25 466.98 454.81"
|
||||||
|
style="fill: #263238; stroke: #263238; stroke-miterlimit: 10"
|
||||||
|
></polygon>
|
||||||
|
<polygon
|
||||||
|
points="372.23 454.81 362.75 454.81 360.73 432.25 374.26 432.25 372.23 454.81"
|
||||||
|
style="fill: #263238; stroke: #263238; stroke-miterlimit: 10"
|
||||||
|
></polygon>
|
||||||
|
<polygon
|
||||||
|
points="329.93 454.81 320.46 454.81 318.43 432.25 331.96 432.25 329.93 454.81"
|
||||||
|
style="fill: #263238; stroke: #263238; stroke-miterlimit: 10"
|
||||||
|
></polygon>
|
||||||
|
<path
|
||||||
|
d="M375.17,286.9h98.52s.39,34.25-4.68,58.5-11.28,38.91-11.28,38.91H364.11S378,330.67,375.17,286.9Z"
|
||||||
|
style="fill: #fff; stroke: #263238; stroke-miterlimit: 10"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
d="M416.06,292.13c-8.64-.68-12.07-2.42-18.53-6-8.41-4.61-10.21-1.54-9.61,3.07s-8.17,35-10,50.3-1.21,23.53-1.21,23.53-5.4,4.61-1.8,6.14a32.28,32.28,0,0,0,7.21,2.05h69.66s12.38-63.61,13.58-73.84-4.2-11.25-7.21-9.72-3.6,2.56-7.81,3.07C440.5,292,427.9,293.05,416.06,292.13Z"
|
||||||
|
style="fill: currentColor"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
d="M393.63,293.16a16.26,16.26,0,0,0,3.69-7.1c-.84-.45-1.61-.82-2.32-1.12a13.69,13.69,0,0,1-3.26,6.61,14.3,14.3,0,0,1-4.54,3.65c-.18.91-.38,1.9-.6,3A16.06,16.06,0,0,0,393.63,293.16Z"
|
||||||
|
style="fill: #fff"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
d="M456,313.06c-4.41,5.18-4.24,9.78-4.08,14.24.16,4.16.3,8.08-3.48,12.53s-7.69,4.92-11.82,5.44c-4.42.54-9,1.11-13.4,6.29s-4.25,9.78-4.08,14.23a28.53,28.53,0,0,1-.15,5.47h2.52a33.48,33.48,0,0,0,.11-5.56c-.15-4.16-.3-8.09,3.49-12.53s7.68-4.93,11.82-5.44c4.42-.54,9-1.11,13.4-6.29s4.24-9.78,4.08-14.24c-.16-4.16-.3-8.09,3.49-12.53a14,14,0,0,1,5.34-4c.17-1,.34-1.92.5-2.83A16.07,16.07,0,0,0,456,313.06Z"
|
||||||
|
style="fill: #fff"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
d="M385.23,370.14c4.43-.55,9-1.12,13.41-6.3s4.24-9.78,4.08-14.23c-.16-4.16-.3-8.09,3.48-12.53s7.69-4.93,11.82-5.44c4.42-.55,9-1.11,13.4-6.29s4.25-9.79,4.08-14.24c-.15-4.16-.3-8.09,3.49-12.53s7.69-4.93,11.82-5.44,8.26-1,12.34-5.14a4.35,4.35,0,0,0-2.74-.86,14.82,14.82,0,0,1-7.92,3.27,19.67,19.67,0,0,1-2.12.36l-3,.35A16.3,16.3,0,0,0,437.1,297c-4.42,5.19-4.25,9.79-4.08,14.24.15,4.16.3,8.09-3.49,12.54s-7.68,4.92-11.82,5.43c-4.42.55-9,1.11-13.4,6.29s-4.24,9.79-4.08,14.24c.16,4.16.3,8.09-3.48,12.53s-7.69,4.93-11.82,5.44a23.21,23.21,0,0,0-8.38,2.16,32.7,32.7,0,0,0,4,1.12A32.06,32.06,0,0,1,385.23,370.14Z"
|
||||||
|
style="fill: #fff"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
d="M379.74,347.75c4.41-5.18,4.24-9.78,4.08-14.23-.15-4.17-.3-8.09,3.49-12.54s7.68-4.92,11.81-5.43c4.42-.55,9-1.12,13.41-6.3s4.24-9.78,4.08-14.23c0-1-.07-1.91-.06-2.86l-.49,0-2-.19c0,1.07,0,2.12.05,3.17.16,4.16.3,8.09-3.48,12.53s-7.69,4.93-11.82,5.44c-4.42.55-9,1.11-13.41,6.29s-4.24,9.78-4.07,14.24c.15,4.16.29,8.09-3.49,12.53-.21.25-.42.48-.64.7-.1,1.27-.19,2.47-.26,3.59A18.26,18.26,0,0,0,379.74,347.75Z"
|
||||||
|
style="fill: #fff"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
d="M416.06,292.13c-8.64-.68-12.07-2.42-18.53-6-8.41-4.61-10.21-1.54-9.61,3.07s-8.17,35-10,50.3-1.21,23.53-1.21,23.53-5.4,4.61-1.8,6.14a32.28,32.28,0,0,0,7.21,2.05h69.66s12.38-63.61,13.58-73.84-4.2-11.25-7.21-9.72-3.6,2.56-7.81,3.07C440.5,292,427.9,293.05,416.06,292.13Z"
|
||||||
|
style="fill: none; stroke: #263238; stroke-miterlimit: 10"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
d="M375.17,286.9H369.6a4.49,4.49,0,0,0-4.37,3.45l-6.92,29c-1.28,5.38-5.66,20.88-10.48,23.59h0a19.79,19.79,0,0,1-9.69,2.53H317.26l8.88,90.77h38l10-144.53a5.11,5.11,0,0,0,0-1h1.13Z"
|
||||||
|
style="fill: #fff; stroke: #263238; stroke-miterlimit: 10"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
d="M322.83,345.46h20.89a19.82,19.82,0,0,0,9.69-2.53h0c4.82-2.71,9.19-18.21,10.48-23.59l6.91-29a4.5,4.5,0,0,1,4.37-3.45h0a4.49,4.49,0,0,1,4.48,4.8L369.4,436.23h-38Z"
|
||||||
|
style="fill: #fff; stroke: #263238; stroke-miterlimit: 10"
|
||||||
|
></path>
|
||||||
|
<polygon
|
||||||
|
points="412.04 436.28 414.21 371.26 327.1 370.49 329.28 436.28 412.04 436.28"
|
||||||
|
style="fill: #fff; stroke: #263238; stroke-miterlimit: 10"
|
||||||
|
></polygon>
|
||||||
|
<path
|
||||||
|
d="M479.26,286.9h-5.57a4.5,4.5,0,0,0-4.37,3.45l-6.92,29c-1.28,5.38-5.65,20.88-10.48,23.59h0a19.76,19.76,0,0,1-9.69,2.53H421.35L425,436.23h42.82L478.17,291.7a4.39,4.39,0,0,0,0-1h1.13Z"
|
||||||
|
style="fill: #fff; stroke: #263238; stroke-miterlimit: 10"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
d="M426.92,345.46h20.89a19.82,19.82,0,0,0,9.69-2.53h0c4.82-2.71,9.2-18.21,10.48-23.59l6.92-29a4.49,4.49,0,0,1,4.36-3.45h0a4.48,4.48,0,0,1,4.48,4.8L473.58,434a2.39,2.39,0,0,1-2.37,2.21H430.33Z"
|
||||||
|
style="fill: #fff; stroke: #263238; stroke-miterlimit: 10"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
d="M414.21,352,412,436.28h18.29L433.47,352a6.54,6.54,0,0,0-6.55-6.54h-6.16A6.54,6.54,0,0,0,414.21,352Z"
|
||||||
|
style="fill: #fff; stroke: #263238; stroke-miterlimit: 10"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
d="M307.94,352l3.14,84.28h18.29L327.19,352a6.54,6.54,0,0,0-6.54-6.54h-6.16A6.54,6.54,0,0,0,307.94,352Z"
|
||||||
|
style="fill: #fff; stroke: #263238; stroke-miterlimit: 10"
|
||||||
|
></path>
|
||||||
|
<line
|
||||||
|
x1="327.54"
|
||||||
|
y1="399.23"
|
||||||
|
x2="413.38"
|
||||||
|
y2="399.23"
|
||||||
|
style="fill: none; stroke: #263238; stroke-miterlimit: 10"
|
||||||
|
></line>
|
||||||
|
</g>
|
||||||
|
<g id="freepik--Character--inject-163">
|
||||||
|
<path
|
||||||
|
d="M285.55,287.23s-7.94,44.12-9.68,51.58-6,45.49-6,54.94.74,38,.74,38-3.23,1.49-3.23,2.73,2.49,9.2,2.49,9.2l14.66,5.47s3-7,3.23-9.45-1.74-3.48-1.74-7.71,7.46-53.44,9.7-61.64,8.7-32.81,9.94-35.05,7.46-18.64,7.46-18.64l3.48,1.74s2.23,51.7,3.48,54.93S338,429.2,338,429.2s-2.73,1.24-1,4.23,4.72,9.69,4.72,9.69,2.74-3.73,6-4.47,8.7,2.48,8.7,2.48,3.23-7.46,2.48-10.93-4-9.45-4.22-11.19-7-51.86-7-51.86,1.74-70.1,1.74-74.82a31.49,31.49,0,0,0-1.74-9.45,117.57,117.57,0,0,1-32.32,6.22c-17.65.74-27.34-6.46-27.34-6.46Z"
|
||||||
|
style="fill: #263238; stroke: #263238; stroke-miterlimit: 10"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
d="M269.91,443.71s-8.21,7.45-9.7,8.45-11.93,5-11.68,7,3,3.23,3,3.23h33.81s.24-1,.74-5.72a18.86,18.86,0,0,0-1.49-8.95Z"
|
||||||
|
style="fill: #fff; stroke: #263238; stroke-miterlimit: 10"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
d="M285.83,458.76l-37.12,1a6.16,6.16,0,0,0,2.8,2.57h33.81S285.49,461.66,285.83,458.76Z"
|
||||||
|
style="fill: #fff; stroke: #263238; stroke-miterlimit: 10"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
d="M341.74,443.12s-.25,6.21-.25,9.45-.49,7-.25,7.7.75,2,.75,2h19.64s.74-2-.25-5.72-2.74-5.71-3.23-9a36.31,36.31,0,0,0-1.74-6.46S346.71,433.43,341.74,443.12Z"
|
||||||
|
style="fill: #fff; stroke: #263238; stroke-miterlimit: 10"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
d="M361.89,459.91c-7.47,0-16.77-.24-20.72-.34a3.34,3.34,0,0,0,.07.7c.25.75.75,2,.75,2h19.64A7.22,7.22,0,0,0,361.89,459.91Z"
|
||||||
|
style="fill: #fff; stroke: #263238; stroke-miterlimit: 10"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
d="M278.5,193.64s-21.12,19.83-20.2,21.68S265,232.18,265,232.18s1.66,10,.74,16.31a64.54,64.54,0,0,0-.56,12.05,25.59,25.59,0,0,0,7,6.11c4.08,2.23,7.23,6.12,9.83-.18s1.48-17.61,1.48-17.61S287,262.58,287.59,265s-2,22.24-2,22.24S298.33,298,320,295.56s28.54-10,28.54-10-2.23-24.28-2.23-27.06a54,54,0,0,0-.37-5.75s7.6,7.79,11.31,7.42,10.19-15.57,8.52-18-7.41-12.25-10.93-19.29a85.73,85.73,0,0,1-5.28-13.19s-6.12-20.76-8-22.24-22.7-1.25-26.41-2.17a124.1,124.1,0,0,0-14.83-2C299.08,183.26,282.58,187.15,278.5,193.64Z"
|
||||||
|
style="fill: currentColor; stroke: #263238; stroke-miterlimit: 10"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
d="M340.8,221.39c-.1-5.32-.12-9.46-.12-9.46"
|
||||||
|
style="fill: none; stroke: #263238; stroke-miterlimit: 10"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
d="M346,252.75a78.12,78.12,0,0,1-3.7-8.89c-.74-2.21-1.13-10.1-1.34-17.49"
|
||||||
|
style="fill: none; stroke: #263238; stroke-miterlimit: 10"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
d="M272.93,230.8c-1.89-3.3-3.33-5.88-3.51-6.4"
|
||||||
|
style="fill: none; stroke: #263238; stroke-miterlimit: 10"
|
||||||
|
></path>
|
||||||
|
<path d="M283.51,248.86s-4.74-8-8.72-14.84" style="fill: none; stroke: #263238; stroke-miterlimit: 10"></path>
|
||||||
|
<path
|
||||||
|
d="M307.6,185.85a92.16,92.16,0,0,1,.19,19.65"
|
||||||
|
style="fill: none; stroke: #263238; stroke-miterlimit: 10"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
d="M320.07,277.32c2.23.45,4.62.86,7.17,1.19"
|
||||||
|
style="fill: none; stroke: #263238; stroke-miterlimit: 10"
|
||||||
|
></path>
|
||||||
|
<path d="M293.14,265.36s6,6.58,22,10.8" style="fill: none; stroke: #263238; stroke-miterlimit: 10"></path>
|
||||||
|
<path
|
||||||
|
d="M309.45,279.44a50.88,50.88,0,0,0,21.87,4.45"
|
||||||
|
style="fill: none; stroke: #263238; stroke-miterlimit: 10"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
d="M265.16,163.61s2.22,10.38,2.41,12.61.74,5.19,3.52,9.63,10,14.27,11.49,14.83,7.23-.93,7.23-.93,6.11,12.05,7.78,11.68,5.93-14.09,6.49-19.65a39.64,39.64,0,0,0,.18-8.34l-2.59-9.26s-2.22-13-2.22-13.9,1.29-7.6-5.93-10.38-16.13-3-22.06,3.89S265.16,163.61,265.16,163.61Z"
|
||||||
|
style="fill: #fff; stroke: #263238; stroke-miterlimit: 10"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
d="M285,151.94a33.32,33.32,0,0,1-10,9.82c-6.85,4.45-3.52-.37-3.52-.37s-4.82,3-8.71,5-2.41,2.6-3.89,2-7-16.38,5.39-24,21.43-5.61,21.43-5.61-5.38-2.41-.56-2.41S292,139,292,139s2.59-.74,3.7.74a4.4,4.4,0,0,0,1.77,1.17c6,2.54,8.09,8.22,9.22,14.15.74,3.89-2.41,13.16-2.41,13.16L299.08,171a12.88,12.88,0,0,1-3-4.08,15.94,15.94,0,0,1-.74-4.63s-3.15,4.07-3.71,1.11-2-10.56-2-10.56.56,3-1.29,3S285,151.94,285,151.94Z"
|
||||||
|
style="fill: #263238; stroke: #263238; stroke-miterlimit: 10"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
d="M298.7,172.32s-.18-6.3,3.53-8.34,6.3.19,5.18,6.12-7.78,8.15-7.78,8.15"
|
||||||
|
style="fill: #fff; stroke: #263238; stroke-miterlimit: 10"
|
||||||
|
></path>
|
||||||
|
<polygon
|
||||||
|
points="289.81 199.75 299.63 189.93 293.14 204.94 289.81 199.75"
|
||||||
|
style="fill: #263238; stroke: #263238; stroke-miterlimit: 10"
|
||||||
|
></polygon>
|
||||||
|
<path
|
||||||
|
d="M285.27,189.82a1.66,1.66,0,0,1-.51,2.2c-.67.27-1.52-.28-1.9-1.23s-.15-1.94.51-2.21S284.88,188.87,285.27,189.82Z"
|
||||||
|
style="fill: #263238"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
d="M287.83,172.66c.36,1.18.1,2.3-.57,2.51s-1.53-.58-1.89-1.75-.1-2.29.57-2.5S287.47,171.49,287.83,172.66Z"
|
||||||
|
style="fill: #263238"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
d="M275.66,174.72c.36,1.18.1,2.3-.58,2.51s-1.52-.57-1.88-1.75-.11-2.29.57-2.5S275.29,173.55,275.66,174.72Z"
|
||||||
|
style="fill: #263238"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
d="M276.1,170.1a7.06,7.06,0,0,1,2.59,5.56c0,3.71-.56,9.27-.56,9.27l6.68-.93"
|
||||||
|
style="fill: none; stroke: #263238; stroke-miterlimit: 10"
|
||||||
|
></path>
|
||||||
|
<path d="M280.54,167.69s3.34-5.93,8.53-4.82" style="fill: none; stroke: #263238; stroke-miterlimit: 10"></path>
|
||||||
|
<path d="M274.61,168.8s-4.07-1.29-6.85.37" style="fill: none; stroke: #263238; stroke-miterlimit: 10"></path>
|
||||||
|
<path
|
||||||
|
d="M388,216.43s4.08-1.48,5.56-2,3,.37,5.56-.92,5.56-3.9,5.75-2-.75,2.59-2.23,3.52-3.89,2.78-3.89,2.78l-.74,1.85-11.12-.92S387.1,217,388,216.43Z"
|
||||||
|
style="fill: #fff; stroke: #263238; stroke-miterlimit: 10"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
d="M410.35,221.22s5.11-4.05,5.85-5.9-.56-2-1.85-1.3S409,218.1,409,218.1Z"
|
||||||
|
style="fill: #fff; stroke: #263238; stroke-miterlimit: 10"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
d="M406.17,222.1s3.8.4,4.18-.88,3.38-9.34,2.89-10.35-1.44-.92-2.49.75a57.18,57.18,0,0,0-2.9,7.07s-2.54.57-3.18,2.31S406.17,222.1,406.17,222.1Z"
|
||||||
|
style="fill: #fff; stroke: #263238; stroke-miterlimit: 10"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
d="M401.37,223.1s3.53,1.49,4.27.37,5.93-8,5.74-9.08-1.11-1.29-2.59,0a55.84,55.84,0,0,0-4.82,5.93s-2.6-.18-3.71,1.3S401.37,223.1,401.37,223.1Z"
|
||||||
|
style="fill: #fff; stroke: #263238; stroke-miterlimit: 10"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
d="M349.67,253.5a23.39,23.39,0,0,1,10.38-14.27C368.94,233.48,381,223.1,381,223.1s4.08-5.74,7-6.67,12.6,1.85,12.6,1.85,7.79-7.78,8-6.11-3.7,7.6-5.56,9.82-7.6,2.78-8.89,2.78a67,67,0,0,0-6.86.56c-.74.18-13,16.86-18.53,25s-8,9.63-11.49,9.82-7.23-2.6-8.16-3.89S349.67,253.5,349.67,253.5Z"
|
||||||
|
style="fill: #fff; stroke: #263238; stroke-miterlimit: 10"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
d="M233.77,222.82l11.68,25.34a1.86,1.86,0,0,0,2,1l9.69-1.79a1.85,1.85,0,0,0,1.36-2.52L248,220.12a1.82,1.82,0,0,0-1.89-1.11l-10.89,1.21A1.84,1.84,0,0,0,233.77,222.82Z"
|
||||||
|
style="fill: #263238; stroke: #263238; stroke-miterlimit: 10"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
d="M241.44,244.51s-4.19-6.42-4.19-7.86,2.44-4.85,3.87-7.93,1.65-5.14,2.88-4.72,1,1.64.61,3.69a79.85,79.85,0,0,1-2.66,7.8l.41,2.47a26.4,26.4,0,0,1,3.69-5.55c1.85-1.84,3.9-4.92,5.34-4.31s2.05,1,.82,2.47a42.71,42.71,0,0,0-4.14,5.91,36.87,36.87,0,0,0-2.79,5.58,42.73,42.73,0,0,1,4.37-3.66c2-1.44,4.82-3.52,6-2.7s1.44,1.64.21,2.67-2.35,1.73-4,3.17a60.68,60.68,0,0,0-4.84,5.45,17.87,17.87,0,0,1,4.84-2.66c3.49-1.43,6.68-1.27,6.81-.17s-3.67,2.1-3.67,2.1c-2.72.62-2.85,1.55-2.85,1.55s2.26,4.52,1.85,6c0,0,9.12-1.43,12.61-2.45s8.94,1,10.59,2.65c3.49,3.49,5.13,8.83,3.29,13.35s-1.85,3.08-16.43-1-14.3-5.54-17.86-11.5C244.91,252.54,241.44,244.51,241.44,244.51Z"
|
||||||
|
style="fill: #fff; stroke: #263238; stroke-miterlimit: 10"
|
||||||
|
></path>
|
||||||
|
<line
|
||||||
|
x1="254.06"
|
||||||
|
y1="253.77"
|
||||||
|
x2="253.75"
|
||||||
|
y2="257.53"
|
||||||
|
style="fill: none; stroke: #263238; stroke-miterlimit: 10"
|
||||||
|
></line>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts"></script>
|
||||||
|
<style scoped></style>
|
7
src/components/svg/index.ts
Normal file
7
src/components/svg/index.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import SvgNoPermission from './SvgNoPermission.vue';
|
||||||
|
import SvgNotFound from './SvgNotFound.vue';
|
||||||
|
import SvgServiceError from './SvgServiceError.vue';
|
||||||
|
import SvgEmptyData from './SvgEmptyData.vue';
|
||||||
|
import SvgNetworkError from './SvgNetworkError.vue';
|
||||||
|
|
||||||
|
export { SvgNoPermission, SvgNotFound, SvgServiceError, SvgEmptyData, SvgNetworkError };
|
1
src/composables/business/index.ts
Normal file
1
src/composables/business/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './login';
|
57
src/composables/business/login.ts
Normal file
57
src/composables/business/login.ts
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
import { useAuthStore } from '@/store';
|
||||||
|
import { useLoading } from '@/hooks';
|
||||||
|
import { setToken, setRefreshToken, setUserInfo, log } from '@/utils';
|
||||||
|
import type { LoginToken, UserInfo } from '@/interface';
|
||||||
|
import { useRouterPush, useRouteQuery } from '../common';
|
||||||
|
|
||||||
|
export function useLogin() {
|
||||||
|
const auth = useAuthStore();
|
||||||
|
const { setAuthState } = useAuthStore();
|
||||||
|
const { toLoginRedirect } = useRouterPush();
|
||||||
|
const { loginRedirect } = useRouteQuery();
|
||||||
|
const { loading, startLoading, endLoading } = useLoading();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 登录注册
|
||||||
|
* @param param - 请求参数
|
||||||
|
* - phone: 手机号
|
||||||
|
* - pwdOrCode: 密码或验证码
|
||||||
|
* - type: 登录方式: pwd - 密码登录; sms - 验证码登录
|
||||||
|
* @returns 是否登录成功
|
||||||
|
*/
|
||||||
|
async function login(param: { phone: string; pwdOrCode: string; type: 'pwd' | 'sms' }) {
|
||||||
|
log(param); // 打印参数(接入接口后去除)
|
||||||
|
|
||||||
|
startLoading();
|
||||||
|
// 1.这里调用登录接口获取token和refreshToken
|
||||||
|
const loginToken: LoginToken = {
|
||||||
|
token: 'temp-token',
|
||||||
|
refreshToken: 'temp-refresh-token'
|
||||||
|
};
|
||||||
|
const { token, refreshToken } = loginToken;
|
||||||
|
setToken(token);
|
||||||
|
setRefreshToken(refreshToken);
|
||||||
|
// 2.这里调用获取用户信息的接口
|
||||||
|
const userInfo: UserInfo = {
|
||||||
|
userId: 'temp-user-id',
|
||||||
|
userName: 'Soybean',
|
||||||
|
userPhone: '15170283876'
|
||||||
|
};
|
||||||
|
setUserInfo(userInfo);
|
||||||
|
setAuthState({ token, userInfo });
|
||||||
|
|
||||||
|
// 3.登录成功后跳转重定向地址
|
||||||
|
toLoginRedirect(loginRedirect.value);
|
||||||
|
window.$notification?.success({
|
||||||
|
title: '登录成功!',
|
||||||
|
content: `欢迎回来,${auth.userInfo.userName}!`,
|
||||||
|
duration: 3000
|
||||||
|
});
|
||||||
|
endLoading();
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
loading,
|
||||||
|
login
|
||||||
|
};
|
||||||
|
}
|
@ -1,4 +1,5 @@
|
|||||||
import { computed, watch } from 'vue';
|
import { computed, watch } from 'vue';
|
||||||
|
import type { WatchOptions } from 'vue';
|
||||||
import { useRoute } from 'vue-router';
|
import { useRoute } from 'vue-router';
|
||||||
import { routeName } from '@/router';
|
import { routeName } from '@/router';
|
||||||
import type { RouteKey } from '@/interface';
|
import type { RouteKey } from '@/interface';
|
||||||
@ -33,7 +34,7 @@ export function useRouteQuery() {
|
|||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
|
|
||||||
/** 登录跳转链接 */
|
/** 登录跳转链接 */
|
||||||
const loginRedirectUrl = computed(() => {
|
const loginRedirect = computed(() => {
|
||||||
let url: string | undefined;
|
let url: string | undefined;
|
||||||
if (route.name === routeName('login')) {
|
if (route.name === routeName('login')) {
|
||||||
url = (route.query?.redirect as string) || '';
|
url = (route.query?.redirect as string) || '';
|
||||||
@ -42,7 +43,7 @@ export function useRouteQuery() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
loginRedirectUrl
|
loginRedirect
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -50,13 +51,14 @@ export function useRouteQuery() {
|
|||||||
* 路由名称变化后的回调
|
* 路由名称变化后的回调
|
||||||
* @param callback
|
* @param callback
|
||||||
*/
|
*/
|
||||||
export function routeNameWatcher(callback: (name: RouteKey) => void) {
|
export function routeNameWatcher(callback: (name: RouteKey) => void, options?: WatchOptions) {
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
watch(
|
watch(
|
||||||
() => route.name,
|
() => route.name,
|
||||||
newValue => {
|
newValue => {
|
||||||
callback(newValue as RouteKey);
|
callback(newValue as RouteKey);
|
||||||
}
|
},
|
||||||
|
options
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -64,12 +66,29 @@ export function routeNameWatcher(callback: (name: RouteKey) => void) {
|
|||||||
* 路由全路径变化后的回调
|
* 路由全路径变化后的回调
|
||||||
* @param callback
|
* @param callback
|
||||||
*/
|
*/
|
||||||
export function routeFullPathWatcher(callback: (fullPath: string) => void) {
|
export function routeFullPathWatcher(callback: (fullPath: string) => void, options?: WatchOptions) {
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
watch(
|
watch(
|
||||||
() => route.fullPath,
|
() => route.fullPath,
|
||||||
newValue => {
|
newValue => {
|
||||||
callback(newValue);
|
callback(newValue);
|
||||||
}
|
},
|
||||||
|
options
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 路由路径变化后的回调
|
||||||
|
* @param callback - 回调函数
|
||||||
|
* @param options - 监听配置
|
||||||
|
*/
|
||||||
|
export function routePathWatcher(callback: (path: string) => void, options?: WatchOptions) {
|
||||||
|
const route = useRoute();
|
||||||
|
watch(
|
||||||
|
() => route.path,
|
||||||
|
newValue => {
|
||||||
|
callback(newValue);
|
||||||
|
},
|
||||||
|
options
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -21,10 +21,15 @@ export function useRouterPush(inSetup: boolean = true) {
|
|||||||
if (newTab) {
|
if (newTab) {
|
||||||
const routerData = router.resolve(to);
|
const routerData = router.resolve(to);
|
||||||
window.open(routerData.href, '_blank');
|
window.open(routerData.href, '_blank');
|
||||||
return;
|
} else {
|
||||||
}
|
|
||||||
router.push(to);
|
router.push(to);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 返回上一级路由 */
|
||||||
|
function routerBack() {
|
||||||
|
router.go(-1);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 跳转首页
|
* 跳转首页
|
||||||
@ -46,7 +51,7 @@ export function useRouterPush(inSetup: boolean = true) {
|
|||||||
* @param redirect - 重定向地址(登录成功后跳转的地址)
|
* @param redirect - 重定向地址(登录成功后跳转的地址)
|
||||||
* @param newTab - 在新的浏览器标签打开
|
* @param newTab - 在新的浏览器标签打开
|
||||||
*/
|
*/
|
||||||
function toLogin(module: LoginModuleType = 'code-login', redirect: LoginRedirect = 'current', newTab = false) {
|
function toLogin(module: LoginModuleType = 'pwd-login', redirect: LoginRedirect = 'current', newTab = false) {
|
||||||
const routeLocation: RouteLocationRaw = {
|
const routeLocation: RouteLocationRaw = {
|
||||||
name: routeName('login'),
|
name: routeName('login'),
|
||||||
params: { module }
|
params: { module }
|
||||||
@ -71,10 +76,24 @@ export function useRouterPush(inSetup: boolean = true) {
|
|||||||
routerPush({ name: routeName('login'), params: { module }, query: { ...query } }, newTab);
|
routerPush({ name: routeName('login'), params: { module }, query: { ...query } }, newTab);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 登录成功后跳转重定向的地址
|
||||||
|
* @param redirect - 重定向地址
|
||||||
|
*/
|
||||||
|
function toLoginRedirect(redirect?: string) {
|
||||||
|
if (redirect) {
|
||||||
|
routerPush(redirect);
|
||||||
|
} else {
|
||||||
|
toHome();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
routerPush,
|
routerPush,
|
||||||
|
routerBack,
|
||||||
toHome,
|
toHome,
|
||||||
toLogin,
|
toLogin,
|
||||||
toCurrentLogin
|
toCurrentLogin,
|
||||||
|
toLoginRedirect
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,23 @@
|
|||||||
import { useBreakpoints, breakpointsTailwind } from '@vueuse/core';
|
import { useBreakpoints, breakpointsTailwind } from '@vueuse/core';
|
||||||
|
|
||||||
/** 项目名称 */
|
interface AppInfo {
|
||||||
export function useAppTitle() {
|
/** 项目名称 */
|
||||||
return import.meta.env.VITE_APP_TITLE as string;
|
name: string;
|
||||||
|
/** 项目标题 */
|
||||||
|
title: string;
|
||||||
|
/** 项目描述 */
|
||||||
|
desc: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 项目信息 */
|
||||||
|
export function useAppInfo(): AppInfo {
|
||||||
|
const { VITE_APP_NAME: name, VITE_APP_TITLE: title, VITE_APP_DESC: desc } = import.meta.env;
|
||||||
|
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
title,
|
||||||
|
desc
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 是否是移动端 */
|
/** 是否是移动端 */
|
||||||
|
@ -57,3 +57,30 @@ export function useDarkMode() {
|
|||||||
naiveTheme
|
naiveTheme
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 更改html样式 */
|
||||||
|
export function useHtmlStyle() {
|
||||||
|
const HIDE_SCROLL_CLASS = 'overflow-hidden';
|
||||||
|
|
||||||
|
function getHtmlElement() {
|
||||||
|
return document.querySelector('html');
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleHideScroll() {
|
||||||
|
const html = getHtmlElement();
|
||||||
|
if (html) {
|
||||||
|
html.classList.add(HIDE_SCROLL_CLASS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function handleAutoScroll() {
|
||||||
|
const html = getHtmlElement();
|
||||||
|
if (html) {
|
||||||
|
html.classList.remove(HIDE_SCROLL_CLASS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
handleHideScroll,
|
||||||
|
handleAutoScroll
|
||||||
|
};
|
||||||
|
}
|
||||||
|
31
src/composables/events/auth.ts
Normal file
31
src/composables/events/auth.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import { onMounted, onUnmounted } from 'vue';
|
||||||
|
import { useAuthStore } from '@/store';
|
||||||
|
|
||||||
|
/** 添加用户权益变更的全局点击事件监听 */
|
||||||
|
export function useAuthChangeEvent() {
|
||||||
|
const { getIsAuthChange } = useAuthStore();
|
||||||
|
|
||||||
|
function eventHandler(event: MouseEvent) {
|
||||||
|
const change = getIsAuthChange();
|
||||||
|
if (change) {
|
||||||
|
event.stopPropagation();
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function addAuthChangeListener() {
|
||||||
|
document.addEventListener('click', eventHandler, { capture: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeAuthChangeListener() {
|
||||||
|
document.removeEventListener('click', eventHandler);
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
addAuthChangeListener();
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
removeAuthChangeListener();
|
||||||
|
});
|
||||||
|
}
|
12
src/composables/events/global.ts
Normal file
12
src/composables/events/global.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { useAuthChangeEvent } from './auth';
|
||||||
|
|
||||||
|
export function useGlobalEvent() {
|
||||||
|
/** 初始化全局监听事件 */
|
||||||
|
function initGlobalListener() {
|
||||||
|
useAuthChangeEvent();
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
initGlobalListener
|
||||||
|
};
|
||||||
|
}
|
1
src/composables/events/index.ts
Normal file
1
src/composables/events/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './global';
|
@ -1 +1,3 @@
|
|||||||
export * from './common';
|
export * from './common';
|
||||||
|
export * from './business';
|
||||||
|
export * from './events';
|
||||||
|
@ -1,2 +1,3 @@
|
|||||||
|
export * from './regexp';
|
||||||
export * from './service';
|
export * from './service';
|
||||||
export * from './map-sdk';
|
export * from './map-sdk';
|
||||||
|
16
src/config/common/regexp.ts
Normal file
16
src/config/common/regexp.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
/** 手机号码正则 */
|
||||||
|
export const REGEXP_PHONE =
|
||||||
|
/^[1](([3][0-9])|([4][0,1,4-9])|([5][0-3,5-9])|([6][2,5,6,7])|([7][0-8])|([8][0-9])|([9][0-3,5-9]))[0-9]{8}$/;
|
||||||
|
|
||||||
|
/** 邮箱正则 */
|
||||||
|
export const REGEXP_EMAIL = /^\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$/;
|
||||||
|
|
||||||
|
/** 密码正则(密码为8-18位数字/字符/符号的组合) */
|
||||||
|
export const REGEXP_PWD =
|
||||||
|
/^(?![0-9]+$)(?![a-z]+$)(?![A-Z]+$)(?!([^(0-9a-zA-Z)]|[()])+$)(?!^.*[\u4E00-\u9FA5].*$)([^(0-9a-zA-Z)]|[()]|[a-z]|[A-Z]|[0-9]){8,18}$/;
|
||||||
|
|
||||||
|
/** 验证码正则(6位数字) */
|
||||||
|
export const REGEXP_CODE = /^\d{6}$/;
|
||||||
|
|
||||||
|
/** 图片验证码正则(4位数字) */
|
||||||
|
export const REGEXP_IMG_CODE = /^\d{4}$/;
|
@ -21,7 +21,7 @@ export const NETWORK_ERROR_MSG = '网络不可用~';
|
|||||||
|
|
||||||
/** 请求不成功各种状态的错误 */
|
/** 请求不成功各种状态的错误 */
|
||||||
export const ERROR_STATUS = {
|
export const ERROR_STATUS = {
|
||||||
400: '400: 请求出现语法错误',
|
400: '400: 请求出现语法错误~',
|
||||||
401: '401: 用户未授权~',
|
401: '401: 用户未授权~',
|
||||||
403: '403: 服务器拒绝访问~',
|
403: '403: 服务器拒绝访问~',
|
||||||
404: '404: 请求的资源不存在~',
|
404: '404: 请求的资源不存在~',
|
||||||
|
@ -1,10 +0,0 @@
|
|||||||
import useReloadContext from './useReloadContext';
|
|
||||||
|
|
||||||
const { useReloadProvide, useReloadInject } = useReloadContext();
|
|
||||||
|
|
||||||
/** 从App组件注入provide */
|
|
||||||
function setupAppContext() {
|
|
||||||
useReloadProvide();
|
|
||||||
}
|
|
||||||
|
|
||||||
export { setupAppContext, useReloadInject };
|
|
@ -1,33 +0,0 @@
|
|||||||
import { ref, nextTick } from 'vue';
|
|
||||||
import type { Ref } from 'vue';
|
|
||||||
import { useContext } from '@/hooks';
|
|
||||||
|
|
||||||
interface ReloadContext {
|
|
||||||
reload: Ref<boolean>;
|
|
||||||
handleReload(): void;
|
|
||||||
}
|
|
||||||
const { useProvide, useInject: useReloadInject } = useContext<ReloadContext>();
|
|
||||||
|
|
||||||
/** 重载上下文 */
|
|
||||||
export default function useReloadContext() {
|
|
||||||
const reload = ref(true);
|
|
||||||
function handleReload() {
|
|
||||||
reload.value = false;
|
|
||||||
nextTick(() => {
|
|
||||||
reload.value = true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const context: ReloadContext = {
|
|
||||||
reload,
|
|
||||||
handleReload
|
|
||||||
};
|
|
||||||
function useReloadProvide() {
|
|
||||||
useProvide(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
useReloadProvide,
|
|
||||||
useReloadInject
|
|
||||||
};
|
|
||||||
}
|
|
@ -1,2 +0,0 @@
|
|||||||
export * from './app';
|
|
||||||
export * from './part';
|
|
@ -1,3 +0,0 @@
|
|||||||
import useVerticalMixSiderContext from './useVerticalMixSiderContext';
|
|
||||||
|
|
||||||
export { useVerticalMixSiderContext };
|
|
@ -1,54 +0,0 @@
|
|||||||
import { ref } from 'vue';
|
|
||||||
import type { Ref } from 'vue';
|
|
||||||
import { useContext, useBoolean } from '@/hooks';
|
|
||||||
|
|
||||||
interface VerticalMixSiderContext {
|
|
||||||
/** 子菜单可见性 */
|
|
||||||
childMenuVisible: Ref<boolean>;
|
|
||||||
/** 展示子菜单 */
|
|
||||||
showChildMenu(): void;
|
|
||||||
/** 隐藏子菜单 */
|
|
||||||
hideChildMenu(): void;
|
|
||||||
/** 鼠标悬浮的一级菜单对应的路由名称 */
|
|
||||||
hoverRouteName: Ref<string>;
|
|
||||||
/** 设置悬浮路由名称 */
|
|
||||||
setHoverRouteName(name: string): void;
|
|
||||||
isMouseEnterChildMenu: Ref<boolean>;
|
|
||||||
setMouseEnterChildMenu(): void;
|
|
||||||
setMouseLeaveChildMenu(): void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { useProvide, useInject: useVerticalMixSiderInject } = useContext<VerticalMixSiderContext>();
|
|
||||||
|
|
||||||
export default function useVerticalMixSiderContext() {
|
|
||||||
const { bool: childMenuVisible, setTrue: showChildMenu, setFalse: hideChildMenu } = useBoolean();
|
|
||||||
const {
|
|
||||||
bool: isMouseEnterChildMenu,
|
|
||||||
setTrue: setMouseEnterChildMenu,
|
|
||||||
setFalse: setMouseLeaveChildMenu
|
|
||||||
} = useBoolean();
|
|
||||||
|
|
||||||
const hoverRouteName = ref('');
|
|
||||||
function setHoverRouteName(name: string) {
|
|
||||||
hoverRouteName.value = name;
|
|
||||||
}
|
|
||||||
|
|
||||||
const context: VerticalMixSiderContext = {
|
|
||||||
childMenuVisible,
|
|
||||||
showChildMenu,
|
|
||||||
hideChildMenu,
|
|
||||||
hoverRouteName,
|
|
||||||
setHoverRouteName,
|
|
||||||
isMouseEnterChildMenu,
|
|
||||||
setMouseEnterChildMenu,
|
|
||||||
setMouseLeaveChildMenu
|
|
||||||
};
|
|
||||||
function useVerticalMixSiderProvide() {
|
|
||||||
useProvide(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
useVerticalMixSiderProvide,
|
|
||||||
useVerticalMixSiderInject
|
|
||||||
};
|
|
||||||
}
|
|
@ -0,0 +1,8 @@
|
|||||||
|
import type { App } from 'vue';
|
||||||
|
import setupNetworkDirective from './network';
|
||||||
|
import setupLoginDirective from './login';
|
||||||
|
|
||||||
|
export function setupDirectives(app: App) {
|
||||||
|
setupNetworkDirective(app);
|
||||||
|
setupLoginDirective(app);
|
||||||
|
}
|
27
src/directives/login.ts
Normal file
27
src/directives/login.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import type { App, Directive } from 'vue';
|
||||||
|
import { useAuthStore } from '@/store';
|
||||||
|
import { useRouterPush } from '@/composables';
|
||||||
|
|
||||||
|
export default function setupLoginDirective(app: App) {
|
||||||
|
const auth = useAuthStore();
|
||||||
|
const { toLogin } = useRouterPush(false);
|
||||||
|
function listenerHandler(event: MouseEvent) {
|
||||||
|
if (!auth.isLogin) {
|
||||||
|
event.stopPropagation();
|
||||||
|
toLogin();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const loginDirective: Directive<HTMLElement, boolean | undefined> = {
|
||||||
|
mounted(el: HTMLElement, binding) {
|
||||||
|
if (binding.value === false) return;
|
||||||
|
el.addEventListener('click', listenerHandler, { capture: true });
|
||||||
|
},
|
||||||
|
unmounted(el: HTMLElement, binding) {
|
||||||
|
if (binding.value === false) return;
|
||||||
|
el.removeEventListener('click', listenerHandler);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
app.directive('login', loginDirective);
|
||||||
|
}
|
25
src/directives/network.ts
Normal file
25
src/directives/network.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import type { App, Directive } from 'vue';
|
||||||
|
import { NETWORK_ERROR_MSG } from '@/config';
|
||||||
|
|
||||||
|
export default function setupNetworkDirective(app: App) {
|
||||||
|
function listenerHandler(event: MouseEvent) {
|
||||||
|
const hasNetwork = window.navigator.onLine;
|
||||||
|
if (!hasNetwork) {
|
||||||
|
window.$message?.error(NETWORK_ERROR_MSG);
|
||||||
|
event.stopPropagation();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const networkDirective: Directive<HTMLElement, boolean | undefined> = {
|
||||||
|
mounted(el: HTMLElement, binding) {
|
||||||
|
if (binding.value === false) return;
|
||||||
|
el.addEventListener('click', listenerHandler, { capture: true });
|
||||||
|
},
|
||||||
|
unmounted(el: HTMLElement, binding) {
|
||||||
|
if (binding.value === false) return;
|
||||||
|
el.removeEventListener('click', listenerHandler);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
app.directive('network', networkDirective);
|
||||||
|
}
|
@ -1,4 +1,9 @@
|
|||||||
|
import useAntv from './useAntv';
|
||||||
|
import useAntvTool from './useAntvTool';
|
||||||
import useCountDown from './useCountDown';
|
import useCountDown from './useCountDown';
|
||||||
import useSmsCode from './useSmsCode';
|
import useSmsCode from './useSmsCode';
|
||||||
|
import useImageVerify from './useImageVerify';
|
||||||
|
import useAgreement from './useAgreement';
|
||||||
|
import useVirtualList from './useVirtualList';
|
||||||
|
|
||||||
export { useCountDown, useSmsCode };
|
export { useAntv, useAntvTool, useCountDown, useSmsCode, useImageVerify, useAgreement, useVirtualList };
|
||||||
|
20
src/hooks/business/useAgreement.ts
Normal file
20
src/hooks/business/useAgreement.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
/** 使用勾选协议 */
|
||||||
|
export default function useAgreement(text = '请勾选 "我已经仔细阅读并接受《用户协议》《隐私权政策》"') {
|
||||||
|
const agreement = ref(true);
|
||||||
|
|
||||||
|
function isAgree() {
|
||||||
|
let agree = true;
|
||||||
|
if (!agreement.value) {
|
||||||
|
agree = false;
|
||||||
|
window.$message?.error(text);
|
||||||
|
}
|
||||||
|
return agree;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
agreement,
|
||||||
|
isAgree
|
||||||
|
};
|
||||||
|
}
|
53
src/hooks/business/useAntv.ts
Normal file
53
src/hooks/business/useAntv.ts
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import { ref, watch, onMounted } from 'vue';
|
||||||
|
import type { ComputedRef } from 'vue';
|
||||||
|
import type { Plot } from '@antv/g2plot';
|
||||||
|
import { useBoolean } from '@/hooks';
|
||||||
|
|
||||||
|
interface AntvFn<T, O> {
|
||||||
|
new (dom: HTMLElement, options: O): T;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function useAntv<GraphOption, GraphType extends Plot<GraphOption>>(
|
||||||
|
GraphFn: AntvFn<GraphType, GraphOption>,
|
||||||
|
graphOptions: ComputedRef<GraphOption>
|
||||||
|
) {
|
||||||
|
/** 图表dom容器 */
|
||||||
|
const domRef = ref<HTMLElement>();
|
||||||
|
|
||||||
|
/** 图表实例 */
|
||||||
|
const graph = ref<GraphType>();
|
||||||
|
|
||||||
|
/** 是否可以开始渲染图表 */
|
||||||
|
const { bool: canRender, setTrue: setCanRender } = useBoolean();
|
||||||
|
|
||||||
|
/** 是否是在onMouted第一次渲染图表 */
|
||||||
|
const { bool: isFirstRender, setTrue: setIsFirstRender, setFalse: setIsNotFirstRender } = useBoolean();
|
||||||
|
|
||||||
|
/** 渲染图表 */
|
||||||
|
function renderGraph(options: GraphOption) {
|
||||||
|
if (!domRef.value) return;
|
||||||
|
if (!graph.value) {
|
||||||
|
graph.value = new GraphFn(domRef.value, options);
|
||||||
|
graph.value.render();
|
||||||
|
} else {
|
||||||
|
graph.value.update(options);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
setCanRender();
|
||||||
|
setIsFirstRender();
|
||||||
|
renderGraph(graphOptions.value);
|
||||||
|
setIsNotFirstRender();
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(graphOptions, newValue => {
|
||||||
|
if (!canRender.value || isFirstRender.value) return;
|
||||||
|
renderGraph(newValue);
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
domRef,
|
||||||
|
graph
|
||||||
|
};
|
||||||
|
}
|
31
src/hooks/business/useAntvTool.ts
Normal file
31
src/hooks/business/useAntvTool.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
export default function useAntvTool() {
|
||||||
|
/**
|
||||||
|
* antv滑动调属性
|
||||||
|
*/
|
||||||
|
function getSlider(columns: number, length: number, sliderColor: string) {
|
||||||
|
return {
|
||||||
|
start: 1 - columns / length,
|
||||||
|
end: 1,
|
||||||
|
foregroundStyle: { fill: sliderColor }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFormatter(unit: string) {
|
||||||
|
const EMPTY = ' ';
|
||||||
|
function formatter(v: number | null) {
|
||||||
|
return v === null ? EMPTY : v + unit;
|
||||||
|
}
|
||||||
|
return formatter;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatLabelWithUnit(value: number | null, unit: string) {
|
||||||
|
const EMPTY = ' ';
|
||||||
|
return value === null ? EMPTY : value + unit;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
getSlider,
|
||||||
|
getFormatter,
|
||||||
|
formatLabelWithUnit
|
||||||
|
};
|
||||||
|
}
|
85
src/hooks/business/useImageVerify.ts
Normal file
85
src/hooks/business/useImageVerify.ts
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
import { ref, onMounted } from 'vue';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 绘制图形验证码
|
||||||
|
* @param width - 图形宽度
|
||||||
|
* @param height - 图形高度
|
||||||
|
*/
|
||||||
|
export default function useImageVerify(width = 152, 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 + 23, 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;
|
||||||
|
}
|
@ -1,4 +1,5 @@
|
|||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
|
import { REGEXP_PHONE } from '@/config';
|
||||||
import useCountDown from './useCountDown';
|
import useCountDown from './useCountDown';
|
||||||
|
|
||||||
export default function useSmsCode() {
|
export default function useSmsCode() {
|
||||||
@ -7,9 +8,35 @@ export default function useSmsCode() {
|
|||||||
const countingLabel = (second: number) => `${second}秒后重新获取`;
|
const countingLabel = (second: number) => `${second}秒后重新获取`;
|
||||||
const label = computed(() => (isCounting.value ? countingLabel(counts.value) : initLabel));
|
const label = computed(() => (isCounting.value ? countingLabel(counts.value) : initLabel));
|
||||||
|
|
||||||
|
/** 判断手机号码格式是否正确 */
|
||||||
|
function isPhoneValid(phone: string) {
|
||||||
|
let valid = true;
|
||||||
|
if (phone.trim() === '') {
|
||||||
|
window.$message?.error('手机号码不能为空!');
|
||||||
|
valid = false;
|
||||||
|
} else if (!REGEXP_PHONE.test(phone)) {
|
||||||
|
window.$message?.error('手机号码格式错误!');
|
||||||
|
valid = false;
|
||||||
|
}
|
||||||
|
return valid;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取短信验证码
|
||||||
|
* @param phone - 手机号
|
||||||
|
*/
|
||||||
|
async function getSmsCode(phone: string) {
|
||||||
|
const valid = isPhoneValid(phone);
|
||||||
|
if (!valid) return;
|
||||||
|
// 该处调用验证码接口
|
||||||
|
window.$message?.success('验证码发送成功!');
|
||||||
|
start();
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
label,
|
label,
|
||||||
start,
|
start,
|
||||||
isCounting
|
isCounting,
|
||||||
|
getSmsCode
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
60
src/hooks/business/useVirtualList.ts
Normal file
60
src/hooks/business/useVirtualList.ts
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import { ref, watch, computed, nextTick } from 'vue';
|
||||||
|
import type { Ref, ComputedRef } from 'vue';
|
||||||
|
|
||||||
|
interface VirtualListConfig {
|
||||||
|
/** 容器的高度 */
|
||||||
|
containerHeight: number;
|
||||||
|
/** 渲染的个数 */
|
||||||
|
renderNums: number;
|
||||||
|
/** 触发的高度距离 */
|
||||||
|
triggerHeight: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 虚拟列表
|
||||||
|
* @param list - 列表数据源
|
||||||
|
* @param config - 虚拟列表配置
|
||||||
|
*/
|
||||||
|
export default function useVirtualList<T extends { [key: string]: any }[]>(
|
||||||
|
list: Ref<T>,
|
||||||
|
config: VirtualListConfig = {
|
||||||
|
containerHeight: 200,
|
||||||
|
renderNums: 10,
|
||||||
|
triggerHeight: 24
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
const { containerHeight, renderNums, triggerHeight } = config;
|
||||||
|
|
||||||
|
const renderIndex = ref(1);
|
||||||
|
function setRenderIndex(index: number) {
|
||||||
|
renderIndex.value = index;
|
||||||
|
}
|
||||||
|
function resetRenderIndex() {
|
||||||
|
setRenderIndex(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const dataSource = computed(() => {
|
||||||
|
const endIndex = renderIndex.value * renderNums;
|
||||||
|
return list.value.slice(0, endIndex);
|
||||||
|
}) as ComputedRef<T>;
|
||||||
|
|
||||||
|
function handleScroll(e: Event) {
|
||||||
|
const target = e.target as HTMLElement;
|
||||||
|
const needRender = target.scrollHeight - (target.scrollTop + containerHeight) < triggerHeight;
|
||||||
|
if (needRender) {
|
||||||
|
nextTick(() => {
|
||||||
|
setRenderIndex(renderIndex.value + 1);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(list, () => {
|
||||||
|
resetRenderIndex();
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
containerHeight,
|
||||||
|
dataSource,
|
||||||
|
handleScroll
|
||||||
|
};
|
||||||
|
}
|
@ -1,5 +1,6 @@
|
|||||||
import useContext from './useContext';
|
import useContext from './useContext';
|
||||||
import useBoolean from './useBoolean';
|
import useBoolean from './useBoolean';
|
||||||
import useLoading from './useLoading';
|
import useLoading from './useLoading';
|
||||||
|
import useLoadingEmpty from './useLoadingEmpty';
|
||||||
|
|
||||||
export { useContext, useBoolean, useLoading };
|
export { useContext, useBoolean, useLoading, useLoadingEmpty };
|
||||||
|
14
src/hooks/common/useLoadingEmpty.ts
Normal file
14
src/hooks/common/useLoadingEmpty.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import useBoolean from './useBoolean';
|
||||||
|
|
||||||
|
export default function useLoadingEmpty(initLoading = false, initEmpty = false) {
|
||||||
|
const { bool: loading, setTrue: startLoading, setFalse: endLoading } = useBoolean(initLoading);
|
||||||
|
const { bool: empty, setBool: setEmpty } = useBoolean(initEmpty);
|
||||||
|
|
||||||
|
return {
|
||||||
|
loading,
|
||||||
|
startLoading,
|
||||||
|
endLoading,
|
||||||
|
empty,
|
||||||
|
setEmpty
|
||||||
|
};
|
||||||
|
}
|
@ -1,3 +1,11 @@
|
|||||||
|
/** 登录token */
|
||||||
|
export interface LoginToken {
|
||||||
|
/** token */
|
||||||
|
token: string;
|
||||||
|
/** 刷新token(用户token到期后换取新的token) */
|
||||||
|
refreshToken: string;
|
||||||
|
}
|
||||||
|
|
||||||
/** 用户信息 */
|
/** 用户信息 */
|
||||||
export interface UserInfo {
|
export interface UserInfo {
|
||||||
/** 用户id */
|
/** 用户id */
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
export * from './auth';
|
export * from './auth';
|
||||||
export * from './demo';
|
export * from './demo';
|
||||||
export * from './website';
|
export * from './website';
|
||||||
|
export * from './s-graph';
|
||||||
|
53
src/interface/business/s-graph.ts
Normal file
53
src/interface/business/s-graph.ts
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
/** 缩放比例取值范围 */
|
||||||
|
export type SScaleRange = [number, number];
|
||||||
|
|
||||||
|
/** 偏移量 */
|
||||||
|
export interface STranslate {
|
||||||
|
/** X偏移量 */
|
||||||
|
x: number;
|
||||||
|
/** Y偏移量 */
|
||||||
|
y: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 位置 */
|
||||||
|
export interface SPosition {
|
||||||
|
/** x坐标 */
|
||||||
|
x: number;
|
||||||
|
/** y坐标 */
|
||||||
|
y: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 坐标 */
|
||||||
|
export interface SCoord {
|
||||||
|
/** x坐标 */
|
||||||
|
x: number;
|
||||||
|
/** y坐标 */
|
||||||
|
y: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 节点尺寸 */
|
||||||
|
export interface SNodeSize {
|
||||||
|
/** 节点宽 */
|
||||||
|
w: number;
|
||||||
|
/** 节点高 */
|
||||||
|
h: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 图的节点 */
|
||||||
|
export interface SGraphNode extends SCoord {
|
||||||
|
/** 节点id */
|
||||||
|
id: string;
|
||||||
|
/** 节点名称 */
|
||||||
|
label: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 图的关系线 */
|
||||||
|
export interface SGraphEdge {
|
||||||
|
sourceCoord: SCoord;
|
||||||
|
targetCoord: SCoord;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SGraphData {
|
||||||
|
nodes: SGraphNode[];
|
||||||
|
edges: SGraphEdge[];
|
||||||
|
}
|
@ -2,20 +2,25 @@
|
|||||||
<n-dropdown :options="options" @select="handleDropdown">
|
<n-dropdown :options="options" @select="handleDropdown">
|
||||||
<hover-container class="px-12px">
|
<hover-container class="px-12px">
|
||||||
<img :src="avatar" class="w-32px h-32px" />
|
<img :src="avatar" class="w-32px h-32px" />
|
||||||
<span class="pl-8px text-16px font-medium">Soybean</span>
|
<span class="pl-8px text-16px font-medium">{{ auth.userInfo.userName }}</span>
|
||||||
</hover-container>
|
</hover-container>
|
||||||
</n-dropdown>
|
</n-dropdown>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
import { useRoute } from 'vue-router';
|
||||||
import { NDropdown, useDialog } from 'naive-ui';
|
import { NDropdown, useDialog } from 'naive-ui';
|
||||||
import { HoverContainer } from '@/components';
|
import { HoverContainer } from '@/components';
|
||||||
|
import { useAuthStore } from '@/store';
|
||||||
import { useRouterPush } from '@/composables';
|
import { useRouterPush } from '@/composables';
|
||||||
import { iconifyRender, resetAuthStorage } from '@/utils';
|
import { iconifyRender } from '@/utils';
|
||||||
import avatar from '@/assets/svg/avatar/avatar01.svg';
|
import avatar from '@/assets/svg/avatar/avatar01.svg';
|
||||||
|
|
||||||
type DropdownKey = 'user-center' | 'logout';
|
type DropdownKey = 'user-center' | 'logout';
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
const auth = useAuthStore();
|
||||||
|
const { resetAuthState } = useAuthStore();
|
||||||
const { toLogin } = useRouterPush();
|
const { toLogin } = useRouterPush();
|
||||||
const dialog = useDialog();
|
const dialog = useDialog();
|
||||||
|
|
||||||
@ -45,9 +50,11 @@ function handleDropdown(optionKey: string) {
|
|||||||
positiveText: '确定',
|
positiveText: '确定',
|
||||||
negativeText: '取消',
|
negativeText: '取消',
|
||||||
onPositiveClick: () => {
|
onPositiveClick: () => {
|
||||||
resetAuthStorage();
|
resetAuthState();
|
||||||
|
if (route.meta.requiresAuth) {
|
||||||
toLogin();
|
toLogin();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,7 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { SystemLogo } from '@/components';
|
import { SystemLogo } from '@/components';
|
||||||
import { useThemeStore } from '@/store';
|
import { useThemeStore } from '@/store';
|
||||||
import { useAppTitle, useLayoutConfig } from '@/composables';
|
import { useAppInfo, useLayoutConfig } from '@/composables';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
/** 显示名字 */
|
/** 显示名字 */
|
||||||
@ -18,7 +18,7 @@ interface Props {
|
|||||||
defineProps<Props>();
|
defineProps<Props>();
|
||||||
|
|
||||||
const theme = useThemeStore();
|
const theme = useThemeStore();
|
||||||
const title = useAppTitle();
|
const { title } = useAppInfo();
|
||||||
const { headerHeight } = useLayoutConfig();
|
const { headerHeight } = useLayoutConfig();
|
||||||
</script>
|
</script>
|
||||||
<style scoped></style>
|
<style scoped></style>
|
||||||
|
@ -24,7 +24,7 @@ import { useRouter, useRoute } from 'vue-router';
|
|||||||
import { NScrollbar, NMenu } from 'naive-ui';
|
import { NScrollbar, NMenu } from 'naive-ui';
|
||||||
import type { MenuOption } from 'naive-ui';
|
import type { MenuOption } from 'naive-ui';
|
||||||
import { useThemeStore, useAppStore } from '@/store';
|
import { useThemeStore, useAppStore } from '@/store';
|
||||||
import { useAppTitle } from '@/composables';
|
import { useAppInfo } from '@/composables';
|
||||||
import { menus } from '@/router';
|
import { menus } from '@/router';
|
||||||
import type { GlobalMenuOption } from '@/interface';
|
import type { GlobalMenuOption } from '@/interface';
|
||||||
|
|
||||||
@ -44,7 +44,7 @@ const route = useRoute();
|
|||||||
const theme = useThemeStore();
|
const theme = useThemeStore();
|
||||||
const app = useAppStore();
|
const app = useAppStore();
|
||||||
const { toggleFixedMixMenu } = useAppStore();
|
const { toggleFixedMixMenu } = useAppStore();
|
||||||
const title = useAppTitle();
|
const { title } = useAppInfo();
|
||||||
|
|
||||||
const childMenus = computed(() => {
|
const childMenus = computed(() => {
|
||||||
const children: MenuOption[] = [];
|
const children: MenuOption[] = [];
|
||||||
|
@ -24,7 +24,7 @@ import { useRouter, useRoute } from 'vue-router';
|
|||||||
import { NScrollbar, NMenu } from 'naive-ui';
|
import { NScrollbar, NMenu } from 'naive-ui';
|
||||||
import type { MenuOption } from 'naive-ui';
|
import type { MenuOption } from 'naive-ui';
|
||||||
import { useThemeStore, useAppStore } from '@/store';
|
import { useThemeStore, useAppStore } from '@/store';
|
||||||
import { useAppTitle } from '@/composables';
|
import { useAppInfo } from '@/composables';
|
||||||
import { menus } from '@/router';
|
import { menus } from '@/router';
|
||||||
import type { GlobalMenuOption } from '@/interface';
|
import type { GlobalMenuOption } from '@/interface';
|
||||||
|
|
||||||
@ -44,7 +44,7 @@ const route = useRoute();
|
|||||||
const theme = useThemeStore();
|
const theme = useThemeStore();
|
||||||
const app = useAppStore();
|
const app = useAppStore();
|
||||||
const { toggleFixedMixMenu } = useAppStore();
|
const { toggleFixedMixMenu } = useAppStore();
|
||||||
const title = useAppTitle();
|
const { title } = useAppInfo();
|
||||||
|
|
||||||
const childMenus = computed(() => {
|
const childMenus = computed(() => {
|
||||||
const children: MenuOption[] = [];
|
const children: MenuOption[] = [];
|
||||||
|
@ -4,6 +4,7 @@ import AppProvider from './AppProvider.vue';
|
|||||||
import { setupStore } from './store';
|
import { setupStore } from './store';
|
||||||
import { setupRouter } from './router';
|
import { setupRouter } from './router';
|
||||||
import { setupAssets } from './plugins';
|
import { setupAssets } from './plugins';
|
||||||
|
import { setupDirectives } from './directives';
|
||||||
|
|
||||||
function setupPlugins() {
|
function setupPlugins() {
|
||||||
/** 引入静态资源 */
|
/** 引入静态资源 */
|
||||||
@ -20,6 +21,9 @@ async function setupApp() {
|
|||||||
// 优先挂载一下 appProvider 解决路由守卫,Axios中可使用,LoadingBar,Dialog,Message 等之类组件
|
// 优先挂载一下 appProvider 解决路由守卫,Axios中可使用,LoadingBar,Dialog,Message 等之类组件
|
||||||
appProvider.mount('#appProvider');
|
appProvider.mount('#appProvider');
|
||||||
|
|
||||||
|
// 挂载自定义vue指令
|
||||||
|
setupDirectives(app);
|
||||||
|
|
||||||
// 挂载路由
|
// 挂载路由
|
||||||
await setupRouter(app);
|
await setupRouter(app);
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { defineStore } from 'pinia';
|
import { defineStore } from 'pinia';
|
||||||
import { store } from '@/store';
|
import { store } from '@/store';
|
||||||
import { removeToken } from '@/utils';
|
import { clearAuthStorage, getToken, getUserInfo } from '@/utils';
|
||||||
import type { UserInfo } from '@/interface';
|
import type { UserInfo } from '@/interface';
|
||||||
|
|
||||||
interface AuthState {
|
interface AuthState {
|
||||||
@ -16,12 +16,8 @@ const authStore = defineStore({
|
|||||||
/** 状态 */
|
/** 状态 */
|
||||||
state: (): AuthState => {
|
state: (): AuthState => {
|
||||||
return {
|
return {
|
||||||
token: '',
|
token: getToken(),
|
||||||
userInfo: {
|
userInfo: getUserInfo()
|
||||||
userId: '',
|
|
||||||
userName: 'Soybean',
|
|
||||||
userPhone: ''
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
getters: {
|
getters: {
|
||||||
@ -29,10 +25,20 @@ const authStore = defineStore({
|
|||||||
isLogin: state => Boolean(state.token)
|
isLogin: state => Boolean(state.token)
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
|
/** 设置Auth状态 */
|
||||||
|
setAuthState(data: Partial<AuthState>) {
|
||||||
|
Object.assign(this, data);
|
||||||
|
},
|
||||||
/** 重置auth状态 */
|
/** 重置auth状态 */
|
||||||
resetAuthState() {
|
resetAuthState() {
|
||||||
removeToken();
|
clearAuthStorage();
|
||||||
this.$reset();
|
this.$reset();
|
||||||
|
},
|
||||||
|
/** 判断用户权益是否变更 */
|
||||||
|
getIsAuthChange() {
|
||||||
|
const token = getToken();
|
||||||
|
const tokenChange = token !== this.token;
|
||||||
|
return tokenChange;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
12
src/typings/env.d.ts
vendored
12
src/typings/env.d.ts
vendored
@ -1,12 +1,14 @@
|
|||||||
interface ImportMetaEnv {
|
interface ImportMetaEnv {
|
||||||
/** 项目标题 */
|
|
||||||
VITE_APP_TITLE: string;
|
|
||||||
/** 项目标题(文本) */
|
|
||||||
VITE_APP_TITLE_LABEL: string;
|
|
||||||
/** 项目基本地址 */
|
/** 项目基本地址 */
|
||||||
VITE_BASE_URL: string;
|
VITE_BASE_URL: string;
|
||||||
|
/** 项目名称 */
|
||||||
|
VITE_APP_NAME: string;
|
||||||
|
/** 项目标题 */
|
||||||
|
VITE_APP_TITLE: string;
|
||||||
|
/** 项目描述 */
|
||||||
|
VITE_APP_DESC: string;
|
||||||
/** 网路请求环境类型 */
|
/** 网路请求环境类型 */
|
||||||
VITE_HTTP_ENV: string;
|
VITE_HTTP_ENV: 'DEV' | 'PROD' | 'STAGING';
|
||||||
/** 网路请求地址 */
|
/** 网路请求地址 */
|
||||||
VITE_HTTP_URL: string;
|
VITE_HTTP_URL: string;
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { EnumStorageKey } from '@/enum';
|
import { EnumStorageKey } from '@/enum';
|
||||||
|
import type { UserInfo } from '@/interface';
|
||||||
import { setLocal, getLocal, removeLocal } from '../storage';
|
import { setLocal, getLocal, removeLocal } from '../storage';
|
||||||
|
|
||||||
/** 设置token */
|
/** 设置token */
|
||||||
@ -31,10 +32,28 @@ export function removeRefreshToken() {
|
|||||||
removeLocal(EnumStorageKey['refresh-koken']);
|
removeLocal(EnumStorageKey['refresh-koken']);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getUserInfo() {}
|
/** 设置用户信息 */
|
||||||
|
export function getUserInfo() {
|
||||||
|
const emptyInfo: UserInfo = {
|
||||||
|
userId: '',
|
||||||
|
userName: '',
|
||||||
|
userPhone: ''
|
||||||
|
};
|
||||||
|
const userInfo: UserInfo = getLocal<UserInfo>(EnumStorageKey['user-info']) || emptyInfo;
|
||||||
|
return userInfo;
|
||||||
|
}
|
||||||
|
/** 获取用户信息 */
|
||||||
|
export function setUserInfo(userInfo: UserInfo) {
|
||||||
|
setLocal(EnumStorageKey['user-info'], userInfo);
|
||||||
|
}
|
||||||
|
/** 去除用户信息 */
|
||||||
|
export function removeUserInfo() {
|
||||||
|
removeLocal(EnumStorageKey['user-info']);
|
||||||
|
}
|
||||||
|
|
||||||
/** 去除用户相关缓存 */
|
/** 去除用户相关缓存 */
|
||||||
export function resetAuthStorage() {
|
export function clearAuthStorage() {
|
||||||
removeToken();
|
removeToken();
|
||||||
removeRefreshToken();
|
removeRefreshToken();
|
||||||
|
removeUserInfo();
|
||||||
}
|
}
|
||||||
|
46
src/utils/common/browser.ts
Normal file
46
src/utils/common/browser.ts
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
interface BrowserInfo {
|
||||||
|
type: string;
|
||||||
|
version: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取浏览器版本信息 */
|
||||||
|
export function getBrowserInfo() {
|
||||||
|
const explorer = window.navigator.userAgent.toLowerCase();
|
||||||
|
const info: BrowserInfo = {
|
||||||
|
type: '',
|
||||||
|
version: ''
|
||||||
|
};
|
||||||
|
function setInfo(data: BrowserInfo) {
|
||||||
|
Object.assign(info, data);
|
||||||
|
}
|
||||||
|
// ie
|
||||||
|
if (explorer.indexOf('msie') >= 0) {
|
||||||
|
const [version] = explorer.match(/msie ([\d.]+)/) || [''];
|
||||||
|
setInfo({ type: 'IE', version });
|
||||||
|
}
|
||||||
|
// firefox
|
||||||
|
if (explorer.indexOf('firefox') >= 0) {
|
||||||
|
const [version] = explorer.match(/firefox\/([\d.]+)/) || [''];
|
||||||
|
setInfo({ type: 'Firefox', version });
|
||||||
|
}
|
||||||
|
// Chrome
|
||||||
|
if (explorer.indexOf('chrome') >= 0) {
|
||||||
|
const [version] = explorer.match(/chrome\/([\d.]+)/) || [''];
|
||||||
|
setInfo({ type: 'Chrome', version });
|
||||||
|
if (explorer.indexOf('qqbrowser') >= 0) {
|
||||||
|
const [version] = explorer.match(/qqbrowser\/([\d.]+)/) || [''];
|
||||||
|
setInfo({ type: 'QQ浏览器', version });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Opera
|
||||||
|
if (explorer.indexOf('opera') >= 0) {
|
||||||
|
const [version] = explorer.match(/opera.([\d.]+)/) || [''];
|
||||||
|
setInfo({ type: 'Opera', version });
|
||||||
|
}
|
||||||
|
// Safari
|
||||||
|
if (explorer.indexOf('Safari') >= 0) {
|
||||||
|
const [version] = explorer.match(/version\/([\d.]+)/) || [''];
|
||||||
|
setInfo({ type: 'Safari', version });
|
||||||
|
}
|
||||||
|
return info;
|
||||||
|
}
|
@ -1,3 +1,6 @@
|
|||||||
export * from './typeof';
|
export * from './typeof';
|
||||||
export * from './color';
|
export * from './color';
|
||||||
export * from './icon';
|
export * from './icon';
|
||||||
|
export * from './browser';
|
||||||
|
export * from './log';
|
||||||
|
export * from './number';
|
||||||
|
5
src/utils/common/log.ts
Normal file
5
src/utils/common/log.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
/** 打印log */
|
||||||
|
export function log(data: any) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log(data);
|
||||||
|
}
|
10
src/utils/common/number.ts
Normal file
10
src/utils/common/number.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
/**
|
||||||
|
* 获取指定整数范围内的随机整数
|
||||||
|
* @param start - 开始范围
|
||||||
|
* @param end - 结束范围
|
||||||
|
*/
|
||||||
|
export function getRandomInterger(end: number, start: number = 0) {
|
||||||
|
const range = end - start;
|
||||||
|
const random = Math.floor(Math.random() * range + start);
|
||||||
|
return random;
|
||||||
|
}
|
1
src/utils/form/index.ts
Normal file
1
src/utils/form/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './rule';
|
73
src/utils/form/rule.ts
Normal file
73
src/utils/form/rule.ts
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
import { Ref } from 'vue';
|
||||||
|
import type { FormItemRule } from 'naive-ui';
|
||||||
|
import { REGEXP_PHONE, REGEXP_PWD, REGEXP_CODE, REGEXP_EMAIL } from '@/config';
|
||||||
|
|
||||||
|
/** 表单规则 */
|
||||||
|
interface CustomFormRules {
|
||||||
|
/** 手机号码 */
|
||||||
|
phone: FormItemRule[];
|
||||||
|
/** 密码 */
|
||||||
|
pwd: FormItemRule[];
|
||||||
|
/** 验证码 */
|
||||||
|
code: FormItemRule[];
|
||||||
|
/** 邮箱 */
|
||||||
|
email: FormItemRule[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 表单规则 */
|
||||||
|
export const formRules: CustomFormRules = {
|
||||||
|
phone: [
|
||||||
|
{ required: true, message: '请输入手机号码' },
|
||||||
|
{ pattern: REGEXP_PHONE, message: '手机号码格式错误', trigger: 'input' }
|
||||||
|
],
|
||||||
|
pwd: [
|
||||||
|
{ required: true, message: '请输入密码' },
|
||||||
|
{ pattern: REGEXP_PWD, message: '密码为8-18位数字/字符/符号,至少2种组合', trigger: 'input' }
|
||||||
|
],
|
||||||
|
code: [
|
||||||
|
{ required: true, message: '请输入验证码' },
|
||||||
|
{ pattern: REGEXP_CODE, message: '验证码格式错误', trigger: 'input' }
|
||||||
|
],
|
||||||
|
email: [{ pattern: REGEXP_EMAIL, message: '邮箱格式错误', trigger: 'blur' }]
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 是否为空字符串 */
|
||||||
|
function isBlankString(str: string) {
|
||||||
|
return str.trim() === '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取确认密码的表单规则 */
|
||||||
|
export function getConfirmPwdRule(pwd: Ref<string>) {
|
||||||
|
const confirmPwdRule: FormItemRule[] = [
|
||||||
|
{ required: true, message: '请输入确认密码' },
|
||||||
|
{
|
||||||
|
validator: (rule, value) => {
|
||||||
|
if (!isBlankString(value) && value !== pwd.value) {
|
||||||
|
return Promise.reject(rule.message);
|
||||||
|
}
|
||||||
|
return Promise.resolve();
|
||||||
|
},
|
||||||
|
message: '输入的值与密码不一致',
|
||||||
|
trigger: 'input'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
return confirmPwdRule;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取图片验证码的表单规则 */
|
||||||
|
export function getImgCodeRule(imgCode: Ref<string>) {
|
||||||
|
const imgCodeRule: FormItemRule[] = [
|
||||||
|
{ required: true, message: '请输入验证码' },
|
||||||
|
{
|
||||||
|
validator: (rule, value) => {
|
||||||
|
if (!isBlankString(value) && value !== imgCode.value) {
|
||||||
|
return Promise.reject(rule.message);
|
||||||
|
}
|
||||||
|
return Promise.resolve();
|
||||||
|
},
|
||||||
|
message: '验证码不正确',
|
||||||
|
trigger: 'blur'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
return imgCodeRule;
|
||||||
|
}
|
@ -2,5 +2,6 @@ export * from './common';
|
|||||||
export * from './storage';
|
export * from './storage';
|
||||||
export * from './router';
|
export * from './router';
|
||||||
export * from './service';
|
export * from './service';
|
||||||
|
export * from './form';
|
||||||
export * from './package';
|
export * from './package';
|
||||||
export * from './auth';
|
export * from './auth';
|
||||||
|
@ -19,12 +19,11 @@ function hasErrorMsg(error: RequestServiceError) {
|
|||||||
* @param error
|
* @param error
|
||||||
*/
|
*/
|
||||||
export function showErrorMsg(error: RequestServiceError) {
|
export function showErrorMsg(error: RequestServiceError) {
|
||||||
|
if (!error.msg) return;
|
||||||
if (!NO_ERROR_MSG_CODE.includes(error.code)) {
|
if (!NO_ERROR_MSG_CODE.includes(error.code)) {
|
||||||
if (!hasErrorMsg(error)) {
|
if (!hasErrorMsg(error)) {
|
||||||
addErrorMsg(error);
|
addErrorMsg(error);
|
||||||
if (error.msg) {
|
|
||||||
window.$message?.error(error.msg, { duration: ERROR_MSG_DURATION });
|
window.$message?.error(error.msg, { duration: ERROR_MSG_DURATION });
|
||||||
}
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
removeErrorMsg(error);
|
removeErrorMsg(error);
|
||||||
}, ERROR_MSG_DURATION);
|
}, ERROR_MSG_DURATION);
|
||||||
|
@ -1,17 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex-center flex-col wh-full">
|
<exception-base type="403" />
|
||||||
<exception-svg type="403" :color="theme.themeColor" />
|
|
||||||
<router-link to="/">
|
|
||||||
<n-button type="primary">回到首页</n-button>
|
|
||||||
</router-link>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { NButton } from 'naive-ui';
|
import { ExceptionBase } from './components';
|
||||||
import { ExceptionSvg } from '@/components';
|
|
||||||
import { useThemeStore } from '@/store';
|
|
||||||
|
|
||||||
const theme = useThemeStore();
|
|
||||||
</script>
|
</script>
|
||||||
<style scoped></style>
|
<style scoped></style>
|
||||||
|
@ -1,17 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex-center flex-col wh-full">
|
<exception-base type="404" />
|
||||||
<exception-svg type="404" :color="theme.themeColor" />
|
|
||||||
<router-link to="/">
|
|
||||||
<n-button type="primary">回到首页</n-button>
|
|
||||||
</router-link>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { NButton } from 'naive-ui';
|
import { ExceptionBase } from './components';
|
||||||
import { ExceptionSvg } from '@/components';
|
|
||||||
import { useThemeStore } from '@/store';
|
|
||||||
|
|
||||||
const theme = useThemeStore();
|
|
||||||
</script>
|
</script>
|
||||||
<style scoped></style>
|
<style scoped></style>
|
||||||
|
@ -1,17 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex-center flex-col wh-full">
|
<exception-base type="500" />
|
||||||
<exception-svg type="500" :color="theme.themeColor" />
|
|
||||||
<router-link to="/">
|
|
||||||
<n-button type="primary">回到首页</n-button>
|
|
||||||
</router-link>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { NButton } from 'naive-ui';
|
import { ExceptionBase } from './components';
|
||||||
import { ExceptionSvg } from '@/components';
|
|
||||||
import { useThemeStore } from '@/store';
|
|
||||||
|
|
||||||
const theme = useThemeStore();
|
|
||||||
</script>
|
</script>
|
||||||
<style scoped></style>
|
<style scoped></style>
|
||||||
|
26
src/views/system/exception/components/ExceptionBase.vue
Normal file
26
src/views/system/exception/components/ExceptionBase.vue
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex-col-center wh-full">
|
||||||
|
<div class="w-400px h-400px text-primary">
|
||||||
|
<svg-no-permission v-if="type === '403'" />
|
||||||
|
<svg-not-found v-if="type === '404'" />
|
||||||
|
<svg-service-error v-if="type === '500'" />
|
||||||
|
</div>
|
||||||
|
<router-link :to="ROUTE_HOME.path">
|
||||||
|
<n-button type="primary">回到首页</n-button>
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { NButton } from 'naive-ui';
|
||||||
|
import { SvgNoPermission, SvgNotFound, SvgServiceError } from '@/components';
|
||||||
|
import { ROUTE_HOME } from '@/router';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
/** 异常类型 */
|
||||||
|
type: '403' | '404' | '500';
|
||||||
|
}
|
||||||
|
|
||||||
|
defineProps<Props>();
|
||||||
|
</script>
|
||||||
|
<style scoped></style>
|
3
src/views/system/exception/components/index.ts
Normal file
3
src/views/system/exception/components/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import ExceptionBase from './ExceptionBase.vue';
|
||||||
|
|
||||||
|
export { ExceptionBase };
|
@ -11,8 +11,17 @@
|
|||||||
<n-button size="large" :disabled="isCounting" @click="handleSmsCode">{{ label }}</n-button>
|
<n-button size="large" :disabled="isCounting" @click="handleSmsCode">{{ label }}</n-button>
|
||||||
</div>
|
</div>
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
|
<n-form-item path="imgCode">
|
||||||
|
<n-input v-model:value="model.imgCode" placeholder="验证码,点击图片刷新" />
|
||||||
|
<div class="pl-8px">
|
||||||
|
<image-verify v-model:code="imgCode" />
|
||||||
|
</div>
|
||||||
|
</n-form-item>
|
||||||
<n-space :vertical="true" size="large">
|
<n-space :vertical="true" size="large">
|
||||||
<n-button type="primary" size="large" :block="true" :round="true" @click="handleSubmit">确定</n-button>
|
<login-agreement v-model:value="agreement" />
|
||||||
|
<n-button type="primary" size="large" :block="true" :round="true" :loading="loading" @click="handleSubmit">
|
||||||
|
确定
|
||||||
|
</n-button>
|
||||||
<n-button size="large" :block="true" :round="true" @click="toCurrentLogin('pwd-login')">返回</n-button>
|
<n-button size="large" :block="true" :round="true" @click="toCurrentLogin('pwd-login')">返回</n-button>
|
||||||
</n-space>
|
</n-space>
|
||||||
</n-form>
|
</n-form>
|
||||||
@ -21,35 +30,34 @@
|
|||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { reactive, ref } from 'vue';
|
import { reactive, ref } from 'vue';
|
||||||
import { NForm, NFormItem, NInput, NSpace, NButton, useMessage } from 'naive-ui';
|
import { NForm, NFormItem, NInput, NSpace, NButton } from 'naive-ui';
|
||||||
import type { FormInst } from 'naive-ui';
|
import type { FormInst } from 'naive-ui';
|
||||||
import { useRouterPush } from '@/composables';
|
import { ImageVerify } from '@/components';
|
||||||
import { useSmsCode } from '@/hooks';
|
import { useRouterPush, useLogin } from '@/composables';
|
||||||
|
import { useSmsCode, useAgreement } from '@/hooks';
|
||||||
|
import { formRules, getImgCodeRule } from '@/utils';
|
||||||
|
import { LoginAgreement } from '../common';
|
||||||
|
|
||||||
const message = useMessage();
|
|
||||||
const { toCurrentLogin } = useRouterPush();
|
const { toCurrentLogin } = useRouterPush();
|
||||||
const { label, isCounting, start } = useSmsCode();
|
const { loading, login } = useLogin();
|
||||||
|
const { label, isCounting, getSmsCode } = useSmsCode();
|
||||||
|
const { agreement, isAgree } = useAgreement();
|
||||||
|
|
||||||
const formRef = ref<(HTMLElement & FormInst) | null>(null);
|
const formRef = ref<(HTMLElement & FormInst) | null>(null);
|
||||||
const model = reactive({
|
const model = reactive({
|
||||||
phone: '',
|
phone: '',
|
||||||
code: ''
|
code: '',
|
||||||
|
imgCode: ''
|
||||||
});
|
});
|
||||||
|
const imgCode = ref('');
|
||||||
const rules = {
|
const rules = {
|
||||||
phone: {
|
phone: formRules.phone,
|
||||||
required: true,
|
code: formRules.code,
|
||||||
trigger: ['blur', 'input'],
|
imgCode: getImgCodeRule(imgCode)
|
||||||
message: '请输入手机号'
|
|
||||||
},
|
|
||||||
code: {
|
|
||||||
required: true,
|
|
||||||
trigger: ['blur', 'input'],
|
|
||||||
message: '请输入验证码'
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
function handleSmsCode() {
|
function handleSmsCode() {
|
||||||
start();
|
getSmsCode(model.phone);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleSubmit(e: MouseEvent) {
|
function handleSubmit(e: MouseEvent) {
|
||||||
@ -58,9 +66,9 @@ function handleSubmit(e: MouseEvent) {
|
|||||||
|
|
||||||
formRef.value.validate(errors => {
|
formRef.value.validate(errors => {
|
||||||
if (!errors) {
|
if (!errors) {
|
||||||
message.success('验证成功');
|
if (!isAgree()) return;
|
||||||
} else {
|
const { phone, code } = model;
|
||||||
message.error('验证失败');
|
login({ phone, pwdOrCode: code, type: 'sms' });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -2,16 +2,23 @@
|
|||||||
<div class="pt-24px">
|
<div class="pt-24px">
|
||||||
<n-form ref="formRef" :model="model" :rules="rules" size="large" :show-label="false">
|
<n-form ref="formRef" :model="model" :rules="rules" size="large" :show-label="false">
|
||||||
<n-form-item path="phone">
|
<n-form-item path="phone">
|
||||||
<n-input v-model:value="model.phone" placeholder="手机号码" />
|
<n-input v-model:value="model.phone" placeholder="请输入手机号码" />
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
<n-form-item path="pwd">
|
<n-form-item path="pwd">
|
||||||
<n-input v-model:value="model.pwd" placeholder="密码" />
|
<n-input v-model:value="model.pwd" type="password" show-password-on="click" placeholder="请输入密码" />
|
||||||
|
</n-form-item>
|
||||||
|
<n-form-item path="imgCode">
|
||||||
|
<n-input v-model:value="model.imgCode" placeholder="验证码,点击图片刷新" />
|
||||||
|
<div class="pl-8px">
|
||||||
|
<image-verify v-model:code="imgCode" />
|
||||||
|
</div>
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
<n-space :vertical="true" size="large">
|
<n-space :vertical="true" size="large">
|
||||||
<div class="flex-y-center justify-between">
|
<div class="flex-y-center justify-between">
|
||||||
<n-checkbox v-model:checked="rememberMe">记住我</n-checkbox>
|
<n-checkbox v-model:checked="rememberMe">记住我</n-checkbox>
|
||||||
<span class="text-primary cursor-pointer" @click="toCurrentLogin('reset-pwd')">忘记密码?</span>
|
<span class="text-primary cursor-pointer" @click="toCurrentLogin('reset-pwd')">忘记密码?</span>
|
||||||
</div>
|
</div>
|
||||||
|
<login-agreement v-model:value="agreement" />
|
||||||
<n-button type="primary" size="large" :block="true" :round="true" :loading="loading" @click="handleSubmit">
|
<n-button type="primary" size="large" :block="true" :round="true" :loading="loading" @click="handleSubmit">
|
||||||
确定
|
确定
|
||||||
</n-button>
|
</n-button>
|
||||||
@ -32,37 +39,31 @@
|
|||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { reactive, ref } from 'vue';
|
import { reactive, ref } from 'vue';
|
||||||
import { NForm, NFormItem, NInput, NSpace, NCheckbox, NButton, useNotification } from 'naive-ui';
|
import { NForm, NFormItem, NInput, NSpace, NCheckbox, NButton } from 'naive-ui';
|
||||||
import type { FormInst, FormRules } from 'naive-ui';
|
import type { FormInst, FormRules } from 'naive-ui';
|
||||||
import { EnumLoginModule } from '@/enum';
|
import { EnumLoginModule } from '@/enum';
|
||||||
import { useAuthStore } from '@/store';
|
import { ImageVerify } from '@/components';
|
||||||
import { useRouterPush, useRouteQuery } from '@/composables';
|
import { useRouterPush, useLogin } from '@/composables';
|
||||||
import { useLoading } from '@/hooks';
|
import { useAgreement } from '@/hooks';
|
||||||
import { setToken } from '@/utils';
|
import { formRules, getImgCodeRule } from '@/utils';
|
||||||
import { OtherLogin } from './components';
|
import { OtherLogin } from './components';
|
||||||
|
import { LoginAgreement } from '../common';
|
||||||
|
|
||||||
const notification = useNotification();
|
const { toCurrentLogin } = useRouterPush();
|
||||||
const auth = useAuthStore();
|
const { loading, login } = useLogin();
|
||||||
const { routerPush, toHome, toCurrentLogin } = useRouterPush();
|
const { agreement, isAgree } = useAgreement();
|
||||||
const { loginRedirectUrl } = useRouteQuery();
|
|
||||||
const { loading, startLoading, endLoading } = useLoading();
|
|
||||||
|
|
||||||
const formRef = ref<(HTMLElement & FormInst) | null>(null);
|
const formRef = ref<(HTMLElement & FormInst) | null>(null);
|
||||||
const model = reactive({
|
const model = reactive({
|
||||||
phone: '151****3876',
|
phone: '15170283876',
|
||||||
pwd: '123456'
|
pwd: 'a123456789',
|
||||||
|
imgCode: ''
|
||||||
});
|
});
|
||||||
|
const imgCode = ref('');
|
||||||
const rules: FormRules = {
|
const rules: FormRules = {
|
||||||
phone: {
|
phone: formRules.phone,
|
||||||
required: true,
|
pwd: formRules.pwd,
|
||||||
trigger: ['blur', 'input'],
|
imgCode: getImgCodeRule(imgCode)
|
||||||
message: '请输入手机号'
|
|
||||||
},
|
|
||||||
pwd: {
|
|
||||||
required: true,
|
|
||||||
trigger: ['blur', 'input'],
|
|
||||||
message: '请输入密码'
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
const rememberMe = ref(false);
|
const rememberMe = ref(false);
|
||||||
|
|
||||||
@ -72,22 +73,9 @@ function handleSubmit(e: MouseEvent) {
|
|||||||
|
|
||||||
formRef.value.validate(errors => {
|
formRef.value.validate(errors => {
|
||||||
if (!errors) {
|
if (!errors) {
|
||||||
startLoading();
|
if (!isAgree()) return;
|
||||||
setTimeout(() => {
|
const { phone, pwd } = model;
|
||||||
endLoading();
|
login({ phone, pwdOrCode: pwd, type: 'pwd' });
|
||||||
setToken('temp-token');
|
|
||||||
if (loginRedirectUrl.value) {
|
|
||||||
routerPush(loginRedirectUrl.value);
|
|
||||||
} else {
|
|
||||||
toHome();
|
|
||||||
}
|
|
||||||
const { userName } = auth.userInfo;
|
|
||||||
notification.success({
|
|
||||||
title: '登录成功!',
|
|
||||||
content: `欢迎回来,${userName}!`,
|
|
||||||
duration: 3000
|
|
||||||
});
|
|
||||||
}, 1000);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
41
src/views/system/login/components/common/LoginAgreement.vue
Normal file
41
src/views/system/login/components/common/LoginAgreement.vue
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
<template>
|
||||||
|
<div class="w-full text-[14px]">
|
||||||
|
<n-checkbox v-model:checked="checked">我已经仔细阅读并接受</n-checkbox>
|
||||||
|
<n-button :text="true" type="primary">《用户协议》</n-button>
|
||||||
|
<n-button :text="true" type="primary">《隐私权政策》</n-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { watch } from 'vue';
|
||||||
|
import { NCheckbox, NButton } from 'naive-ui';
|
||||||
|
import { useBoolean } from '@/hooks';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
/** 是否选中 */
|
||||||
|
value: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Emits {
|
||||||
|
(e: 'update:value', value: boolean): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
value: true
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits<Emits>();
|
||||||
|
|
||||||
|
const { bool: checked, setBool } = useBoolean(props.value);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.value,
|
||||||
|
newValue => {
|
||||||
|
setBool(newValue);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
watch(checked, newValue => {
|
||||||
|
emit('update:value', newValue);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<style scoped></style>
|
3
src/views/system/login/components/common/index.ts
Normal file
3
src/views/system/login/components/common/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import LoginAgreement from './LoginAgreement.vue';
|
||||||
|
|
||||||
|
export { LoginAgreement };
|
@ -23,7 +23,7 @@ import { computed } from 'vue';
|
|||||||
import type { Component } from 'vue';
|
import type { Component } from 'vue';
|
||||||
import { NCard, NGradientText } from 'naive-ui';
|
import { NCard, NGradientText } from 'naive-ui';
|
||||||
import { SystemLogo, LoginBg } from '@/components';
|
import { SystemLogo, LoginBg } from '@/components';
|
||||||
import { useAppTitle } from '@/composables';
|
import { useAppInfo } from '@/composables';
|
||||||
import { EnumLoginModule } from '@/enum';
|
import { EnumLoginModule } from '@/enum';
|
||||||
import { mixColor } from '@/utils';
|
import { mixColor } from '@/utils';
|
||||||
import type { LoginModuleType } from '@/interface';
|
import type { LoginModuleType } from '@/interface';
|
||||||
@ -44,7 +44,7 @@ interface LoginModule {
|
|||||||
const props = defineProps<Props>();
|
const props = defineProps<Props>();
|
||||||
|
|
||||||
const theme = useThemeStore();
|
const theme = useThemeStore();
|
||||||
const title = useAppTitle();
|
const { title } = useAppInfo();
|
||||||
|
|
||||||
const modules: LoginModule[] = [
|
const modules: LoginModule[] = [
|
||||||
{ key: 'pwd-login', label: EnumLoginModule['pwd-login'], component: PwdLogin },
|
{ key: 'pwd-login', label: EnumLoginModule['pwd-login'], component: PwdLogin },
|
||||||
|
Loading…
Reference in New Issue
Block a user