Compare commits

...

18 Commits

Author SHA1 Message Date
Soybean
114072277f chore(release): 0.9.2 2022-02-11 16:27:20 +08:00
Soybean
e65034d946 refactor(projects): 重构路由页面组件的导入 2022-02-11 16:15:50 +08:00
Soybean
a7a269d6a6 fix(projects): 修复项目配置拷贝功能 2022-02-07 23:22:55 +08:00
Soybean
2c9660fdbf fix(components): 修复Tab在移动端设备无法点击的问题 2022-02-07 15:46:35 +08:00
Soybean
e93b94cb24 fix(projects): 修复分析页和工作台的布局问题 2022-02-07 15:00:04 +08:00
Soybean
3befb22903 docs(projects): update README.md 2022-02-07 09:17:27 +08:00
Soybean
7ed5d0de2d docs(projects): update README.md 2022-02-07 09:16:18 +08:00
Soybean
47f2871cb5 docs(projects): update README.md 2022-02-03 17:48:04 +08:00
Soybean
852ddb64ad Merge pull request #37 from yanbowe/main
feat(projects): 迁移全局搜索菜单功能
2022-01-24 15:52:11 +08:00
yanbowen
7e1f9f1138 Merge branch 'honghuangdc:main' into main 2022-01-24 15:44:11 +08:00
yanbowen
554d7fd611 feat(projects): 迁移全局搜索菜单功能 2022-01-24 15:34:57 +08:00
Soybean
1797f29a79 Merge pull request #36 from dxxzst/main
build(projects): update .eslintignore
2022-01-24 14:28:37 +08:00
Grazing Wind
50063187ec Update .eslintignore
add package.json to .eslintignore
2022-01-24 13:57:42 +08:00
Soybean
b16721b2b7 build(projects): add license 2022-01-24 12:26:04 +08:00
Soybean
facc00e8b4 fix(projects): vite配置修复 2022-01-24 11:30:08 +08:00
Soybean
02c51e6fb9 docs(projects): update README.md 2022-01-24 01:28:35 +08:00
Soybean
be374089ba chore(release): 0.9.1 2022-01-24 00:54:16 +08:00
Soybean
68b42304d5 feat(projects): 新版重构完成 2022-01-24 00:53:51 +08:00
79 changed files with 3786 additions and 2225 deletions

View File

@@ -12,3 +12,4 @@ lib
.vscode
.local
!.env-config.ts
package.json

View File

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

View File

@@ -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](https://img.shields.io/badge/license-MIT-green.svg)](./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分支少了插件示例其他都一样后面会适当精简一些代码。
## 项目示例图
![](https://i.loli.net/2021/11/24/pIhTKP7fdCqbVHl.png)
![](https://i.loli.net/2021/11/24/gxRwsLnKi6IVp7C.png)
![](https://s2.loli.net/2022/01/24/ovK6Oyqr7gIMu2n.png)
![](https://i.loli.net/2021/11/24/UmVfjSJbxH6iYc2.png)
![](https://s2.loli.net/2022/01/24/O8loxYhMySHwGfJ.png)
![](https://i.loli.net/2021/11/24/Uot1bcfGXiF726T.png)
![](https://s2.loli.net/2022/01/24/HKwpJ7Ab6j8fVvk.png)
![](https://i.loli.net/2021/11/24/WzOIvlgJZaUtGm7.png)
![](https://s2.loli.net/2022/01/24/bqJRSDZHBv3jsif.png)
![](https://s2.loli.net/2022/01/24/wXpHeau6UrSTWdF.png)
### 使用 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`

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

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

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

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

View File

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

View File

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

View File

@@ -1,2 +1,3 @@
export * from './service';
export * from './regexp';
export * from './map-sdk';

View 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';

View File

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

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

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

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

View File

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

View File

@@ -0,0 +1 @@
export type RouteList = AuthRoute.Route;

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

View File

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

View File

@@ -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: '操作成功',

View File

@@ -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、字体等) */

View File

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

View File

@@ -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": "闪现"
}
]
}
}

View File

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

View File

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

View File

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

View File

@@ -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>;
}
/** 给页面组件设置名称 */

View File

@@ -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 - 权限路由

View File

@@ -1,3 +0,0 @@
const About = () => import('./index.vue');
export { About };

View File

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

View File

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

View File

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

View File

@@ -1,4 +0,0 @@
const DashboardAnalysis = () => import('./analysis/index.vue');
const DashboardWorkbench = () => import('./workbench/index.vue');
export { DashboardAnalysis, DashboardWorkbench };

View File

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

View File

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

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

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

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

View File

@@ -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();

View File

@@ -1,4 +0,0 @@
const MultiMenuFirstSecond = () => import('./first/second/index.vue');
const MultiMenuFirstSecondNewThird = () => import('./first/second-new/third/index.vue');
export { MultiMenuFirstSecond, MultiMenuFirstSecondNewThird };

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

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

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

View 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'
];

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

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

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

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

View File

@@ -0,0 +1,5 @@
import BaiduMap from './BaiduMap.vue';
import GaodeMap from './GaodeMap.vue';
import TencentMap from './TencentMap.vue';
export { BaiduMap, GaodeMap, TencentMap };

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

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

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

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

View File

@@ -0,0 +1,8 @@
<template>
<exception-base type="404" />
</template>
<script lang="ts" setup>
import { ExceptionBase } from '../components';
</script>
<style scoped></style>

View File

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

View File

@@ -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 *;`
}
}
},