mirror of
https://github.com/soybeanjs/soybean-admin.git
synced 2025-10-12 21:03:42 +08:00
Compare commits
16 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
114072277f | ||
|
e65034d946 | ||
|
a7a269d6a6 | ||
|
2c9660fdbf | ||
|
e93b94cb24 | ||
|
3befb22903 | ||
|
7ed5d0de2d | ||
|
47f2871cb5 | ||
|
852ddb64ad | ||
|
7e1f9f1138 | ||
|
554d7fd611 | ||
|
1797f29a79 | ||
|
50063187ec | ||
|
b16721b2b7 | ||
|
facc00e8b4 | ||
|
02c51e6fb9 |
@@ -12,3 +12,4 @@ lib
|
||||
.vscode
|
||||
.local
|
||||
!.env-config.ts
|
||||
package.json
|
||||
|
15
CHANGELOG.md
15
CHANGELOG.md
@@ -2,6 +2,21 @@
|
||||
|
||||
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
||||
|
||||
### [0.9.2](https://github.com/honghuangdc/soybean-admin/compare/v0.9.1...v0.9.2) (2022-02-11)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **projects:** 迁移全局搜索菜单功能 ([554d7fd](https://github.com/honghuangdc/soybean-admin/commit/554d7fd6114b9cf6df571c3cb02f4cb0cc6dcfd4))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **components:** 修复Tab在移动端设备无法点击的问题 ([2c9660f](https://github.com/honghuangdc/soybean-admin/commit/2c9660fdbf9a84e980db0aff5cd0aed0f75963ca))
|
||||
* **projects:** 修复分析页和工作台的布局问题 ([e93b94c](https://github.com/honghuangdc/soybean-admin/commit/e93b94cb2435a130bb1d94a703328af342cd24c9))
|
||||
* **projects:** 修复项目配置拷贝功能 ([a7a269d](https://github.com/honghuangdc/soybean-admin/commit/a7a269d6a61ccd25883e6bb69639d39e0260587d))
|
||||
* **projects:** vite配置修复 ([facc00e](https://github.com/honghuangdc/soybean-admin/commit/facc00e8b4998dc8bd338e3b63a652b4bfe2ed3e))
|
||||
|
||||
### [0.9.1](https://github.com/honghuangdc/soybean-admin/compare/v0.1.3...v0.9.1) (2022-01-23)
|
||||
|
||||
|
||||
|
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2021 Soybean
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
11
README.md
11
README.md
@@ -32,7 +32,16 @@ Soybean Admin 是一个基于 Vue3、Vite、TypeScript、Naive UI 的免费中
|
||||
|
||||
- [gitee](https://gitee.com/honghuangdc/soybean-admin)
|
||||
|
||||
### 温馨提示(老用户)
|
||||
|
||||
旧版代码在old分支,等main分支稳定下来,再删除old分支。
|
||||
|
||||
如果不是第一次进预览地址 soybean.pro,新版的发布会导致有缓存,退出用户,重新登录即可。
|
||||
|
||||
thin分支相对于main分支少了插件示例,其他都一样,后面会适当精简一些代码。
|
||||
|
||||
## 项目示例图
|
||||
|
||||

|
||||
|
||||

|
||||
@@ -109,7 +118,7 @@ pnpm i -g commitizen
|
||||
|
||||
- 微信交流群:
|
||||
<div style="text-align:left">
|
||||
<img src="https://s2.loli.net/2022/01/24/uX8KaGt7W2jbw6V.jpg" style="width:200px" />
|
||||
<img src="https://s2.loli.net/2022/02/07/hJWkOUAjpCQNwdf.jpg" style="width:200px" />
|
||||
</div>
|
||||
|
||||
- QQ 群 `711301266`
|
||||
|
40
package.json
40
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "soybean-admin",
|
||||
"version": "0.9.1",
|
||||
"version": "0.9.2",
|
||||
"scripts": {
|
||||
"dev": "cross-env VITE_HTTP_ENV=test vite",
|
||||
"dev:prod": "cross-env VITE_HTTP_ENV=prod vite",
|
||||
@@ -26,21 +26,22 @@
|
||||
"dependencies": {
|
||||
"@antv/g2plot": "^2.4.7",
|
||||
"@better-scroll/core": "^2.4.2",
|
||||
"@vueuse/core": "^7.5.4",
|
||||
"@vueuse/core": "^7.5.5",
|
||||
"axios": "^0.25.0",
|
||||
"clipboard": "^2.0.8",
|
||||
"clipboard": "^2.0.10",
|
||||
"colord": "^2.9.2",
|
||||
"crypto-js": "^4.1.1",
|
||||
"dayjs": "^1.10.7",
|
||||
"form-data": "^4.0.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"naive-ui": "^2.24.1",
|
||||
"pinia": "^2.0.9",
|
||||
"naive-ui": "^2.25.1",
|
||||
"pinia": "^2.0.11",
|
||||
"print-js": "^1.6.0",
|
||||
"qs": "^6.10.3",
|
||||
"swiper": "^7.4.1",
|
||||
"swiper": "^8.0.3",
|
||||
"ua-parser-js": "^1.0.2",
|
||||
"vditor": "^3.8.11",
|
||||
"vue": "^3.2.26",
|
||||
"vue": "^3.2.29",
|
||||
"vue-router": "^4.0.12",
|
||||
"wangeditor": "^4.7.11",
|
||||
"xgplayer": "^2.31.4"
|
||||
@@ -49,14 +50,15 @@
|
||||
"@amap/amap-jsapi-types": "^0.0.8",
|
||||
"@commitlint/cli": "^16.1.0",
|
||||
"@commitlint/config-conventional": "^16.0.0",
|
||||
"@iconify/json": "^1.1.459",
|
||||
"@iconify/vue": "^3.1.2",
|
||||
"@iconify/json": "^2.0.33",
|
||||
"@iconify/vue": "^3.1.3",
|
||||
"@types/bmapgl": "^0.0.5",
|
||||
"@types/crypto-js": "^4.1.0",
|
||||
"@types/node": "^17.0.10",
|
||||
"@types/node": "^17.0.15",
|
||||
"@types/qs": "^6.9.7",
|
||||
"@typescript-eslint/eslint-plugin": "^5.10.0",
|
||||
"@typescript-eslint/parser": "^5.10.0",
|
||||
"@types/ua-parser-js": "^0.7.36",
|
||||
"@typescript-eslint/eslint-plugin": "^5.10.2",
|
||||
"@typescript-eslint/parser": "^5.10.2",
|
||||
"@vitejs/plugin-vue": "^2.1.0",
|
||||
"@vue/eslint-config-prettier": "^7.0.0",
|
||||
"@vue/eslint-config-typescript": "^10.0.0",
|
||||
@@ -64,29 +66,29 @@
|
||||
"cross-env": "^7.0.3",
|
||||
"cz-conventional-changelog": "^3.3.0",
|
||||
"cz-customizable": "^6.3.0",
|
||||
"eslint": "^8.7.0",
|
||||
"eslint": "^8.8.0",
|
||||
"eslint-config-airbnb-base": "^15.0.0",
|
||||
"eslint-config-prettier": "^8.3.0",
|
||||
"eslint-plugin-import": "^2.25.4",
|
||||
"eslint-plugin-prettier": "^4.0.0",
|
||||
"eslint-plugin-vue": "^8.3.0",
|
||||
"eslint-plugin-vue": "^8.4.1",
|
||||
"husky": "^7.0.4",
|
||||
"lint-staged": "^12.3.1",
|
||||
"lint-staged": "^12.3.3",
|
||||
"mockjs": "^1.1.0",
|
||||
"patch-package": "^6.4.7",
|
||||
"postinstall-postinstall": "^2.1.0",
|
||||
"prettier": "^2.5.1",
|
||||
"rollup-plugin-visualizer": "^5.5.4",
|
||||
"sass": "^1.49.0",
|
||||
"sass": "^1.49.7",
|
||||
"typescript": "^4.5.5",
|
||||
"unplugin-icons": "^0.13.0",
|
||||
"unplugin-vue-components": "^0.17.14",
|
||||
"unplugin-vue-components": "^0.17.17",
|
||||
"vite": "^2.7.13",
|
||||
"vite-plugin-html": "^2.1.2",
|
||||
"vite-plugin-mock": "^2.9.6",
|
||||
"vite-plugin-windicss": "^1.6.3",
|
||||
"vue-tsc": "^0.31.1",
|
||||
"vueuc": "^0.4.23",
|
||||
"vue-tsc": "^0.31.2",
|
||||
"vueuc": "^0.4.25",
|
||||
"windicss": "^3.4.3"
|
||||
}
|
||||
}
|
||||
|
4223
pnpm-lock.yaml
generated
4223
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -30,8 +30,9 @@ function initBetterScroll() {
|
||||
}
|
||||
|
||||
// 滚动元素发生变化,刷新BS
|
||||
const { width: wrapWidth } = useElementSize(bsWrap);
|
||||
const { width, height } = useElementSize(bsContent);
|
||||
watch([() => width.value, () => height.value], () => {
|
||||
watch([() => wrapWidth.value, () => width.value, () => height.value], () => {
|
||||
if (instance.value) {
|
||||
instance.value.refresh();
|
||||
}
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { useBreakpoints, breakpointsTailwind } from '@vueuse/core';
|
||||
import UAParser from 'ua-parser-js';
|
||||
|
||||
interface AppInfo {
|
||||
/** 项目名称 */
|
||||
@@ -20,9 +20,9 @@ export function useAppInfo(): AppInfo {
|
||||
};
|
||||
}
|
||||
|
||||
/** 是否是移动端 */
|
||||
export function useIsMobile() {
|
||||
const breakpoints = useBreakpoints(breakpointsTailwind);
|
||||
const isMobile = breakpoints.smaller('lg');
|
||||
return isMobile;
|
||||
/** 获取设备信息 */
|
||||
export function useDeviceInfo() {
|
||||
const parser = new UAParser();
|
||||
const result = parser.getResult();
|
||||
return result;
|
||||
}
|
||||
|
@@ -9,6 +9,7 @@
|
||||
<header-menu />
|
||||
</div>
|
||||
<div class="flex justify-end h-full">
|
||||
<global-search />
|
||||
<github-site />
|
||||
<full-screen />
|
||||
<theme-mode />
|
||||
@@ -22,6 +23,7 @@ import { DarkModeContainer } from '@/components';
|
||||
import { useThemeStore } from '@/store';
|
||||
import type { GlobalHeaderProps } from '@/interface';
|
||||
import GlobalLogo from '../GlobalLogo/index.vue';
|
||||
import GlobalSearch from '../GlobalSearch/index.vue';
|
||||
import {
|
||||
MenuCollapse,
|
||||
GlobalBreadcrumb,
|
||||
|
24
src/layouts/common/GlobalSearch/components/SearchFooter.vue
Normal file
24
src/layouts/common/GlobalSearch/components/SearchFooter.vue
Normal file
@@ -0,0 +1,24 @@
|
||||
<template>
|
||||
<div class="px-24px h-44px flex-y-center">
|
||||
<span class="mr-14px">
|
||||
<icon-ant-design:enter-outlined class="icon text-20px p-2px mr-3px" />
|
||||
确认
|
||||
</span>
|
||||
<span class="mr-14px">
|
||||
<icon-mdi:arrow-up-thin class="icon text-20px p-2px mr-5px" />
|
||||
<icon-mdi:arrow-down-thin class="icon text-20px p-2px mr-3px" />
|
||||
切换
|
||||
</span>
|
||||
<span>
|
||||
<icon-mdi:close class="icon text-20px p-2px mr-3px" />
|
||||
关闭
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup></script>
|
||||
<style lang="scss" scoped>
|
||||
.icon {
|
||||
box-shadow: inset 0 -2px #cdcde6, inset 0 0 1px 1px #fff, 0 1px 2px 1px #1e235a66;
|
||||
}
|
||||
</style>
|
136
src/layouts/common/GlobalSearch/components/SearchModal.vue
Normal file
136
src/layouts/common/GlobalSearch/components/SearchModal.vue
Normal file
@@ -0,0 +1,136 @@
|
||||
<template>
|
||||
<n-modal
|
||||
v-model:show="show"
|
||||
:segmented="{ footer: 'soft' }"
|
||||
:closable="false"
|
||||
preset="card"
|
||||
footer-style="padding: 0; margin: 0"
|
||||
class="w-630px fixed top-50px left-1/2 transform -translate-x-1/2"
|
||||
@after-leave="handleClose"
|
||||
>
|
||||
<n-input ref="inputRef" v-model:value="keyword" clearable placeholder="请输入关键词搜索" @input="handleSearch">
|
||||
<template #prefix>
|
||||
<icon-uil:search class="text-15px text-[#c2c2c2]" />
|
||||
</template>
|
||||
</n-input>
|
||||
<div class="mt-20px">
|
||||
<n-empty v-if="resultOptions.length === 0" description="暂无搜索结果" />
|
||||
<search-result v-else v-model:value="activePath" :options="resultOptions" @enter="handleEnter" />
|
||||
</div>
|
||||
<template #footer>
|
||||
<search-footer />
|
||||
</template>
|
||||
</n-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, shallowRef, computed, watch, nextTick } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { NModal, NInput, NEmpty } from 'naive-ui';
|
||||
import { useDebounceFn, onKeyStroke } from '@vueuse/core';
|
||||
import { useRouteStore } from '@/store';
|
||||
import type { RouteList } from './types';
|
||||
import SearchResult from './SearchResult.vue';
|
||||
import SearchFooter from './SearchFooter.vue';
|
||||
|
||||
interface Props {
|
||||
/** 弹窗显隐 */
|
||||
value: boolean;
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:value', val: boolean): void;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {});
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
const router = useRouter();
|
||||
const routeStore = useRouteStore();
|
||||
const keyword = ref('');
|
||||
const activePath = ref('');
|
||||
const resultOptions = shallowRef<RouteList[]>([]);
|
||||
const inputRef = ref<HTMLInputElement | null>(null);
|
||||
const handleSearch = useDebounceFn(search, 300);
|
||||
|
||||
const show = computed({
|
||||
get() {
|
||||
return props.value;
|
||||
},
|
||||
set(val: boolean) {
|
||||
emit('update:value', val);
|
||||
}
|
||||
});
|
||||
|
||||
watch(show, async val => {
|
||||
if (val) {
|
||||
/** 自动聚焦 */
|
||||
await nextTick();
|
||||
inputRef.value?.focus();
|
||||
}
|
||||
});
|
||||
|
||||
/** 查询 */
|
||||
function search() {
|
||||
resultOptions.value = routeStore.menusList.filter(
|
||||
menu => keyword.value && menu.meta?.title.toLocaleLowerCase().includes(keyword.value.toLocaleLowerCase().trim())
|
||||
);
|
||||
if (resultOptions.value?.length > 0) {
|
||||
activePath.value = resultOptions.value[0].path;
|
||||
} else {
|
||||
activePath.value = '';
|
||||
}
|
||||
}
|
||||
|
||||
function handleClose() {
|
||||
show.value = false;
|
||||
/** 延时处理防止用户看到某些操作 */
|
||||
setTimeout(() => {
|
||||
resultOptions.value = [];
|
||||
keyword.value = '';
|
||||
}, 200);
|
||||
}
|
||||
|
||||
/** key up */
|
||||
function handleUp() {
|
||||
const { length } = resultOptions.value;
|
||||
if (length === 0) return;
|
||||
const index = resultOptions.value.findIndex(item => item.path === activePath.value);
|
||||
if (index === 0) {
|
||||
activePath.value = resultOptions.value[length - 1].path;
|
||||
} else {
|
||||
activePath.value = resultOptions.value[index - 1].path;
|
||||
}
|
||||
}
|
||||
|
||||
/** key down */
|
||||
function handleDown() {
|
||||
const { length } = resultOptions.value;
|
||||
if (length === 0) return;
|
||||
const index = resultOptions.value.findIndex(item => item.path === activePath.value);
|
||||
if (index + 1 === length) {
|
||||
activePath.value = resultOptions.value[0].path;
|
||||
} else {
|
||||
activePath.value = resultOptions.value[index + 1].path;
|
||||
}
|
||||
}
|
||||
|
||||
/** key enter */
|
||||
function handleEnter() {
|
||||
const { length } = resultOptions.value;
|
||||
if (length === 0 || activePath.value === '') return;
|
||||
const item = resultOptions.value.find(item => item.path === activePath.value);
|
||||
if (item?.meta?.href) {
|
||||
window.open(activePath.value, '__blank');
|
||||
} else {
|
||||
router.push(activePath.value);
|
||||
handleClose();
|
||||
}
|
||||
}
|
||||
|
||||
onKeyStroke('Escape', handleClose);
|
||||
onKeyStroke('Enter', handleEnter);
|
||||
onKeyStroke('ArrowUp', handleUp);
|
||||
onKeyStroke('ArrowDown', handleDown);
|
||||
</script>
|
||||
<style lang="scss" scoped></style>
|
62
src/layouts/common/GlobalSearch/components/SearchResult.vue
Normal file
62
src/layouts/common/GlobalSearch/components/SearchResult.vue
Normal file
@@ -0,0 +1,62 @@
|
||||
<template>
|
||||
<n-scrollbar>
|
||||
<div class="pb-12px">
|
||||
<template v-for="item in options" :key="item.path">
|
||||
<div
|
||||
class="bg-[#e5e7eb] dark:bg-dark h-56px mt-8px px-14px rounded-4px cursor-pointer flex-y-center justify-between"
|
||||
:style="{
|
||||
background: item.path === active ? theme.themeColor : '',
|
||||
color: item.path === active ? '#fff' : ''
|
||||
}"
|
||||
@click="handleTo"
|
||||
@mouseenter="handleMouse(item)"
|
||||
>
|
||||
<Icon :icon="item.meta?.icon ?? 'mdi:bookmark-minus-outline'" />
|
||||
<span class="flex-1 ml-5px">{{ item.meta?.title }}</span>
|
||||
<icon-ant-design:enter-outlined class="icon text-20px p-2px mr-3px" />
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</n-scrollbar>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
import { NScrollbar } from 'naive-ui';
|
||||
import { Icon } from '@iconify/vue';
|
||||
import { useThemeStore } from '@/store';
|
||||
import type { RouteList } from './types';
|
||||
|
||||
interface Props {
|
||||
value: string;
|
||||
options: RouteList[];
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:value', val: string): void;
|
||||
(e: 'enter'): void;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {});
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
const active = computed({
|
||||
get() {
|
||||
return props.value;
|
||||
},
|
||||
set(val: string) {
|
||||
emit('update:value', val);
|
||||
}
|
||||
});
|
||||
const theme = useThemeStore();
|
||||
|
||||
/** 鼠标移入 */
|
||||
async function handleMouse(item: RouteList) {
|
||||
active.value = item.path;
|
||||
}
|
||||
|
||||
function handleTo() {
|
||||
emit('enter');
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped></style>
|
3
src/layouts/common/GlobalSearch/components/index.ts
Normal file
3
src/layouts/common/GlobalSearch/components/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import SearchModal from './SearchModal.vue';
|
||||
|
||||
export { SearchModal };
|
1
src/layouts/common/GlobalSearch/components/types.ts
Normal file
1
src/layouts/common/GlobalSearch/components/types.ts
Normal file
@@ -0,0 +1 @@
|
||||
export type RouteList = AuthRoute.Route;
|
20
src/layouts/common/GlobalSearch/index.vue
Normal file
20
src/layouts/common/GlobalSearch/index.vue
Normal file
@@ -0,0 +1,20 @@
|
||||
<template>
|
||||
<div>
|
||||
<hover-container tooltip-content="搜索" class="w-40px h-full" @click="handleSearch">
|
||||
<icon-uil:search class="text-20px text-[#666]" />
|
||||
</hover-container>
|
||||
<search-modal v-model:value="show" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { HoverContainer } from '@/components';
|
||||
import { useBoolean } from '@/hooks';
|
||||
import { SearchModal } from './components';
|
||||
|
||||
const { bool: show, toggle } = useBoolean();
|
||||
function handleSearch() {
|
||||
toggle();
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped></style>
|
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<dark-mode-container class="global-tab flex-y-center w-full pl-16px" :style="{ height: theme.tab.height + 'px' }">
|
||||
<div ref="bsWrapper" class="flex-1-hidden h-full">
|
||||
<better-scroll ref="bsScroll" :options="{ scrollX: true, scrollY: false, click: isMobile }">
|
||||
<better-scroll ref="bsScroll" :options="{ scrollX: true, scrollY: false, click: canClick }">
|
||||
<tab-detail @scroll="handleScroll" />
|
||||
</better-scroll>
|
||||
</div>
|
||||
@@ -15,20 +15,21 @@ import { useRoute } from 'vue-router';
|
||||
import { useElementBounding } from '@vueuse/core';
|
||||
import { DarkModeContainer, BetterScroll } from '@/components';
|
||||
import { useThemeStore, useTabStore } from '@/store';
|
||||
import { useIsMobile } from '@/composables';
|
||||
import { useDeviceInfo } from '@/composables';
|
||||
import type { ExposeBetterScroll } from '@/interface';
|
||||
import { TabDetail, ReloadButton } from './components';
|
||||
|
||||
const route = useRoute();
|
||||
const theme = useThemeStore();
|
||||
const tab = useTabStore();
|
||||
const deviceInfo = useDeviceInfo();
|
||||
|
||||
const bsWrapper = ref<HTMLElement>();
|
||||
const { width: bsWrapperWidth, left: bsWrapperLeft } = useElementBounding(bsWrapper);
|
||||
|
||||
const bsScroll = ref<ExposeBetterScroll>();
|
||||
|
||||
const isMobile = useIsMobile();
|
||||
const canClick = Boolean(deviceInfo.device.type);
|
||||
|
||||
function handleScroll(clientX: number) {
|
||||
const currentX = clientX - bsWrapperLeft.value;
|
||||
|
@@ -1,7 +1,8 @@
|
||||
<template>
|
||||
<n-divider title-placement="center">主题配置</n-divider>
|
||||
<textarea id="themeConfigCopyTarget" v-model="dataClipboardText" class="absolute opacity-0" />
|
||||
<n-space vertical>
|
||||
<div ref="copyRef" :data-clipboard-text="dataClipboardText">
|
||||
<div ref="copyRef" data-clipboard-target="#themeConfigCopyTarget">
|
||||
<n-button type="primary" :block="true">拷贝当前配置</n-button>
|
||||
</div>
|
||||
<n-button type="warning" :block="true" @click="handleResetConfig">重置当前配置</n-button>
|
||||
@@ -17,6 +18,7 @@ import { useThemeStore } from '@/store';
|
||||
const theme = useThemeStore();
|
||||
|
||||
const copyRef = ref<HTMLElement>();
|
||||
|
||||
const dataClipboardText = ref(getClipboardText());
|
||||
|
||||
function getClipboardText() {
|
||||
@@ -29,8 +31,7 @@ function handleResetConfig() {
|
||||
}
|
||||
|
||||
function clipboardEventListener() {
|
||||
if (!copyRef.value) return;
|
||||
const copy = new Clipboard(copyRef.value);
|
||||
const copy = new Clipboard(copyRef.value!);
|
||||
copy.on('success', () => {
|
||||
window.$dialog?.success({
|
||||
title: '操作成功',
|
||||
|
@@ -1,7 +1,9 @@
|
||||
import { createRequest } from './request';
|
||||
import { serviceEnv } from '~/.env-config';
|
||||
|
||||
const { url } = serviceEnv[import.meta.env.VITE_HTTP_ENV];
|
||||
const env = import.meta.env.VITE_HTTP_ENV || 'test';
|
||||
|
||||
const { url } = serviceEnv[env];
|
||||
|
||||
export const request = createRequest({ baseURL: url });
|
||||
|
||||
|
@@ -0,0 +1,142 @@
|
||||
{
|
||||
"darkMode": false,
|
||||
"layout": {
|
||||
"minWidth": 900,
|
||||
"mode": "vertical",
|
||||
"modeList": [
|
||||
{
|
||||
"value": "vertical",
|
||||
"label": "左侧菜单模式"
|
||||
},
|
||||
{
|
||||
"value": "vertical-mix",
|
||||
"label": "左侧菜单混合模式"
|
||||
},
|
||||
{
|
||||
"value": "horizontal",
|
||||
"label": "顶部菜单模式"
|
||||
},
|
||||
{
|
||||
"value": "horizontal-mix",
|
||||
"label": "顶部菜单混合模式"
|
||||
}
|
||||
]
|
||||
},
|
||||
"themeColor": "#1890ff",
|
||||
"themeColorList": [
|
||||
"#1890ff",
|
||||
"#409EFF",
|
||||
"#2d8cf0",
|
||||
"#007AFF",
|
||||
"#5ac8fa",
|
||||
"#5856D6",
|
||||
"#536dfe",
|
||||
"#9c27b0",
|
||||
"#AF52DE",
|
||||
"#0096c7",
|
||||
"#00C1D4",
|
||||
"#34C759",
|
||||
"#43a047",
|
||||
"#7cb342",
|
||||
"#c0ca33",
|
||||
"#78DEC7",
|
||||
"#e53935",
|
||||
"#d81b60",
|
||||
"#f4511e",
|
||||
"#fb8c00",
|
||||
"#ffb300",
|
||||
"#fdd835",
|
||||
"#6d4c41",
|
||||
"#546e7a"
|
||||
],
|
||||
"otherColor": {
|
||||
"info": "#0099ad",
|
||||
"success": "#52c41a",
|
||||
"warning": "#faad14",
|
||||
"error": "#f5222d"
|
||||
},
|
||||
"isCustomizeInfoColor": false,
|
||||
"fixedHeaderAndTab": true,
|
||||
"showReload": true,
|
||||
"header": {
|
||||
"height": 56,
|
||||
"crumb": {
|
||||
"visible": true,
|
||||
"showIcon": true
|
||||
}
|
||||
},
|
||||
"tab": {
|
||||
"visible": true,
|
||||
"height": 44,
|
||||
"mode": "chrome",
|
||||
"modeList": [
|
||||
{
|
||||
"value": "chrome",
|
||||
"label": "谷歌风格"
|
||||
},
|
||||
{
|
||||
"value": "button",
|
||||
"label": "按钮风格"
|
||||
}
|
||||
],
|
||||
"isCache": true
|
||||
},
|
||||
"sider": {
|
||||
"width": 220,
|
||||
"collapsedWidth": 64,
|
||||
"mixWidth": 80,
|
||||
"mixCollapsedWidth": 48,
|
||||
"mixChildMenuWidth": 200
|
||||
},
|
||||
"menu": {
|
||||
"horizontalPosition": "flex-start",
|
||||
"horizontalPositionList": [
|
||||
{
|
||||
"value": "flex-start",
|
||||
"label": "居左"
|
||||
},
|
||||
{
|
||||
"value": "center",
|
||||
"label": "居中"
|
||||
},
|
||||
{
|
||||
"value": "flex-end",
|
||||
"label": "居右"
|
||||
}
|
||||
]
|
||||
},
|
||||
"footer": {
|
||||
"fixed": false,
|
||||
"height": 48
|
||||
},
|
||||
"page": {
|
||||
"animate": true,
|
||||
"animateMode": "fade-slide",
|
||||
"animateModeList": [
|
||||
{
|
||||
"value": "fade-slide",
|
||||
"label": "滑动"
|
||||
},
|
||||
{
|
||||
"value": "fade",
|
||||
"label": "消退"
|
||||
},
|
||||
{
|
||||
"value": "fade-bottom",
|
||||
"label": "底部消退"
|
||||
},
|
||||
{
|
||||
"value": "fade-scale",
|
||||
"label": "缩放消退"
|
||||
},
|
||||
{
|
||||
"value": "zoom-fade",
|
||||
"label": "渐变"
|
||||
},
|
||||
{
|
||||
"value": "zoom-out",
|
||||
"label": "闪现"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import { EnumThemeLayoutMode, EnumThemeTabMode, EnumThemeHorizontalMenuPosition, EnumThemeAnimateMode } from '@/enum';
|
||||
import type { ThemeSetting } from '@/interface';
|
||||
import jsonSetting from './theme.json';
|
||||
|
||||
const themeColorList = [
|
||||
'#1890ff',
|
||||
@@ -101,4 +102,4 @@ const defaultThemeSetting: ThemeSetting = {
|
||||
}
|
||||
};
|
||||
|
||||
export const themeSetting = defaultThemeSetting;
|
||||
export const themeSetting = (jsonSetting as ThemeSetting) || defaultThemeSetting;
|
||||
|
@@ -1,7 +1,13 @@
|
||||
import type { Router } from 'vue-router';
|
||||
import { defineStore } from 'pinia';
|
||||
import { fetchUserRoutes } from '@/service';
|
||||
import { getUserInfo, transformAuthRouteToMenu, transformAuthRoutesToVueRoutes, getCacheRoutes } from '@/utils';
|
||||
import {
|
||||
getUserInfo,
|
||||
transformAuthRouteToMenu,
|
||||
transformAuthRoutesToVueRoutes,
|
||||
transformRouteToList,
|
||||
getCacheRoutes
|
||||
} from '@/utils';
|
||||
import type { GlobalMenuOption } from '@/interface';
|
||||
import { useTabStore } from '../tab';
|
||||
|
||||
@@ -12,6 +18,7 @@ interface RouteState {
|
||||
routeHomeName: AuthRoute.RouteKey;
|
||||
/** 菜单 */
|
||||
menus: GlobalMenuOption[];
|
||||
menusList: AuthRoute.Route[];
|
||||
/** 缓存的路由名称 */
|
||||
cacheRoutes: string[];
|
||||
}
|
||||
@@ -21,6 +28,7 @@ export const useRouteStore = defineStore('route-store', {
|
||||
isAddedDynamicRoute: false,
|
||||
routeHomeName: 'dashboard_analysis',
|
||||
menus: [],
|
||||
menusList: [],
|
||||
cacheRoutes: []
|
||||
}),
|
||||
actions: {
|
||||
@@ -37,6 +45,7 @@ export const useRouteStore = defineStore('route-store', {
|
||||
if (data) {
|
||||
this.routeHomeName = data.home;
|
||||
this.menus = transformAuthRouteToMenu(data.routes);
|
||||
this.menusList = transformRouteToList(data.routes);
|
||||
|
||||
const vueRoutes = transformAuthRoutesToVueRoutes(data.routes);
|
||||
vueRoutes.forEach(route => {
|
||||
|
2
src/typings/common/route.d.ts
vendored
2
src/typings/common/route.d.ts
vendored
@@ -125,7 +125,7 @@ declare namespace AuthRoute {
|
||||
type SingleRouteParentPath = KeyToPath<SingleRouteParentKey>;
|
||||
|
||||
/** 路由key转换路由path */
|
||||
type KeyToPath<Key extends RouteKey> = Key extends `${infer Left}_${infer Right}`
|
||||
type KeyToPath<Key extends string> = Key extends `${infer Left}_${infer Right}`
|
||||
? KeyToPath<`${Left}/${Right}`>
|
||||
: `/${Key}`;
|
||||
|
||||
|
@@ -1,32 +1,7 @@
|
||||
import type { Component } from 'vue';
|
||||
import { EnumLayoutComponentName } from '@/enum';
|
||||
import { BasicLayout, BlankLayout } from '@/layouts';
|
||||
import {
|
||||
Login,
|
||||
NoPermission,
|
||||
NotFound,
|
||||
ServiceError,
|
||||
DashboardAnalysis,
|
||||
DashboardWorkbench,
|
||||
DocumentVue,
|
||||
DocumentVueNew,
|
||||
DocumentVite,
|
||||
DocumentNaive,
|
||||
ComponentButton,
|
||||
ComponentCard,
|
||||
ComponentTable,
|
||||
PluginMap,
|
||||
PluginVideo,
|
||||
PluginEditorQuill,
|
||||
PluginEditorMarkdown,
|
||||
PluginSwiper,
|
||||
PluginCopy,
|
||||
PluginIcon,
|
||||
PluginPrint,
|
||||
MultiMenuFirstSecond,
|
||||
MultiMenuFirstSecondNewThird,
|
||||
About
|
||||
} from '@/views';
|
||||
import { views } from '@/views';
|
||||
import type { LayoutComponentName } from '@/interface';
|
||||
|
||||
type LayoutComponent = Record<LayoutComponentName, () => Promise<Component>>;
|
||||
@@ -43,94 +18,12 @@ export function getLayoutComponent(layoutType: LayoutComponentName) {
|
||||
return () => setViewComponentName(layoutComponent[layoutType], EnumLayoutComponentName[layoutType]);
|
||||
}
|
||||
|
||||
/** 需要用到自身vue组件的页面 */
|
||||
type ViewComponentKey = Exclude<
|
||||
AuthRoute.RouteKey,
|
||||
| 'root'
|
||||
| 'dashboard'
|
||||
| 'document'
|
||||
| 'document_project'
|
||||
| 'component'
|
||||
| 'plugin'
|
||||
| 'plugin_editor'
|
||||
| 'multi-menu'
|
||||
| 'multi-menu_first'
|
||||
| 'multi-menu_first_second-new'
|
||||
| 'exception'
|
||||
>;
|
||||
|
||||
type ViewComponent = Record<ViewComponentKey, () => Promise<Component>>;
|
||||
|
||||
/**
|
||||
* 获取页面导入的vue文件(懒加载的方式)
|
||||
* @param routeKey - 路由key
|
||||
*/
|
||||
export function getViewComponent(routeKey: AuthRoute.RouteKey) {
|
||||
const keys: ViewComponentKey[] = [
|
||||
'login',
|
||||
'no-permission',
|
||||
'not-found',
|
||||
'service-error',
|
||||
'dashboard_analysis',
|
||||
'dashboard_workbench',
|
||||
'document_vue',
|
||||
'document_vue-new',
|
||||
'document_vite',
|
||||
'document_naive',
|
||||
'component_button',
|
||||
'component_card',
|
||||
'component_table',
|
||||
'plugin_map',
|
||||
'plugin_video',
|
||||
'plugin_editor_quill',
|
||||
'plugin_editor_markdown',
|
||||
'plugin_copy',
|
||||
'plugin_icon',
|
||||
'plugin_print',
|
||||
'plugin_swiper',
|
||||
'exception_403',
|
||||
'exception_404',
|
||||
'exception_500',
|
||||
'multi-menu_first_second',
|
||||
'multi-menu_first_second-new_third',
|
||||
'about',
|
||||
'not-found-page'
|
||||
];
|
||||
|
||||
const key = keys.includes(routeKey as ViewComponentKey) ? (routeKey as ViewComponentKey) : 'not-found';
|
||||
|
||||
const viewComponent: ViewComponent = {
|
||||
login: Login,
|
||||
'no-permission': NoPermission,
|
||||
'not-found': NotFound,
|
||||
'service-error': ServiceError,
|
||||
dashboard_analysis: DashboardAnalysis,
|
||||
dashboard_workbench: DashboardWorkbench,
|
||||
document_vue: DocumentVue,
|
||||
'document_vue-new': DocumentVueNew,
|
||||
document_vite: DocumentVite,
|
||||
document_naive: DocumentNaive,
|
||||
component_button: ComponentButton,
|
||||
component_card: ComponentCard,
|
||||
component_table: ComponentTable,
|
||||
plugin_map: PluginMap,
|
||||
plugin_video: PluginVideo,
|
||||
plugin_editor_quill: PluginEditorQuill,
|
||||
plugin_editor_markdown: PluginEditorMarkdown,
|
||||
plugin_copy: PluginCopy,
|
||||
plugin_icon: PluginIcon,
|
||||
plugin_print: PluginPrint,
|
||||
plugin_swiper: PluginSwiper,
|
||||
exception_403: NoPermission,
|
||||
exception_404: NotFound,
|
||||
exception_500: ServiceError,
|
||||
'multi-menu_first_second': MultiMenuFirstSecond,
|
||||
'multi-menu_first_second-new_third': MultiMenuFirstSecondNewThird,
|
||||
about: About,
|
||||
'not-found-page': NotFound
|
||||
};
|
||||
|
||||
return () => setViewComponentName(viewComponent[key], key) as Promise<Component>;
|
||||
return () => setViewComponentName(views[routeKey], routeKey) as Promise<Component>;
|
||||
}
|
||||
|
||||
/** 给页面组件设置名称 */
|
||||
|
@@ -13,6 +13,20 @@ export function transformAuthRoutesToVueRoutes(routes: AuthRoute.Route[]) {
|
||||
return routes.map(route => transformAuthRouteToVueRoute(route)).flat(1);
|
||||
}
|
||||
|
||||
/** 将路由转换成菜单列表 */
|
||||
export function transformRouteToList(routes: AuthRoute.Route[], treeMap: AuthRoute.Route[] = []) {
|
||||
if (routes && routes.length === 0) return [];
|
||||
return routes.reduce((acc, cur) => {
|
||||
if (!cur.meta?.hide) {
|
||||
acc.push(cur);
|
||||
}
|
||||
if (cur.children && cur.children.length > 0) {
|
||||
transformRouteToList(cur.children, treeMap);
|
||||
}
|
||||
return acc;
|
||||
}, treeMap);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将单个权限路由转换成vue路由
|
||||
* @param route - 权限路由
|
||||
|
@@ -1,3 +0,0 @@
|
||||
const About = () => import('./index.vue');
|
||||
|
||||
export { About };
|
@@ -1,5 +0,0 @@
|
||||
const ComponentButton = () => import('./button/index.vue');
|
||||
const ComponentCard = () => import('./card/index.vue');
|
||||
const ComponentTable = () => import('./table/index.vue');
|
||||
|
||||
export { ComponentButton, ComponentCard, ComponentTable };
|
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<n-grid :x-gap="16" :y-gap="16" :item-responsive="true" responsive="screen">
|
||||
<n-grid-item span="s:24 m:8">
|
||||
<n-grid :x-gap="16" :y-gap="16" :item-responsive="true">
|
||||
<n-grid-item span="0:24 640:24 1024:8">
|
||||
<n-card title="时间线" :bordered="false" class="rounded-16px shadow-sm">
|
||||
<div class="h-360px">
|
||||
<n-timeline>
|
||||
@@ -9,7 +9,7 @@
|
||||
</div>
|
||||
</n-card>
|
||||
</n-grid-item>
|
||||
<n-grid-item span="s:24 m:16">
|
||||
<n-grid-item span="0:24 640:24 1024:16">
|
||||
<n-card title="表格" :bordered="false" class="rounded-16px shadow-sm">
|
||||
<div class="h-360px">
|
||||
<n-data-table size="small" :columns="columns" :data="tableData" />
|
||||
|
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<n-grid :x-gap="16" :y-gap="16" :item-responsive="true" responsive="screen">
|
||||
<n-grid-item span="s:24 m:16">
|
||||
<n-grid :x-gap="16" :y-gap="16" :item-responsive="true">
|
||||
<n-grid-item span="0:24 640:24 1024:16">
|
||||
<n-card :bordered="false" class="rounded-16px shadow-sm">
|
||||
<div class="flex w-full h-360px">
|
||||
<div class="w-200px h-full py-12px">
|
||||
@@ -22,7 +22,7 @@
|
||||
</div>
|
||||
</n-card>
|
||||
</n-grid-item>
|
||||
<n-grid-item span="s:24 m:8">
|
||||
<n-grid-item span="0:24 640:24 1024:8">
|
||||
<n-card :bordered="false" class="rounded-16px shadow-sm">
|
||||
<div ref="pieRef" class="w-full h-360px"></div>
|
||||
</n-card>
|
||||
|
@@ -1,4 +0,0 @@
|
||||
const DashboardAnalysis = () => import('./analysis/index.vue');
|
||||
const DashboardWorkbench = () => import('./workbench/index.vue');
|
||||
|
||||
export { DashboardAnalysis, DashboardWorkbench };
|
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<n-grid :item-responsive="true" responsive="screen" :x-gap="16" :y-gap="16">
|
||||
<n-grid-item span="s:24 m:16">
|
||||
<n-grid :item-responsive="true" :x-gap="16" :y-gap="16">
|
||||
<n-grid-item span="0:24 640:24 1024:16">
|
||||
<n-space :vertical="true" :size="16">
|
||||
<n-card title="项目主要技术栈" :bordered="false" size="small" class="shadow-sm rounded-16px">
|
||||
<template #header-extra>
|
||||
@@ -29,7 +29,7 @@
|
||||
</n-card>
|
||||
</n-space>
|
||||
</n-grid-item>
|
||||
<n-grid-item span="s:24 m:8">
|
||||
<n-grid-item span="0:24 640:24 1024:8">
|
||||
<n-space :vertical="true" :size="16">
|
||||
<n-card title="快捷操作" :bordered="false" size="small" class="shadow-sm rounded-16px">
|
||||
<n-grid :item-responsive="true" responsive="screen" cols="m:2 l:3" :x-gap="8" :y-gap="8">
|
||||
|
@@ -1,6 +0,0 @@
|
||||
const DocumentVue = () => import('./vue/index.vue');
|
||||
const DocumentVueNew = () => import('./vue-new/index.vue');
|
||||
const DocumentVite = () => import('./vite/index.vue');
|
||||
const DocumentNaive = () => import('./naive/index.vue');
|
||||
|
||||
export { DocumentVue, DocumentVueNew, DocumentVite, DocumentNaive };
|
8
src/views/exception/403/index.vue
Normal file
8
src/views/exception/403/index.vue
Normal file
@@ -0,0 +1,8 @@
|
||||
<template>
|
||||
<exception-base type="403" />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ExceptionBase } from '../../system-view/components';
|
||||
</script>
|
||||
<style scoped></style>
|
8
src/views/exception/404/index.vue
Normal file
8
src/views/exception/404/index.vue
Normal file
@@ -0,0 +1,8 @@
|
||||
<template>
|
||||
<exception-base type="404" />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ExceptionBase } from '../../system-view/components';
|
||||
</script>
|
||||
<style scoped></style>
|
8
src/views/exception/500/index.vue
Normal file
8
src/views/exception/500/index.vue
Normal file
@@ -0,0 +1,8 @@
|
||||
<template>
|
||||
<exception-base type="500" />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ExceptionBase } from '../../system-view/components';
|
||||
</script>
|
||||
<style scoped></style>
|
@@ -1,7 +1,31 @@
|
||||
export * from './system';
|
||||
export * from './dashboard';
|
||||
export * from './document';
|
||||
export * from './component';
|
||||
export * from './plugin';
|
||||
export * from './multi-menu';
|
||||
export * from './about';
|
||||
import type { Component } from 'vue';
|
||||
|
||||
type ViewComponent = Record<string, () => Promise<Component>>;
|
||||
|
||||
const importViews = import.meta.glob('./**/index.vue');
|
||||
|
||||
const COMPONENTS_KEY = 'components';
|
||||
const PREFIX = './';
|
||||
const SUFFIX = '/index.vue';
|
||||
const PATH_SPLIT_MARK = '/';
|
||||
const ROUTE_KEY_SPLIT_MARK = '_';
|
||||
/** 系统的内置路由,该文件夹名称不作为RouteKey */
|
||||
const SYSTEM_VIEW = 'system-view_';
|
||||
|
||||
/** 过滤掉组件文件 */
|
||||
const viewKeys = Object.keys(importViews).filter(key => !key.includes(COMPONENTS_KEY));
|
||||
|
||||
function getViewComponent() {
|
||||
const components: ViewComponent = {};
|
||||
viewKeys.forEach(key => {
|
||||
const routeKey = key
|
||||
.replace(PREFIX, '')
|
||||
.replace(SUFFIX, '')
|
||||
.replaceAll(PATH_SPLIT_MARK, ROUTE_KEY_SPLIT_MARK)
|
||||
.replace(SYSTEM_VIEW, '');
|
||||
components[routeKey] = importViews[key];
|
||||
});
|
||||
return components;
|
||||
}
|
||||
|
||||
export const views = getViewComponent();
|
||||
|
@@ -1,4 +0,0 @@
|
||||
const MultiMenuFirstSecond = () => import('./first/second/index.vue');
|
||||
const MultiMenuFirstSecondNewThird = () => import('./first/second-new/third/index.vue');
|
||||
|
||||
export { MultiMenuFirstSecond, MultiMenuFirstSecondNewThird };
|
@@ -1,19 +0,0 @@
|
||||
const PluginMap = () => import('./map/index.vue');
|
||||
const PluginVideo = () => import('./video/index.vue');
|
||||
const PluginEditorQuill = () => import('./editor/quill/index.vue');
|
||||
const PluginEditorMarkdown = () => import('./editor/markdown/index.vue');
|
||||
const PluginSwiper = () => import('./swiper/index.vue');
|
||||
const PluginCopy = () => import('./copy/index.vue');
|
||||
const PluginIcon = () => import('./icon/index.vue');
|
||||
const PluginPrint = () => import('./print/index.vue');
|
||||
|
||||
export {
|
||||
PluginMap,
|
||||
PluginVideo,
|
||||
PluginEditorQuill,
|
||||
PluginEditorMarkdown,
|
||||
PluginSwiper,
|
||||
PluginCopy,
|
||||
PluginIcon,
|
||||
PluginPrint
|
||||
};
|
8
src/views/system-view/not-found/index.vue
Normal file
8
src/views/system-view/not-found/index.vue
Normal file
@@ -0,0 +1,8 @@
|
||||
<template>
|
||||
<exception-base type="404" />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ExceptionBase } from '../components';
|
||||
</script>
|
||||
<style scoped></style>
|
@@ -1,6 +0,0 @@
|
||||
const Login = () => import('./login/index.vue');
|
||||
const NoPermission = () => import('./exception/no-permission/index.vue');
|
||||
const NotFound = () => import('./exception/not-found/index.vue');
|
||||
const ServiceError = () => import('./exception/service-error/index.vue');
|
||||
|
||||
export { Login, NoPermission, NotFound, ServiceError };
|
@@ -18,7 +18,7 @@ export default defineConfig(configEnv => {
|
||||
css: {
|
||||
preprocessorOptions: {
|
||||
scss: {
|
||||
additionalData: `@use "${fileURLToPath(new URL('./src', import.meta.url))}/styles/scss/global.scss" as *;`
|
||||
additionalData: `@use "./src/styles/scss/global.scss" as *;`
|
||||
}
|
||||
}
|
||||
},
|
||||
|
Reference in New Issue
Block a user