mirror of
https://github.com/soybeanjs/soybean-admin.git
synced 2025-10-12 21:03:42 +08:00
Compare commits
18 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
114072277f | ||
|
e65034d946 | ||
|
a7a269d6a6 | ||
|
2c9660fdbf | ||
|
e93b94cb24 | ||
|
3befb22903 | ||
|
7ed5d0de2d | ||
|
47f2871cb5 | ||
|
852ddb64ad | ||
|
7e1f9f1138 | ||
|
554d7fd611 | ||
|
1797f29a79 | ||
|
50063187ec | ||
|
b16721b2b7 | ||
|
facc00e8b4 | ||
|
02c51e6fb9 | ||
|
be374089ba | ||
|
68b42304d5 |
@@ -12,3 +12,4 @@ lib
|
||||
.vscode
|
||||
.local
|
||||
!.env-config.ts
|
||||
package.json
|
||||
|
22
CHANGELOG.md
22
CHANGELOG.md
@@ -2,6 +2,28 @@
|
||||
|
||||
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)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **projects:** 新版重构完成 ([68b4230](https://github.com/honghuangdc/soybean-admin/commit/68b42304d5964246775c7a82dcc1406c5db7a4e4))
|
||||
|
||||
### [0.1.3](https://github.com/honghuangdc/soybean-admin/compare/v0.1.2...v0.1.3) (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.
|
30
README.md
30
README.md
@@ -1,21 +1,22 @@
|
||||
<div align="center">
|
||||
<img src="https://i.loli.net/2021/11/24/x5lLfuSnEawBAgi.png"/>
|
||||
<h1>Soybean Admin Thin</h1>
|
||||
<h1>Soybean Admin</h1>
|
||||
</div>
|
||||
|
||||
[](./LICENSE)
|
||||
|
||||
## 简介
|
||||
|
||||
Soybean Admin Thin 是Soybean Admin的精简版。
|
||||
Soybean Admin 是一个基于 Vue3、Vite、TypeScript、Naive UI 的免费中后台模版,它使用了最新的前端技术栈,内置丰富的主题配置,有着极高的代码规范,基于mock实现的动态权限路由,开箱即用的中后台前端解决方案,也可用于学习参考。
|
||||
|
||||
## 特性
|
||||
|
||||
- **最新技术栈**:使用 Vue3/vite2 等前端前沿技术开发, 使用高效率的npm包管理器pnpm
|
||||
- **TypeScript**: 应用程序级 JavaScript 的语言
|
||||
- **主题**:丰富可配置的主题
|
||||
- **主题**:丰富可配置的主题、暗黑模式,基于windicss的动态主题颜色
|
||||
- **代码规范**:丰富的规范插件及极高的代码规范
|
||||
- **路由配置**:简易的路由配置
|
||||
- **权限路由**:简易的路由配置、基于mock的动态路由能快速实现后端动态路由
|
||||
- **请求函数**:完善的请求函数封装,提供Promise和hooks两种请求函数
|
||||
|
||||
## 预览
|
||||
|
||||
@@ -31,16 +32,25 @@ Soybean Admin Thin 是Soybean Admin的精简版。
|
||||
|
||||
- [gitee](https://gitee.com/honghuangdc/soybean-admin)
|
||||
|
||||
### 温馨提示(老用户)
|
||||
|
||||
旧版代码在old分支,等main分支稳定下来,再删除old分支。
|
||||
|
||||
如果不是第一次进预览地址 soybean.pro,新版的发布会导致有缓存,退出用户,重新登录即可。
|
||||
|
||||
thin分支相对于main分支少了插件示例,其他都一样,后面会适当精简一些代码。
|
||||
|
||||
## 项目示例图
|
||||

|
||||
|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
|
||||

|
||||
|
||||
### 使用 Gitpod
|
||||
|
||||
@@ -108,7 +118,7 @@ pnpm i -g commitizen
|
||||
|
||||
- 微信交流群:
|
||||
<div style="text-align:left">
|
||||
<img src="https://s2.loli.net/2021/12/29/m65oExs7yHcPbKZ.jpg" style="width:200px" />
|
||||
<img src="https://s2.loli.net/2022/02/07/hJWkOUAjpCQNwdf.jpg" style="width:200px" />
|
||||
</div>
|
||||
|
||||
- QQ 群 `711301266`
|
||||
|
@@ -125,6 +125,99 @@ const routes: AuthRoute.Route[] = [
|
||||
meta: {
|
||||
title: '组件示例',
|
||||
icon: 'fluent:app-store-24-regular',
|
||||
order: 3
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'plugin',
|
||||
path: '/plugin',
|
||||
component: 'basic',
|
||||
children: [
|
||||
{
|
||||
name: 'plugin_map',
|
||||
path: '/plugin/map',
|
||||
component: 'self',
|
||||
meta: {
|
||||
title: '地图',
|
||||
requiresAuth: true
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'plugin_video',
|
||||
path: '/plugin/video',
|
||||
component: 'self',
|
||||
meta: {
|
||||
title: '视频',
|
||||
requiresAuth: true
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'plugin_editor',
|
||||
path: '/plugin/editor',
|
||||
component: 'multi',
|
||||
children: [
|
||||
{
|
||||
name: 'plugin_editor_quill',
|
||||
path: '/plugin/editor/quill',
|
||||
component: 'self',
|
||||
meta: {
|
||||
title: '富文本编辑器'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'plugin_editor_markdown',
|
||||
path: '/plugin/editor/markdown',
|
||||
component: 'self',
|
||||
meta: {
|
||||
title: 'markdown编辑器'
|
||||
}
|
||||
}
|
||||
],
|
||||
meta: {
|
||||
title: '编辑器',
|
||||
requiresAuth: true
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'plugin_swiper',
|
||||
path: '/plugin/swiper',
|
||||
component: 'self',
|
||||
meta: {
|
||||
title: 'Swiper插件',
|
||||
requiresAuth: true
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'plugin_copy',
|
||||
path: '/plugin/copy',
|
||||
component: 'self',
|
||||
meta: {
|
||||
title: '剪贴板',
|
||||
requiresAuth: true
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'plugin_icon',
|
||||
path: '/plugin/icon',
|
||||
component: 'self',
|
||||
meta: {
|
||||
title: '图标',
|
||||
requiresAuth: true
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'plugin_print',
|
||||
path: '/plugin/print',
|
||||
component: 'self',
|
||||
meta: {
|
||||
title: '打印',
|
||||
requiresAuth: true
|
||||
}
|
||||
}
|
||||
],
|
||||
meta: {
|
||||
title: '插件示例',
|
||||
icon: 'clarity:plugin-line',
|
||||
order: 4
|
||||
}
|
||||
},
|
||||
|
49
package.json
49
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "soybean-admin-thin",
|
||||
"version": "0.1.3",
|
||||
"name": "soybean-admin",
|
||||
"version": "0.9.2",
|
||||
"scripts": {
|
||||
"dev": "cross-env VITE_HTTP_ENV=test vite",
|
||||
"dev:prod": "cross-env VITE_HTTP_ENV=prod vite",
|
||||
@@ -26,30 +26,39 @@
|
||||
"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",
|
||||
"vue": "^3.2.26",
|
||||
"vue-router": "^4.0.12"
|
||||
"swiper": "^8.0.3",
|
||||
"ua-parser-js": "^1.0.2",
|
||||
"vditor": "^3.8.11",
|
||||
"vue": "^3.2.29",
|
||||
"vue-router": "^4.0.12",
|
||||
"wangeditor": "^4.7.11",
|
||||
"xgplayer": "^2.31.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@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",
|
||||
@@ -57,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"
|
||||
}
|
||||
}
|
||||
|
4487
pnpm-lock.yaml
generated
4487
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();
|
||||
}
|
||||
|
15
src/components/custom/GithubLink/index.vue
Normal file
15
src/components/custom/GithubLink/index.vue
Normal file
@@ -0,0 +1,15 @@
|
||||
<template>
|
||||
<web-site-link label="github地址:" :link="link" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import WebSiteLink from '../WebSiteLink/index.vue';
|
||||
|
||||
interface Props {
|
||||
/** github链接 */
|
||||
link: string;
|
||||
}
|
||||
|
||||
defineProps<Props>();
|
||||
</script>
|
||||
<style scoped></style>
|
77
src/components/custom/IconSelect/index.vue
Normal file
77
src/components/custom/IconSelect/index.vue
Normal file
@@ -0,0 +1,77 @@
|
||||
<template>
|
||||
<n-popover placement="bottom-end" trigger="click">
|
||||
<template #trigger>
|
||||
<n-input v-model:value="modelValue" readonly placeholder="点击选择图标">
|
||||
<template #suffix>
|
||||
<Icon :icon="modelValue ? modelValue : emptyIcon" class="text-30px p-5px" />
|
||||
</template>
|
||||
</n-input>
|
||||
</template>
|
||||
<template #header>
|
||||
<n-input v-model:value="searchValue" placeholder="搜索图标"></n-input>
|
||||
</template>
|
||||
<div v-if="iconsList.length > 0" class="grid grid-cols-9 h-auto overflow-auto">
|
||||
<template v-for="iconItem in iconsList" :key="iconItem">
|
||||
<Icon
|
||||
:icon="iconItem"
|
||||
class="border-1px border-[#d9d9d9] text-30px m-2px p-5px"
|
||||
:style="{ 'border-color': modelValue === iconItem ? theme.themeColor : '' }"
|
||||
@click="handleChange(iconItem)"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
<n-empty v-else class="w-306px" description="你什么也找不到" />
|
||||
</n-popover>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed } from 'vue';
|
||||
import { NPopover, NInput, NEmpty } from 'naive-ui';
|
||||
import { Icon } from '@iconify/vue';
|
||||
import { useThemeStore } from '@/store';
|
||||
|
||||
interface Props {
|
||||
/** 选中的图标 */
|
||||
value: string;
|
||||
/** 图标列表 */
|
||||
icons: string[];
|
||||
/** 未选中图标 */
|
||||
emptyIcon?: string;
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:value', val: string): void;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
emptyIcon: 'mdi:apps'
|
||||
});
|
||||
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
const theme = useThemeStore();
|
||||
|
||||
const searchValue = ref('');
|
||||
const iconsList = computed(() => props.icons.filter(v => v.includes(searchValue.value)));
|
||||
|
||||
const modelValue = computed({
|
||||
get() {
|
||||
return props.value;
|
||||
},
|
||||
set(val: string) {
|
||||
emit('update:value', val);
|
||||
}
|
||||
});
|
||||
|
||||
function handleChange(iconItem: string) {
|
||||
modelValue.value = iconItem;
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
:deep(.n-input-wrapper) {
|
||||
padding-right: 0;
|
||||
}
|
||||
:deep(.n-input__suffix) {
|
||||
border: 1px solid #d9d9d9;
|
||||
}
|
||||
</style>
|
20
src/components/custom/WebSiteLink/index.vue
Normal file
20
src/components/custom/WebSiteLink/index.vue
Normal file
@@ -0,0 +1,20 @@
|
||||
<template>
|
||||
<p>
|
||||
<span>{{ label }}</span>
|
||||
<a class="text-blue-500" :href="link" target="_blank">
|
||||
{{ link }}
|
||||
</a>
|
||||
</p>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
interface Props {
|
||||
/** 网址名称 */
|
||||
label: string;
|
||||
/** 网址链接 */
|
||||
link: string;
|
||||
}
|
||||
|
||||
defineProps<Props>();
|
||||
</script>
|
||||
<style scoped></style>
|
@@ -3,5 +3,8 @@ import ButtonTab from './ButtonTab/index.vue';
|
||||
import ChromeTab from './ChromeTab/index.vue';
|
||||
import CountTo from './CountTo/index.vue';
|
||||
import ImageVerify from './ImageVerify/index.vue';
|
||||
import WebSiteLink from './WebSiteLink/index.vue';
|
||||
import GithubLink from './GithubLink/index.vue';
|
||||
import IconSelect from './IconSelect/index.vue';
|
||||
|
||||
export { BetterScroll, ButtonTab, ChromeTab, CountTo, ImageVerify };
|
||||
export { BetterScroll, ButtonTab, ChromeTab, CountTo, ImageVerify, WebSiteLink, GithubLink, IconSelect };
|
||||
|
@@ -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;
|
||||
}
|
||||
|
@@ -1,2 +1,3 @@
|
||||
export * from './service';
|
||||
export * from './regexp';
|
||||
export * from './map-sdk';
|
||||
|
9
src/config/common/map-sdk.ts
Normal file
9
src/config/common/map-sdk.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
/** 百度地图sdk地址 */
|
||||
export const BAIDU_MAP_SDK_URL =
|
||||
'https://api.map.baidu.com/getscript?v=3.0&ak=KSezYymXPth1DIGILRX3oYN9PxbOQQmU&services=&t=20210201100830&s=1';
|
||||
|
||||
/** 高德地图sdk地址 */
|
||||
export const GAODE_MAP_SDK_URL = 'https://webapi.amap.com/maps?v=2.0&key=e7bd02bd504062087e6563daf4d6721d';
|
||||
|
||||
/** 腾讯地图sdk地址 */
|
||||
export const TENCENT_MAP_SDK_URL = 'https://map.qq.com/api/gljs?v=1.exp&key=A6DBZ-KXPLW-JKSRY-ONZF4-CPHY3-K6BL7';
|
@@ -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,4 +1,7 @@
|
||||
import 'virtual:windi.css';
|
||||
import 'swiper/css';
|
||||
import 'swiper/css/navigation';
|
||||
import 'swiper/css/pagination';
|
||||
import '../styles/css/global.css';
|
||||
|
||||
/** 引入静态资源(全局引入css、字体等) */
|
||||
|
@@ -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 => {
|
||||
|
9
src/typings/common/map.d.ts
vendored
Normal file
9
src/typings/common/map.d.ts
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
/// <reference types="@amap/amap-jsapi-types" />
|
||||
/// <reference types="bmapgl" />
|
||||
|
||||
declare namespace BMap {
|
||||
class Map extends BMapGL.Map {}
|
||||
class Point extends BMapGL.Point {}
|
||||
}
|
||||
|
||||
declare const TMap: any;
|
12
src/typings/common/route.d.ts
vendored
12
src/typings/common/route.d.ts
vendored
@@ -26,6 +26,16 @@ declare namespace AuthRoute {
|
||||
| 'component_button'
|
||||
| 'component_card'
|
||||
| 'component_table'
|
||||
| 'plugin'
|
||||
| 'plugin_map'
|
||||
| 'plugin_video'
|
||||
| 'plugin_editor'
|
||||
| 'plugin_editor_quill'
|
||||
| 'plugin_editor_markdown'
|
||||
| 'plugin_copy'
|
||||
| 'plugin_icon'
|
||||
| 'plugin_print'
|
||||
| 'plugin_swiper'
|
||||
| 'exception'
|
||||
| 'exception_403'
|
||||
| 'exception_404'
|
||||
@@ -115,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,24 +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,
|
||||
MultiMenuFirstSecond,
|
||||
MultiMenuFirstSecondNewThird,
|
||||
About
|
||||
} from '@/views';
|
||||
import { views } from '@/views';
|
||||
import type { LayoutComponentName } from '@/interface';
|
||||
|
||||
type LayoutComponent = Record<LayoutComponentName, () => Promise<Component>>;
|
||||
@@ -35,76 +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'
|
||||
| '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',
|
||||
'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,
|
||||
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,6 +1,31 @@
|
||||
export * from './system';
|
||||
export * from './dashboard';
|
||||
export * from './document';
|
||||
export * from './component';
|
||||
export * from './about';
|
||||
export * from './multi-menu';
|
||||
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 };
|
34
src/views/plugin/copy/index.vue
Normal file
34
src/views/plugin/copy/index.vue
Normal file
@@ -0,0 +1,34 @@
|
||||
<template>
|
||||
<div class="h-full">
|
||||
<n-card title="文本复制" class="h-full shadow-sm rounded-16px">
|
||||
<n-input-group>
|
||||
<n-input v-model:value="source" placeholder="请输入要复制的内容吧" />
|
||||
<n-button type="primary" @click="handleCopy">复制</n-button>
|
||||
</n-input-group>
|
||||
</n-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import { NCard, NInputGroup, NInput, NButton, useMessage } from 'naive-ui';
|
||||
import { useClipboard } from '@vueuse/core';
|
||||
|
||||
const source = ref('');
|
||||
const message = useMessage();
|
||||
const { copy, isSupported } = useClipboard();
|
||||
|
||||
function handleCopy() {
|
||||
if (!isSupported) {
|
||||
message.error('您的浏览器不支持Clipboard API');
|
||||
return;
|
||||
}
|
||||
if (!source.value) {
|
||||
message.error('请输入要复制的内容');
|
||||
return;
|
||||
}
|
||||
copy(source.value);
|
||||
message.success(`复制成功:${source.value}`);
|
||||
}
|
||||
</script>
|
||||
<style scoped></style>
|
50
src/views/plugin/editor/markdown/index.vue
Normal file
50
src/views/plugin/editor/markdown/index.vue
Normal file
@@ -0,0 +1,50 @@
|
||||
<template>
|
||||
<div class="h-full">
|
||||
<n-card title="markdown插件" class="shadow-sm rounded-16px">
|
||||
<div ref="domRef"></div>
|
||||
<template #footer>
|
||||
<github-link link="https://github.com/Vanessa219/vditor" />
|
||||
</template>
|
||||
</n-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, onMounted, onUnmounted } from 'vue';
|
||||
import { NCard } from 'naive-ui';
|
||||
import Vditor from 'vditor';
|
||||
import 'vditor/src/assets/scss/index.scss';
|
||||
import { GithubLink } from '@/components';
|
||||
import { useThemeStore } from '@/store';
|
||||
|
||||
const theme = useThemeStore();
|
||||
|
||||
const vditor = ref<Vditor>();
|
||||
const domRef = ref<HTMLElement>();
|
||||
|
||||
function renderVditor() {
|
||||
vditor.value = new Vditor(domRef.value!, {
|
||||
minHeight: 400,
|
||||
theme: theme.darkMode ? 'dark' : 'classic',
|
||||
icon: 'material',
|
||||
cache: { enable: false }
|
||||
});
|
||||
}
|
||||
|
||||
const stopHandle = watch(
|
||||
() => theme.darkMode,
|
||||
newValue => {
|
||||
const themeMode = newValue ? 'dark' : 'classic';
|
||||
vditor.value?.setTheme(themeMode);
|
||||
}
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
renderVditor();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
stopHandle();
|
||||
});
|
||||
</script>
|
||||
<style scoped></style>
|
44
src/views/plugin/editor/quill/index.vue
Normal file
44
src/views/plugin/editor/quill/index.vue
Normal file
@@ -0,0 +1,44 @@
|
||||
<template>
|
||||
<div class="h-full">
|
||||
<n-card title="富文本插件" class="shadow-sm rounded-16px">
|
||||
<div ref="domRef" class="bg-white dark:bg-dark"></div>
|
||||
<template #footer>
|
||||
<github-link link="https://github.com/wangeditor-team/wangEditor" />
|
||||
</template>
|
||||
</n-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { NCard } from 'naive-ui';
|
||||
import WangEditor from 'wangeditor';
|
||||
import { GithubLink } from '@/components';
|
||||
|
||||
const editor = ref<WangEditor>();
|
||||
const domRef = ref<HTMLElement>();
|
||||
|
||||
function renderWangEditor() {
|
||||
editor.value = new WangEditor(domRef.value);
|
||||
setEditorConfig();
|
||||
editor.value.create();
|
||||
}
|
||||
|
||||
function setEditorConfig() {
|
||||
editor.value!.config.zIndex = 10;
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
renderWangEditor();
|
||||
});
|
||||
</script>
|
||||
<style scoped>
|
||||
:deep(.w-e-toolbar) {
|
||||
background: inherit !important;
|
||||
border-color: #999 !important;
|
||||
}
|
||||
:deep(.w-e-text-container) {
|
||||
background: inherit;
|
||||
border-color: #999 !important;
|
||||
}
|
||||
</style>
|
32
src/views/plugin/icon/icons.ts
Normal file
32
src/views/plugin/icon/icons.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
export const icons = [
|
||||
'mdi:emoticon',
|
||||
'mdi:ab-testing',
|
||||
'ph:alarm',
|
||||
'ph:android-logo',
|
||||
'ph:align-bottom',
|
||||
'ph:archive-box-light',
|
||||
'uil:basketball',
|
||||
'uil:brightness-plus',
|
||||
'uil:capture',
|
||||
'mdi:apps-box',
|
||||
'mdi:alert',
|
||||
'mdi:airballoon',
|
||||
'mdi:airplane-edit',
|
||||
'mdi:alpha-f-box-outline',
|
||||
'mdi:arm-flex-outline',
|
||||
'ic:baseline-10mp',
|
||||
'ic:baseline-access-time',
|
||||
'ic:baseline-brightness-4',
|
||||
'ic:baseline-brightness-5',
|
||||
'ic:baseline-credit-card',
|
||||
'ic:baseline-filter-1',
|
||||
'ic:baseline-filter-2',
|
||||
'ic:baseline-filter-3',
|
||||
'ic:baseline-filter-4',
|
||||
'ic:baseline-filter-5',
|
||||
'ic:baseline-filter-6',
|
||||
'ic:baseline-filter-7',
|
||||
'ic:baseline-filter-8',
|
||||
'ic:baseline-filter-9',
|
||||
'ic:baseline-filter-9-plus'
|
||||
];
|
31
src/views/plugin/icon/index.vue
Normal file
31
src/views/plugin/icon/index.vue
Normal file
@@ -0,0 +1,31 @@
|
||||
<template>
|
||||
<div class="h-full">
|
||||
<n-card title="Icon组件示例" class="shadow-sm rounded-16px">
|
||||
<div class="grid grid-cols-10">
|
||||
<template v-for="item in icons" :key="item">
|
||||
<div class="mt-5px flex-x-center">
|
||||
<Icon :icon="item" class="text-30px" />
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<div class="mt-50px">
|
||||
<h1 class="mb-20px text-18px font-500">Icon图标选择器</h1>
|
||||
<icon-select v-model:value="selectVal" :icons="icons" />
|
||||
</div>
|
||||
<template #footer>
|
||||
<web-site-link label="iconify地址:" link="https://icones.js.org/" class="mt-10px" />
|
||||
</template>
|
||||
</n-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import { NCard } from 'naive-ui';
|
||||
import { Icon } from '@iconify/vue';
|
||||
import { IconSelect, WebSiteLink } from '@/components';
|
||||
import { icons } from './icons';
|
||||
|
||||
const selectVal = ref('');
|
||||
</script>
|
||||
<style scoped></style>
|
26
src/views/plugin/map/components/BaiduMap.vue
Normal file
26
src/views/plugin/map/components/BaiduMap.vue
Normal file
@@ -0,0 +1,26 @@
|
||||
<template>
|
||||
<div ref="domRef" class="w-full h-full"></div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { useScriptTag } from '@vueuse/core';
|
||||
import { BAIDU_MAP_SDK_URL } from '@/config';
|
||||
|
||||
const { load } = useScriptTag(BAIDU_MAP_SDK_URL);
|
||||
|
||||
const domRef = ref<HTMLDivElement>();
|
||||
|
||||
async function renderBaiduMap() {
|
||||
await load(true);
|
||||
const map = new BMap.Map(domRef.value!);
|
||||
const point = new BMap.Point(114.05834626586915, 22.546789983033168);
|
||||
map.centerAndZoom(point, 15);
|
||||
map.enableScrollWheelZoom();
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
renderBaiduMap();
|
||||
});
|
||||
</script>
|
||||
<style scoped></style>
|
29
src/views/plugin/map/components/GaodeMap.vue
Normal file
29
src/views/plugin/map/components/GaodeMap.vue
Normal file
@@ -0,0 +1,29 @@
|
||||
<template>
|
||||
<div ref="domRef" class="w-full h-full"></div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { useScriptTag } from '@vueuse/core';
|
||||
import { GAODE_MAP_SDK_URL } from '@/config';
|
||||
|
||||
const { load } = useScriptTag(GAODE_MAP_SDK_URL);
|
||||
|
||||
const domRef = ref<HTMLDivElement>();
|
||||
|
||||
async function renderBaiduMap() {
|
||||
await load(true);
|
||||
const map = new AMap.Map(domRef.value!, {
|
||||
zoom: 11,
|
||||
center: [114.05834626586915, 22.546789983033168],
|
||||
viewMode: '3D'
|
||||
});
|
||||
|
||||
return map;
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
renderBaiduMap();
|
||||
});
|
||||
</script>
|
||||
<style scoped></style>
|
29
src/views/plugin/map/components/TencentMap.vue
Normal file
29
src/views/plugin/map/components/TencentMap.vue
Normal file
@@ -0,0 +1,29 @@
|
||||
<template>
|
||||
<div ref="domRef"></div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { useScriptTag } from '@vueuse/core';
|
||||
import { TENCENT_MAP_SDK_URL } from '@/config';
|
||||
|
||||
const { load } = useScriptTag(TENCENT_MAP_SDK_URL);
|
||||
|
||||
const domRef = ref<HTMLDivElement | null>(null);
|
||||
|
||||
async function renderBaiduMap() {
|
||||
await load(true);
|
||||
const map = new TMap.Map(domRef.value!, {
|
||||
center: new TMap.LatLng(39.98412, 116.307484),
|
||||
zoom: 11,
|
||||
viewMode: '3D'
|
||||
});
|
||||
|
||||
return map;
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
renderBaiduMap();
|
||||
});
|
||||
</script>
|
||||
<style scoped></style>
|
5
src/views/plugin/map/components/index.ts
Normal file
5
src/views/plugin/map/components/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import BaiduMap from './BaiduMap.vue';
|
||||
import GaodeMap from './GaodeMap.vue';
|
||||
import TencentMap from './TencentMap.vue';
|
||||
|
||||
export { BaiduMap, GaodeMap, TencentMap };
|
29
src/views/plugin/map/index.vue
Normal file
29
src/views/plugin/map/index.vue
Normal file
@@ -0,0 +1,29 @@
|
||||
<template>
|
||||
<div class="h-full">
|
||||
<n-card title="地图插件" class="h-full shadow-sm rounded-16px" content-style="overflow:hidden">
|
||||
<n-tabs type="line" class="flex-col-stretch h-full" pane-class="flex-1-hidden">
|
||||
<n-tab-pane v-for="item in maps" :key="item.id" :name="item.id" :tab="item.label">
|
||||
<component :is="item.component" />
|
||||
</n-tab-pane>
|
||||
</n-tabs>
|
||||
</n-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Component } from 'vue';
|
||||
import { NCard, NTabs, NTabPane } from 'naive-ui';
|
||||
import { GaodeMap, TencentMap } from './components';
|
||||
|
||||
interface Map {
|
||||
id: string;
|
||||
label: string;
|
||||
component: Component;
|
||||
}
|
||||
|
||||
const maps: Map[] = [
|
||||
{ id: 'gaode', label: '高德地图', component: GaodeMap },
|
||||
{ id: 'tencent', label: '腾讯地图', component: TencentMap }
|
||||
];
|
||||
</script>
|
||||
<style scoped></style>
|
40
src/views/plugin/print/index.vue
Normal file
40
src/views/plugin/print/index.vue
Normal file
@@ -0,0 +1,40 @@
|
||||
<template>
|
||||
<div class="h-full">
|
||||
<n-card title="打印" class="shadow-sm rounded-16px">
|
||||
<n-button type="primary" class="mr-10px" @click="printTable">打印表格</n-button>
|
||||
<n-button type="primary" @click="printImage">打印图片</n-button>
|
||||
<template #footer>
|
||||
<github-link label="printJS:" link="https://github.com/crabbly/Print.js" class="mt-10px" />
|
||||
</template>
|
||||
</n-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { NCard, NButton } from 'naive-ui';
|
||||
import printJS from 'print-js';
|
||||
import { GithubLink } from '@/components';
|
||||
|
||||
function printTable() {
|
||||
printJS({
|
||||
printable: [
|
||||
{ name: 'soybean', wechat: 'honghuangdc', remark: '欢迎来技术交流' },
|
||||
{ name: 'soybean', wechat: 'honghuangdc', remark: '欢迎来技术交流' }
|
||||
],
|
||||
properties: ['name', 'wechat', 'remark'],
|
||||
type: 'json'
|
||||
});
|
||||
}
|
||||
function printImage() {
|
||||
printJS({
|
||||
printable: [
|
||||
'https://raw.githubusercontent.com/honghuangdc/project-assets/main/img/qq_qrcode.JPG',
|
||||
'https://raw.githubusercontent.com/honghuangdc/project-assets/main/img/qq_qrcode.JPG'
|
||||
],
|
||||
type: 'image',
|
||||
header: 'Multiple Images',
|
||||
imageStyle: 'width:100%;'
|
||||
});
|
||||
}
|
||||
</script>
|
||||
<style scoped></style>
|
118
src/views/plugin/swiper/index.vue
Normal file
118
src/views/plugin/swiper/index.vue
Normal file
@@ -0,0 +1,118 @@
|
||||
<template>
|
||||
<div>
|
||||
<n-card title="Swiper插件" class="shadow-sm rounded-16px">
|
||||
<n-space :vertical="true">
|
||||
<github-link link="https://github.com/nolimits4web/swiper" />
|
||||
<web-site-link label="vue3版文档地址:" link="https://swiperjs.com/vue" />
|
||||
<web-site-link label="插件demo地址:" link="https://swiperjs.com/demos" />
|
||||
</n-space>
|
||||
<n-space :vertical="true">
|
||||
<div v-for="item in swiperExample" :key="item.id">
|
||||
<h3 class="py-24px text-24px font-bold">{{ item.label }}</h3>
|
||||
<swiper v-bind="item.options">
|
||||
<swiper-slide v-for="i in 5" :key="i">
|
||||
<div class="flex-center h-240px border-1px border-[#999] text-18px font-bold">Slide{{ i }}</div>
|
||||
</swiper-slide>
|
||||
</swiper>
|
||||
</div>
|
||||
</n-space>
|
||||
</n-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { NCard, NSpace } from 'naive-ui';
|
||||
import SwiperCore, { Navigation, Pagination } from 'swiper';
|
||||
import { Swiper, SwiperSlide } from 'swiper/vue';
|
||||
import type { SwiperOptions } from 'swiper';
|
||||
import { WebSiteLink, GithubLink } from '@/components';
|
||||
|
||||
type SwiperExampleOptions = Pick<
|
||||
SwiperOptions,
|
||||
| 'navigation'
|
||||
| 'pagination'
|
||||
| 'scrollbar'
|
||||
| 'slidesPerView'
|
||||
| 'slidesPerGroup'
|
||||
| 'spaceBetween'
|
||||
| 'direction'
|
||||
| 'loop'
|
||||
| 'loopFillGroupWithBlank'
|
||||
>;
|
||||
|
||||
interface SwiperExample {
|
||||
id: number;
|
||||
label: string;
|
||||
options: Partial<SwiperExampleOptions>;
|
||||
}
|
||||
|
||||
SwiperCore.use([Navigation, Pagination]);
|
||||
|
||||
const swiperExample: SwiperExample[] = [
|
||||
{ id: 0, label: 'Default', options: {} },
|
||||
{
|
||||
id: 1,
|
||||
label: 'Navigation',
|
||||
options: {
|
||||
navigation: true
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
label: 'Pagination',
|
||||
options: {
|
||||
pagination: true
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
label: 'Pagination dynamic',
|
||||
options: {
|
||||
pagination: { dynamicBullets: true }
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
label: 'Pagination progress',
|
||||
options: {
|
||||
navigation: true,
|
||||
pagination: {
|
||||
type: 'progressbar'
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
label: 'Pagination fraction',
|
||||
options: {
|
||||
navigation: true,
|
||||
pagination: {
|
||||
type: 'fraction'
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
label: 'Slides per view',
|
||||
options: {
|
||||
pagination: {
|
||||
clickable: true
|
||||
},
|
||||
slidesPerView: 3,
|
||||
spaceBetween: 30
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
label: 'Infinite loop',
|
||||
options: {
|
||||
navigation: true,
|
||||
pagination: {
|
||||
clickable: true
|
||||
},
|
||||
loop: true
|
||||
}
|
||||
}
|
||||
];
|
||||
</script>
|
||||
<style scoped></style>
|
37
src/views/plugin/video/index.vue
Normal file
37
src/views/plugin/video/index.vue
Normal file
@@ -0,0 +1,37 @@
|
||||
<template>
|
||||
<div class="h-full">
|
||||
<n-card title="视频播放器插件" class="h-full shadow-sm rounded-16px">
|
||||
<div ref="domRef"></div>
|
||||
</n-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted } from 'vue';
|
||||
import { NCard } from 'naive-ui';
|
||||
import Player from 'xgplayer';
|
||||
|
||||
const domRef = ref<HTMLElement>();
|
||||
const player = ref<Player>();
|
||||
|
||||
function renderXgPlayer() {
|
||||
const url = 'https://lf9-cdn-tos.bytecdntp.com/cdn/expire-1-M/byted-player-videos/1.0.0/xgplayer-demo.mp4';
|
||||
player.value = new Player({
|
||||
el: domRef.value!,
|
||||
url,
|
||||
playbackRate: [0.5, 0.75, 1, 1.5, 2]
|
||||
});
|
||||
}
|
||||
function destroyXgPlayer() {
|
||||
player.value?.destroy();
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
renderXgPlayer();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
destroyXgPlayer();
|
||||
});
|
||||
</script>
|
||||
<style scoped></style>
|
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