Compare commits
1 Commits
thin-v0.10
...
tauri-v0.1
Author | SHA1 | Date | |
---|---|---|---|
|
c70b2299a9 |
2
.env
@@ -10,7 +10,7 @@ VITE_APP_DESC=SoybeanAdmin是一个中后台管理系统模版
|
|||||||
VITE_AUTH_ROUTE_MODE=static
|
VITE_AUTH_ROUTE_MODE=static
|
||||||
|
|
||||||
# 路由首页(根路由重定向), 用于static模式的权限路由,dynamic模式取决于后端返回的路由首页
|
# 路由首页(根路由重定向), 用于static模式的权限路由,dynamic模式取决于后端返回的路由首页
|
||||||
VITE_ROUTE_HOME_PATH=/multi-menu/first/second
|
VITE_ROUTE_HOME_PATH=/dashboard/analysis
|
||||||
|
|
||||||
# iconify图标作为组件的前缀
|
# iconify图标作为组件的前缀
|
||||||
VITE_ICON_PREFIX=icon
|
VITE_ICON_PREFIX=icon
|
||||||
|
@@ -1 +1,10 @@
|
|||||||
|
VITE_VISUALIZER=N
|
||||||
|
|
||||||
|
VITE_COMPRESS=N
|
||||||
|
|
||||||
|
# gzip | brotliCompress | deflate | deflateRaw
|
||||||
|
VITE_COMPRESS_TYPE=gzip
|
||||||
|
|
||||||
|
VITE_PWA=N
|
||||||
|
|
||||||
VITE_PROD_MOCK=Y
|
VITE_PROD_MOCK=Y
|
||||||
|
@@ -1,3 +1,4 @@
|
|||||||
!.env-config.ts
|
!.env-config.ts
|
||||||
router-page.d.ts
|
router-page.d.ts
|
||||||
|
*.svg
|
||||||
|
src-tauri/target
|
||||||
|
16
Makefile
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
ImageTag ?=v0.9.6
|
||||||
|
SoybeanAdminImg ?= soybeanjs/soybean-admin:$(ImageTag)
|
||||||
|
|
||||||
|
VERSION=$(shell git rev-parse --short HEAD)
|
||||||
|
|
||||||
|
soybean-admin: soybean-admin-build soybean-admin-push
|
||||||
|
|
||||||
|
soybean-admin-build:
|
||||||
|
docker build --build-arg version=$(VERSION) -t ${SoybeanAdminImg} -f docker/Dockerfile .
|
||||||
|
|
||||||
|
soybean-admin-push:
|
||||||
|
docker push ${SoybeanAdminImg}
|
||||||
|
|
||||||
|
# run tauri app:
|
||||||
|
run:
|
||||||
|
pnpm tauri dev
|
8
build/config/define.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
|
/** 项目构建时间 */
|
||||||
|
const PROJECT_BUILD_TIME = JSON.stringify(dayjs().format('YYYY-MM-DD HH:mm:ss'));
|
||||||
|
|
||||||
|
export const viteDefine = {
|
||||||
|
PROJECT_BUILD_TIME
|
||||||
|
};
|
@@ -1 +1,2 @@
|
|||||||
|
export * from './define';
|
||||||
export * from './proxy';
|
export * from './proxy';
|
||||||
|
6
build/plugins/compress.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import ViteCompression from 'vite-plugin-compression';
|
||||||
|
|
||||||
|
export default (viteEnv: ImportMetaEnv) => {
|
||||||
|
const { VITE_COMPRESS_TYPE = 'gzip' } = viteEnv;
|
||||||
|
return ViteCompression({ algorithm: VITE_COMPRESS_TYPE });
|
||||||
|
};
|
@@ -2,10 +2,15 @@ import type { PluginOption } from 'vite';
|
|||||||
import vue from '@vitejs/plugin-vue';
|
import vue from '@vitejs/plugin-vue';
|
||||||
import vueJsx from '@vitejs/plugin-vue-jsx';
|
import vueJsx from '@vitejs/plugin-vue-jsx';
|
||||||
import unocss from '@unocss/vite';
|
import unocss from '@unocss/vite';
|
||||||
|
import progress from 'vite-plugin-progress';
|
||||||
import VueDevtools from 'vite-plugin-vue-devtools';
|
import VueDevtools from 'vite-plugin-vue-devtools';
|
||||||
import pageRoute from '@soybeanjs/vite-plugin-vue-page-route';
|
import pageRoute from '@soybeanjs/vite-plugin-vue-page-route';
|
||||||
|
import { webUpdateNotice } from '@plugin-web-update-notification/vite';
|
||||||
import unplugin from './unplugin';
|
import unplugin from './unplugin';
|
||||||
import mock from './mock';
|
import mock from './mock';
|
||||||
|
import visualizer from './visualizer';
|
||||||
|
import compress from './compress';
|
||||||
|
import pwa from './pwa';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* vite插件
|
* vite插件
|
||||||
@@ -22,9 +27,27 @@ export function setupVitePlugins(viteEnv: ImportMetaEnv): (PluginOption | Plugin
|
|||||||
VueDevtools(),
|
VueDevtools(),
|
||||||
...unplugin(viteEnv),
|
...unplugin(viteEnv),
|
||||||
unocss(),
|
unocss(),
|
||||||
mock(viteEnv)
|
mock(viteEnv),
|
||||||
|
progress(),
|
||||||
|
webUpdateNotice({
|
||||||
|
notificationProps: {
|
||||||
|
title: '👋 有新版本了',
|
||||||
|
description: '点击刷新页面获取最新版本',
|
||||||
|
buttonText: '刷新',
|
||||||
|
dismissButtonText: '忽略'
|
||||||
|
}
|
||||||
|
})
|
||||||
];
|
];
|
||||||
|
|
||||||
|
if (viteEnv.VITE_VISUALIZER === 'Y') {
|
||||||
|
plugins.push(visualizer as PluginOption);
|
||||||
|
}
|
||||||
|
if (viteEnv.VITE_COMPRESS === 'Y') {
|
||||||
|
plugins.push(compress(viteEnv));
|
||||||
|
}
|
||||||
|
if (viteEnv.VITE_PWA === 'Y' || viteEnv.VITE_VERCEL === 'Y') {
|
||||||
|
plugins.push(pwa());
|
||||||
|
}
|
||||||
if (viteEnv.VITE_SOYBEAN_ROUTE_PLUGIN === 'Y') {
|
if (viteEnv.VITE_SOYBEAN_ROUTE_PLUGIN === 'Y') {
|
||||||
plugins.push(pageRoute());
|
plugins.push(pageRoute());
|
||||||
}
|
}
|
||||||
|
31
build/plugins/pwa.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import { VitePWA } from 'vite-plugin-pwa';
|
||||||
|
|
||||||
|
export default function setupVitePwa() {
|
||||||
|
return VitePWA({
|
||||||
|
registerType: 'autoUpdate',
|
||||||
|
includeAssets: ['favicon.ico'],
|
||||||
|
manifest: {
|
||||||
|
name: 'SoybeanAdmin',
|
||||||
|
short_name: 'SoybeanAdmin',
|
||||||
|
theme_color: '#fff',
|
||||||
|
icons: [
|
||||||
|
{
|
||||||
|
src: '/logo.png',
|
||||||
|
sizes: '192x192',
|
||||||
|
type: 'image/png'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: '/logo.png',
|
||||||
|
sizes: '512x512',
|
||||||
|
type: 'image/png'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: '/logo.png',
|
||||||
|
sizes: '512x512',
|
||||||
|
type: 'image/png',
|
||||||
|
purpose: 'any maskable'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
7
build/plugins/visualizer.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { visualizer } from 'rollup-plugin-visualizer';
|
||||||
|
|
||||||
|
export default visualizer({
|
||||||
|
gzipSize: true,
|
||||||
|
brotliSize: true,
|
||||||
|
open: true
|
||||||
|
});
|
32
docker/.dockerignore
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
node_modules
|
||||||
|
.DS_Store
|
||||||
|
dist
|
||||||
|
.npmrc
|
||||||
|
.cache
|
||||||
|
|
||||||
|
tests/server/static
|
||||||
|
tests/server/static/upload
|
||||||
|
|
||||||
|
.local
|
||||||
|
# local env files
|
||||||
|
.env.local
|
||||||
|
.env.*.local
|
||||||
|
.eslintcache
|
||||||
|
|
||||||
|
# Log files
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.idea
|
||||||
|
# .vscode
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
|
yarn.lock
|
||||||
|
pnpm-lock.yaml
|
||||||
|
/vite-profile.cpuprofile
|
24
docker/Dockerfile
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
FROM node:16.17.0 as builder
|
||||||
|
|
||||||
|
ENV WORKDIR=/soybean-admin
|
||||||
|
|
||||||
|
WORKDIR $WORKDIR
|
||||||
|
|
||||||
|
COPY ./ $WORKDIR/
|
||||||
|
|
||||||
|
ARG version
|
||||||
|
ENV COMMITID=$version
|
||||||
|
|
||||||
|
RUN npm i -g pnpm
|
||||||
|
|
||||||
|
RUN pnpm install
|
||||||
|
RUN pnpm build
|
||||||
|
|
||||||
|
FROM nginx:alpine as prod
|
||||||
|
|
||||||
|
RUN mkdir /soybean
|
||||||
|
|
||||||
|
COPY --from=builder /soybean-admin/dist /soybean-admin
|
||||||
|
COPY --from=builder /soybean-admin/docker/nginx.conf /etc/nginx/nginx.conf
|
||||||
|
|
||||||
|
EXPOSE 80
|
54
docker/nginx.conf
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
user nginx;
|
||||||
|
worker_processes 1;
|
||||||
|
error_log /var/log/nginx/error.log warn;
|
||||||
|
pid /var/run/nginx.pid;
|
||||||
|
|
||||||
|
events {
|
||||||
|
worker_connections 1024;
|
||||||
|
}
|
||||||
|
|
||||||
|
http {
|
||||||
|
include /etc/nginx/mime.types;
|
||||||
|
default_type application/octet-stream;
|
||||||
|
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
|
||||||
|
'$status $body_bytes_sent "$http_referer" '
|
||||||
|
'"$http_user_agent" "$http_x_forwarded_for"';
|
||||||
|
access_log /var/log/nginx/access.log main;
|
||||||
|
sendfile on;
|
||||||
|
keepalive_timeout 65;
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name localhost;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
# 不缓存html,防止程序更新后缓存继续生效
|
||||||
|
if ($request_filename ~* .*\.(?:htm|html)$) {
|
||||||
|
add_header Cache-Control "private, no-store, no-cache, must-revalidate, proxy-revalidate";
|
||||||
|
access_log on;
|
||||||
|
}
|
||||||
|
root /soybean-admin/;
|
||||||
|
index index.html index.htm;
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}
|
||||||
|
|
||||||
|
# location /soybean/soybean-webserver/v1 {
|
||||||
|
# proxy_set_header Host $host;
|
||||||
|
# proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
# proxy_set_header REMOTE-HOST $remote_addr;
|
||||||
|
|
||||||
|
# # 后台接口地址
|
||||||
|
# proxy_pass http://192.168.1.99:30597/v1;
|
||||||
|
# proxy_redirect default;
|
||||||
|
# add_header Access-Control-Allow-Origin *;
|
||||||
|
# add_header Access-Control-Allow-Headers X-Requested-With;
|
||||||
|
# add_header Access-Control-Allow-Methods GET,POST,OPTIONS;
|
||||||
|
# }
|
||||||
|
|
||||||
|
error_page 500 502 503 504 /50x.html;
|
||||||
|
location = /50x.html {
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -1,4 +1,5 @@
|
|||||||
import auth from './auth';
|
import auth from './auth';
|
||||||
import route from './route';
|
import route from './route';
|
||||||
|
import management from './management';
|
||||||
|
|
||||||
export default [...auth, ...route];
|
export default [...auth, ...route, ...management];
|
||||||
|
33
mock/api/management.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import { mock } from 'mockjs';
|
||||||
|
import type { MockMethod } from 'vite-plugin-mock';
|
||||||
|
|
||||||
|
const apis: MockMethod[] = [
|
||||||
|
{
|
||||||
|
url: '/mock/getAllUserList',
|
||||||
|
method: 'post',
|
||||||
|
response: (): Service.MockServiceResult<ApiUserManagement.User[]> => {
|
||||||
|
const data = mock({
|
||||||
|
'list|1000': [
|
||||||
|
{
|
||||||
|
id: '@id',
|
||||||
|
userName: '@cname',
|
||||||
|
'age|18-56': 56,
|
||||||
|
'gender|1': ['0', '1', null],
|
||||||
|
phone:
|
||||||
|
/^[1](([3][0-9])|([4][01456789])|([5][012356789])|([6][2567])|([7][0-8])|([8][0-9])|([9][012356789]))[0-9]{8}$/,
|
||||||
|
'email|1': ['@email("qq.com")', null],
|
||||||
|
'userStatus|1': ['1', '2', '3', '4', null]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
code: 200,
|
||||||
|
message: 'ok',
|
||||||
|
data: data.list
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
export default apis;
|
@@ -8,7 +8,7 @@ const apis: MockMethod[] = [
|
|||||||
response: (options: Service.MockOption): Service.MockServiceResult => {
|
response: (options: Service.MockOption): Service.MockServiceResult => {
|
||||||
const { userId = undefined } = options.body;
|
const { userId = undefined } = options.body;
|
||||||
|
|
||||||
const routeHomeName: AuthRoute.LastDegreeRouteKey = 'multi-menu_first_second';
|
const routeHomeName: AuthRoute.LastDegreeRouteKey = 'dashboard_analysis';
|
||||||
|
|
||||||
const role = userModel.find(item => item.userId === userId)?.userRole || 'user';
|
const role = userModel.find(item => item.userId === userId)?.userRole || 'user';
|
||||||
|
|
||||||
|
1184
mock/model/route.ts
32
package.json
@@ -38,10 +38,13 @@
|
|||||||
"dev": "cross-env VITE_SERVICE_ENV=dev vite",
|
"dev": "cross-env VITE_SERVICE_ENV=dev vite",
|
||||||
"dev:test": "cross-env VITE_SERVICE_ENV=test vite",
|
"dev:test": "cross-env VITE_SERVICE_ENV=test vite",
|
||||||
"dev:prod": "cross-env VITE_SERVICE_ENV=prod vite",
|
"dev:prod": "cross-env VITE_SERVICE_ENV=prod vite",
|
||||||
|
"dev:tauri": "pnpm tauri dev",
|
||||||
"build": "npm run typecheck && cross-env VITE_SERVICE_ENV=prod vite build",
|
"build": "npm run typecheck && cross-env VITE_SERVICE_ENV=prod vite build",
|
||||||
"build:dev": "npm run typecheck && cross-env VITE_SERVICE_ENV=dev vite build",
|
"build:dev": "npm run typecheck && cross-env VITE_SERVICE_ENV=dev vite build",
|
||||||
"build:test": "npm run typecheck && cross-env VITE_SERVICE_ENV=test vite build",
|
"build:test": "npm run typecheck && cross-env VITE_SERVICE_ENV=test vite build",
|
||||||
"build:vercel": "cross-env VITE_HASH_ROUTE=Y VITE_VERCEL=Y vite build",
|
"build:vercel": "cross-env VITE_HASH_ROUTE=Y VITE_VERCEL=Y vite build",
|
||||||
|
"build:tauri": "pnpm tauri build",
|
||||||
|
"tauri-icon": "pnpm tauri icon ./public/logo.png",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"typecheck": "vue-tsc --noEmit --skipLibCheck",
|
"typecheck": "vue-tsc --noEmit --skipLibCheck",
|
||||||
"lint": "eslint . --fix",
|
"lint": "eslint . --fix",
|
||||||
@@ -49,10 +52,14 @@
|
|||||||
"commit": "soy git-commit",
|
"commit": "soy git-commit",
|
||||||
"cleanup": "soy cleanup",
|
"cleanup": "soy cleanup",
|
||||||
"update-pkg": "soy ncu",
|
"update-pkg": "soy ncu",
|
||||||
|
"release": "soy release",
|
||||||
"tsx": "tsx",
|
"tsx": "tsx",
|
||||||
"logo": "tsx ./scripts/logo.ts"
|
"logo": "tsx ./scripts/logo.ts",
|
||||||
|
"prepare": "soy init-simple-git-hooks"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@antv/data-set": "0.11.8",
|
||||||
|
"@antv/g2": "4.2.10",
|
||||||
"@better-scroll/core": "2.5.1",
|
"@better-scroll/core": "2.5.1",
|
||||||
"@soybeanjs/vue-materials": "0.2.0",
|
"@soybeanjs/vue-materials": "0.2.0",
|
||||||
"@vueuse/core": "10.4.1",
|
"@vueuse/core": "10.4.1",
|
||||||
@@ -61,22 +68,33 @@
|
|||||||
"colord": "2.9.3",
|
"colord": "2.9.3",
|
||||||
"crypto-js": "4.1.1",
|
"crypto-js": "4.1.1",
|
||||||
"dayjs": "1.11.10",
|
"dayjs": "1.11.10",
|
||||||
|
"echarts": "5.4.3",
|
||||||
"form-data": "4.0.0",
|
"form-data": "4.0.0",
|
||||||
"lodash-es": "4.17.21",
|
"lodash-es": "4.17.21",
|
||||||
"naive-ui": "2.34.4",
|
"naive-ui": "2.34.4",
|
||||||
"pinia": "2.1.6",
|
"pinia": "2.1.6",
|
||||||
|
"print-js": "1.6.0",
|
||||||
"qs": "6.11.2",
|
"qs": "6.11.2",
|
||||||
|
"socket.io-client": "4.7.2",
|
||||||
|
"swiper": "10.2.0",
|
||||||
"ua-parser-js": "1.0.36",
|
"ua-parser-js": "1.0.36",
|
||||||
|
"vditor": "3.9.5",
|
||||||
"vue": "3.3.4",
|
"vue": "3.3.4",
|
||||||
"vue-i18n": "9.4.1",
|
"vue-i18n": "9.4.1",
|
||||||
"vue-router": "4.2.4",
|
"vue-router": "4.2.4",
|
||||||
|
"vuedraggable": "4.1.0",
|
||||||
|
"wangeditor": "4.7.15",
|
||||||
"xgplayer": "3.0.9"
|
"xgplayer": "3.0.9"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@amap/amap-jsapi-types": "0.0.13",
|
||||||
"@iconify/json": "2.2.118",
|
"@iconify/json": "2.2.118",
|
||||||
"@iconify/vue": "4.1.1",
|
"@iconify/vue": "4.1.1",
|
||||||
|
"@plugin-web-update-notification/vite": "^1.6.5",
|
||||||
"@soybeanjs/cli": "0.7.1",
|
"@soybeanjs/cli": "0.7.1",
|
||||||
"@soybeanjs/vite-plugin-vue-page-route": "0.0.10",
|
"@soybeanjs/vite-plugin-vue-page-route": "0.0.10",
|
||||||
|
"@tauri-apps/cli": "^1.3.1",
|
||||||
|
"@types/bmapgl": "0.0.7",
|
||||||
"@types/crypto-js": "4.1.2",
|
"@types/crypto-js": "4.1.2",
|
||||||
"@types/node": "20.6.3",
|
"@types/node": "20.6.3",
|
||||||
"@types/qs": "6.9.8",
|
"@types/qs": "6.9.8",
|
||||||
@@ -90,13 +108,18 @@
|
|||||||
"eslint": "8.49.0",
|
"eslint": "8.49.0",
|
||||||
"eslint-config-soybeanjs": "0.5.6",
|
"eslint-config-soybeanjs": "0.5.6",
|
||||||
"mockjs": "1.1.0",
|
"mockjs": "1.1.0",
|
||||||
|
"rollup-plugin-visualizer": "5.9.2",
|
||||||
"sass": "1.67.0",
|
"sass": "1.67.0",
|
||||||
|
"simple-git-hooks": "2.9.0",
|
||||||
"tsx": "3.12.10",
|
"tsx": "3.12.10",
|
||||||
"typescript": "5.2.2",
|
"typescript": "5.2.2",
|
||||||
"unplugin-icons": "0.17.0",
|
"unplugin-icons": "0.17.0",
|
||||||
"unplugin-vue-components": "0.25.2",
|
"unplugin-vue-components": "0.25.2",
|
||||||
"vite": "4.4.9",
|
"vite": "4.4.9",
|
||||||
|
"vite-plugin-compression": "0.5.1",
|
||||||
"vite-plugin-mock": "2.9.8",
|
"vite-plugin-mock": "2.9.8",
|
||||||
|
"vite-plugin-progress": "0.0.7",
|
||||||
|
"vite-plugin-pwa": "0.16.5",
|
||||||
"vite-plugin-svg-icons": "2.0.1",
|
"vite-plugin-svg-icons": "2.0.1",
|
||||||
"vite-plugin-vue-devtools": "1.0.0-rc.4",
|
"vite-plugin-vue-devtools": "1.0.0-rc.4",
|
||||||
"vue-tsc": "1.8.13"
|
"vue-tsc": "1.8.13"
|
||||||
@@ -105,5 +128,12 @@
|
|||||||
"patchedDependencies": {
|
"patchedDependencies": {
|
||||||
"mockjs@1.1.0": "patches/mockjs@1.1.0.patch"
|
"mockjs@1.1.0": "patches/mockjs@1.1.0.patch"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"simple-git-hooks": {
|
||||||
|
"commit-msg": "pnpm soy git-commit-verify",
|
||||||
|
"pre-commit": "pnpm typecheck && pnpm lint"
|
||||||
|
},
|
||||||
|
"soybean": {
|
||||||
|
"useSoybeanToken": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
2373
pnpm-lock.yaml
generated
3
src-tauri/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# Generated by Cargo
|
||||||
|
# will have compiled files and executables
|
||||||
|
/target/
|
4302
src-tauri/Cargo.lock
generated
Normal file
28
src-tauri/Cargo.toml
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
[package]
|
||||||
|
name = "app"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "A Tauri App"
|
||||||
|
authors = ["you"]
|
||||||
|
license = ""
|
||||||
|
repository = ""
|
||||||
|
default-run = "app"
|
||||||
|
edition = "2021"
|
||||||
|
rust-version = "1.57"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
tauri-build = { version = "1.1.1", features = [] }
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
serde_json = "1.0"
|
||||||
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
tauri = { version = "1.1.1", features = ["api-all"] }
|
||||||
|
|
||||||
|
[features]
|
||||||
|
# by default Tauri runs in production mode
|
||||||
|
# when `tauri dev` runs it is executed with `cargo run --no-default-features` if `devPath` is an URL
|
||||||
|
default = [ "custom-protocol" ]
|
||||||
|
# this feature is used for production builds where `devPath` points to the filesystem
|
||||||
|
# DO NOT remove this
|
||||||
|
custom-protocol = [ "tauri/custom-protocol" ]
|
3
src-tauri/build.rs
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
fn main() {
|
||||||
|
tauri_build::build()
|
||||||
|
}
|
BIN
src-tauri/icons/128x128.png
Normal file
After Width: | Height: | Size: 9.0 KiB |
BIN
src-tauri/icons/128x128@2x.png
Normal file
After Width: | Height: | Size: 20 KiB |
BIN
src-tauri/icons/32x32.png
Normal file
After Width: | Height: | Size: 1.8 KiB |
BIN
src-tauri/icons/Square107x107Logo.png
Normal file
After Width: | Height: | Size: 7.8 KiB |
BIN
src-tauri/icons/Square142x142Logo.png
Normal file
After Width: | Height: | Size: 10 KiB |
BIN
src-tauri/icons/Square150x150Logo.png
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
src-tauri/icons/Square284x284Logo.png
Normal file
After Width: | Height: | Size: 22 KiB |
BIN
src-tauri/icons/Square30x30Logo.png
Normal file
After Width: | Height: | Size: 1.7 KiB |
BIN
src-tauri/icons/Square310x310Logo.png
Normal file
After Width: | Height: | Size: 24 KiB |
BIN
src-tauri/icons/Square44x44Logo.png
Normal file
After Width: | Height: | Size: 2.8 KiB |
BIN
src-tauri/icons/Square71x71Logo.png
Normal file
After Width: | Height: | Size: 4.9 KiB |
BIN
src-tauri/icons/Square89x89Logo.png
Normal file
After Width: | Height: | Size: 6.4 KiB |
BIN
src-tauri/icons/StoreLogo.png
Normal file
After Width: | Height: | Size: 3.2 KiB |
BIN
src-tauri/icons/icon.icns
Normal file
BIN
src-tauri/icons/icon.ico
Normal file
After Width: | Height: | Size: 32 KiB |
BIN
src-tauri/icons/icon.png
Normal file
After Width: | Height: | Size: 42 KiB |
10
src-tauri/src/main.rs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
#![cfg_attr(
|
||||||
|
all(not(debug_assertions), target_os = "windows"),
|
||||||
|
windows_subsystem = "windows"
|
||||||
|
)]
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
tauri::Builder::default()
|
||||||
|
.run(tauri::generate_context!())
|
||||||
|
.expect("error while running tauri application");
|
||||||
|
}
|
60
src-tauri/tauri.conf.json
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
{
|
||||||
|
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
|
||||||
|
"build": {
|
||||||
|
"beforeBuildCommand": "npm run build",
|
||||||
|
"beforeDevCommand": "npm run dev",
|
||||||
|
"devPath": "http://localhost:3200",
|
||||||
|
"distDir": "../dist"
|
||||||
|
},
|
||||||
|
"package": {
|
||||||
|
"productName": "soybean-admin",
|
||||||
|
"version": "0.10.4"
|
||||||
|
},
|
||||||
|
"tauri": {
|
||||||
|
"allowlist": {
|
||||||
|
"all": true
|
||||||
|
},
|
||||||
|
"bundle": {
|
||||||
|
"active": true,
|
||||||
|
"category": "DeveloperTool",
|
||||||
|
"copyright": "",
|
||||||
|
"deb": {
|
||||||
|
"depends": []
|
||||||
|
},
|
||||||
|
"externalBin": [],
|
||||||
|
"icon": ["icons/32x32.png", "icons/128x128.png", "icons/128x128@2x.png", "icons/icon.icns", "icons/icon.ico"],
|
||||||
|
"identifier": "cn.soybeanjs.tauri-admin",
|
||||||
|
"longDescription": "",
|
||||||
|
"macOS": {
|
||||||
|
"entitlements": null,
|
||||||
|
"exceptionDomain": "",
|
||||||
|
"frameworks": [],
|
||||||
|
"providerShortName": null,
|
||||||
|
"signingIdentity": null
|
||||||
|
},
|
||||||
|
"resources": [],
|
||||||
|
"shortDescription": "",
|
||||||
|
"targets": "all",
|
||||||
|
"windows": {
|
||||||
|
"certificateThumbprint": null,
|
||||||
|
"digestAlgorithm": "sha256",
|
||||||
|
"timestampUrl": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": {
|
||||||
|
"csp": null
|
||||||
|
},
|
||||||
|
"updater": {
|
||||||
|
"active": false
|
||||||
|
},
|
||||||
|
"windows": [
|
||||||
|
{
|
||||||
|
"fullscreen": false,
|
||||||
|
"height": 800,
|
||||||
|
"resizable": true,
|
||||||
|
"title": "soybean-admin",
|
||||||
|
"width": 1000
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
17
src/components/custom/github-link.vue
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<template>
|
||||||
|
<web-site-link label="github地址:" :link="link" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import WebSiteLink from './web-site-link.vue';
|
||||||
|
|
||||||
|
defineOptions({ name: 'GithubLink' });
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
/** github链接 */
|
||||||
|
link: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
defineProps<Props>();
|
||||||
|
</script>
|
||||||
|
<style scoped></style>
|
77
src/components/custom/icon-select.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>
|
||||||
|
<svg-icon :icon="selectedIcon" 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">
|
||||||
|
<span v-for="iconItem in iconsList" :key="iconItem" @click="handleChange(iconItem)">
|
||||||
|
<svg-icon
|
||||||
|
:icon="iconItem"
|
||||||
|
class="border-1px border-#d9d9d9 text-30px m-2px p-5px cursor-pointer"
|
||||||
|
:class="{ 'border-primary': modelValue === iconItem }"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<n-empty v-else class="w-306px" description="你什么也找不到" />
|
||||||
|
</n-popover>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
|
||||||
|
defineOptions({ name: 'IconSelect' });
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
/** 选中的图标 */
|
||||||
|
value: string;
|
||||||
|
/** 图标列表 */
|
||||||
|
icons: string[];
|
||||||
|
/** 未选中图标 */
|
||||||
|
emptyIcon?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
emptyIcon: 'mdi:apps'
|
||||||
|
});
|
||||||
|
|
||||||
|
interface Emits {
|
||||||
|
(e: 'update:value', val: string): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const emit = defineEmits<Emits>();
|
||||||
|
|
||||||
|
const modelValue = computed({
|
||||||
|
get() {
|
||||||
|
return props.value;
|
||||||
|
},
|
||||||
|
set(val: string) {
|
||||||
|
emit('update:value', val);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const selectedIcon = computed(() => modelValue.value || props.emptyIcon);
|
||||||
|
|
||||||
|
const searchValue = ref('');
|
||||||
|
|
||||||
|
const iconsList = computed(() => props.icons.filter(v => v.includes(searchValue.value)));
|
||||||
|
|
||||||
|
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>
|
42
src/components/custom/image-verify.vue
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<canvas ref="domRef" width="152" height="40" class="cursor-pointer" @click="getImgCode"></canvas>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { watch } from 'vue';
|
||||||
|
import { useImageVerify } from '@/hooks';
|
||||||
|
|
||||||
|
defineOptions({ name: 'ImageVerify' });
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
code?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
code: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
interface Emits {
|
||||||
|
(e: 'update:code', code: string): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const emit = defineEmits<Emits>();
|
||||||
|
|
||||||
|
const { domRef, imgCode, setImgCode, getImgCode } = useImageVerify();
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.code,
|
||||||
|
newValue => {
|
||||||
|
setImgCode(newValue);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
watch(imgCode, newValue => {
|
||||||
|
emit('update:code', newValue);
|
||||||
|
});
|
||||||
|
|
||||||
|
defineExpose({ getImgCode });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped></style>
|
23
src/components/custom/web-site-link.vue
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<template>
|
||||||
|
<p>
|
||||||
|
<span>{{ label }}</span>
|
||||||
|
<a class="text-blue-500" :href="link" target="_blank">
|
||||||
|
{{ link }}
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
defineOptions({ name: 'WebSiteLink' });
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
/** 网址名称 */
|
||||||
|
label: string;
|
||||||
|
/** 网址链接 */
|
||||||
|
link: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
defineProps<Props>();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped></style>
|
174
src/composables/echarts.ts
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
import { nextTick, effectScope, onScopeDispose, ref, watch } from 'vue';
|
||||||
|
import type { ComputedRef, Ref } from 'vue';
|
||||||
|
import * as echarts from 'echarts/core';
|
||||||
|
import { BarChart, GaugeChart, LineChart, PictorialBarChart, PieChart, RadarChart, ScatterChart } from 'echarts/charts';
|
||||||
|
import type {
|
||||||
|
BarSeriesOption,
|
||||||
|
GaugeSeriesOption,
|
||||||
|
LineSeriesOption,
|
||||||
|
PictorialBarSeriesOption,
|
||||||
|
PieSeriesOption,
|
||||||
|
RadarSeriesOption,
|
||||||
|
ScatterSeriesOption
|
||||||
|
} from 'echarts/charts';
|
||||||
|
import {
|
||||||
|
DatasetComponent,
|
||||||
|
GridComponent,
|
||||||
|
LegendComponent,
|
||||||
|
TitleComponent,
|
||||||
|
ToolboxComponent,
|
||||||
|
TooltipComponent,
|
||||||
|
TransformComponent
|
||||||
|
} from 'echarts/components';
|
||||||
|
import type {
|
||||||
|
DatasetComponentOption,
|
||||||
|
GridComponentOption,
|
||||||
|
LegendComponentOption,
|
||||||
|
TitleComponentOption,
|
||||||
|
ToolboxComponentOption,
|
||||||
|
TooltipComponentOption
|
||||||
|
} from 'echarts/components';
|
||||||
|
import { LabelLayout, UniversalTransition } from 'echarts/features';
|
||||||
|
import { CanvasRenderer } from 'echarts/renderers';
|
||||||
|
import { useElementSize } from '@vueuse/core';
|
||||||
|
import { useThemeStore } from '@/store';
|
||||||
|
|
||||||
|
export type ECOption = echarts.ComposeOption<
|
||||||
|
| BarSeriesOption
|
||||||
|
| LineSeriesOption
|
||||||
|
| PieSeriesOption
|
||||||
|
| ScatterSeriesOption
|
||||||
|
| PictorialBarSeriesOption
|
||||||
|
| RadarSeriesOption
|
||||||
|
| GaugeSeriesOption
|
||||||
|
| TitleComponentOption
|
||||||
|
| LegendComponentOption
|
||||||
|
| TooltipComponentOption
|
||||||
|
| GridComponentOption
|
||||||
|
| ToolboxComponentOption
|
||||||
|
| DatasetComponentOption
|
||||||
|
>;
|
||||||
|
|
||||||
|
echarts.use([
|
||||||
|
TitleComponent,
|
||||||
|
LegendComponent,
|
||||||
|
TooltipComponent,
|
||||||
|
GridComponent,
|
||||||
|
DatasetComponent,
|
||||||
|
TransformComponent,
|
||||||
|
ToolboxComponent,
|
||||||
|
BarChart,
|
||||||
|
LineChart,
|
||||||
|
PieChart,
|
||||||
|
ScatterChart,
|
||||||
|
PictorialBarChart,
|
||||||
|
RadarChart,
|
||||||
|
GaugeChart,
|
||||||
|
LabelLayout,
|
||||||
|
UniversalTransition,
|
||||||
|
CanvasRenderer
|
||||||
|
]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Echarts hooks函数
|
||||||
|
* @param options - 图表配置
|
||||||
|
* @param renderFun - 图表渲染函数(例如:图表监听函数)
|
||||||
|
* @description 按需引入图表组件,没注册的组件需要先引入
|
||||||
|
*/
|
||||||
|
export function useEcharts(
|
||||||
|
options: Ref<ECOption> | ComputedRef<ECOption>,
|
||||||
|
renderFun?: (chartInstance: echarts.ECharts) => void
|
||||||
|
) {
|
||||||
|
const theme = useThemeStore();
|
||||||
|
|
||||||
|
const domRef = ref<HTMLElement>();
|
||||||
|
|
||||||
|
const initialSize = { width: 0, height: 0 };
|
||||||
|
const { width, height } = useElementSize(domRef, initialSize);
|
||||||
|
|
||||||
|
let chart: echarts.ECharts | null = null;
|
||||||
|
|
||||||
|
function canRender() {
|
||||||
|
return initialSize.width > 0 && initialSize.height > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isRendered() {
|
||||||
|
return Boolean(domRef.value && chart);
|
||||||
|
}
|
||||||
|
|
||||||
|
function update(updateOptions: ECOption) {
|
||||||
|
if (isRendered()) {
|
||||||
|
chart?.clear();
|
||||||
|
chart!.setOption({ ...updateOptions, backgroundColor: 'transparent' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function render() {
|
||||||
|
if (domRef.value) {
|
||||||
|
const chartTheme = theme.darkMode ? 'dark' : 'light';
|
||||||
|
await nextTick();
|
||||||
|
chart = echarts.init(domRef.value, chartTheme);
|
||||||
|
if (renderFun) {
|
||||||
|
renderFun(chart);
|
||||||
|
}
|
||||||
|
update(options.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function resize() {
|
||||||
|
chart?.resize();
|
||||||
|
}
|
||||||
|
|
||||||
|
function destroy() {
|
||||||
|
chart?.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateTheme() {
|
||||||
|
destroy();
|
||||||
|
render();
|
||||||
|
}
|
||||||
|
|
||||||
|
const scope = effectScope();
|
||||||
|
|
||||||
|
scope.run(() => {
|
||||||
|
watch([width, height], ([newWidth, newHeight]) => {
|
||||||
|
initialSize.width = newWidth;
|
||||||
|
initialSize.height = newHeight;
|
||||||
|
if (newWidth === 0 && newHeight === 0) {
|
||||||
|
// 节点被删除 将chart置为空
|
||||||
|
chart = null;
|
||||||
|
}
|
||||||
|
if (canRender()) {
|
||||||
|
if (!isRendered()) {
|
||||||
|
render();
|
||||||
|
} else {
|
||||||
|
resize();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
options,
|
||||||
|
newValue => {
|
||||||
|
update(newValue);
|
||||||
|
},
|
||||||
|
{ deep: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => theme.darkMode,
|
||||||
|
() => {
|
||||||
|
updateTheme();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
onScopeDispose(() => {
|
||||||
|
destroy();
|
||||||
|
scope.stop();
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
domRef
|
||||||
|
};
|
||||||
|
}
|
@@ -48,7 +48,7 @@ export const useIconRender = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!icon && !localIcon) {
|
if (!icon && !localIcon) {
|
||||||
throw Error('没有传递图标名称,请确保给icon或localIcon传递有效值!');
|
window.console.warn('没有传递图标名称,请确保给icon或localIcon传递有效值!');
|
||||||
}
|
}
|
||||||
|
|
||||||
return () => h(SvgIcon, { icon, localIcon, style });
|
return () => h(SvgIcon, { icon, localIcon, style });
|
||||||
|
@@ -2,4 +2,6 @@ export * from './system';
|
|||||||
export * from './router';
|
export * from './router';
|
||||||
export * from './layout';
|
export * from './layout';
|
||||||
export * from './events';
|
export * from './events';
|
||||||
|
export * from './echarts';
|
||||||
export * from './icon';
|
export * from './icon';
|
||||||
|
export * from './websocket';
|
||||||
|
50
src/composables/websocket.ts
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import { io } from 'socket.io-client';
|
||||||
|
import type { Socket } from 'socket.io-client';
|
||||||
|
import { useAppStore } from '../store';
|
||||||
|
|
||||||
|
type ListenEvents = {
|
||||||
|
update: (id: string, data: { name: string; age: number }) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
type EmitEvents = {
|
||||||
|
update: (id: string, data: { name: string; age: number }) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function useWebsocket() {
|
||||||
|
const app = useAppStore();
|
||||||
|
|
||||||
|
const socket: Socket<ListenEvents, EmitEvents> = (app.socket || io('ws://localhost:8080')) as Socket<
|
||||||
|
ListenEvents,
|
||||||
|
EmitEvents
|
||||||
|
>;
|
||||||
|
|
||||||
|
if (!app.socket) {
|
||||||
|
app.setSocket(socket);
|
||||||
|
}
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
window.console.log('[socket.io] connecting...');
|
||||||
|
|
||||||
|
socket.on('connect', () => {
|
||||||
|
window.console.log('[socket.io] connected.');
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('disconnect', () => {
|
||||||
|
window.console.log('[socket.io] disconnected.');
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('update', (id, data) => {
|
||||||
|
window.console.log('[socket.io] update', id, data);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleUpdate(id: string, data: { name: string; age: number }) {
|
||||||
|
socket.emit('update', id, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
init();
|
||||||
|
|
||||||
|
return {
|
||||||
|
handleUpdate
|
||||||
|
};
|
||||||
|
}
|
@@ -1,2 +1,3 @@
|
|||||||
export * from './service';
|
export * from './service';
|
||||||
export * from './regexp';
|
export * from './regexp';
|
||||||
|
export * from './map-sdk';
|
||||||
|
@@ -15,3 +15,19 @@ export const userRoleLabels: Record<Auth.RoleType, string> = {
|
|||||||
user: $t('page.login.pwdLogin.user')
|
user: $t('page.login.pwdLogin.user')
|
||||||
};
|
};
|
||||||
export const userRoleOptions = transformObjectToOption(userRoleLabels);
|
export const userRoleOptions = transformObjectToOption(userRoleLabels);
|
||||||
|
|
||||||
|
/** 用户性别 */
|
||||||
|
export const genderLabels: Record<UserManagement.GenderKey, string> = {
|
||||||
|
0: '女',
|
||||||
|
1: '男'
|
||||||
|
};
|
||||||
|
export const genderOptions = transformObjectToOption(genderLabels);
|
||||||
|
|
||||||
|
/** 用户状态 */
|
||||||
|
export const userStatusLabels: Record<UserManagement.UserStatusKey, string> = {
|
||||||
|
1: '启用',
|
||||||
|
2: '禁用',
|
||||||
|
3: '冻结',
|
||||||
|
4: '软删除'
|
||||||
|
};
|
||||||
|
export const userStatusOptions = transformObjectToOption(userStatusLabels);
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
import useCountDown from './use-count-down';
|
import useCountDown from './use-count-down';
|
||||||
import useSmsCode from './use-sms-code';
|
import useSmsCode from './use-sms-code';
|
||||||
|
import useImageVerify from './use-image-verify';
|
||||||
|
|
||||||
export { useCountDown, useSmsCode };
|
export { useCountDown, useSmsCode, useImageVerify };
|
||||||
|
87
src/hooks/business/use-image-verify.ts
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
import { onMounted, ref } from 'vue';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 绘制图形验证码
|
||||||
|
* @param width - 图形宽度
|
||||||
|
* @param height - 图形高度
|
||||||
|
*/
|
||||||
|
export default function useImageVerify(width = 152, height = 40) {
|
||||||
|
const domRef = ref<HTMLCanvasElement>();
|
||||||
|
const imgCode = ref('');
|
||||||
|
|
||||||
|
function setImgCode(code: string) {
|
||||||
|
imgCode.value = code;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getImgCode() {
|
||||||
|
if (!domRef.value) return;
|
||||||
|
imgCode.value = draw(domRef.value, width, height);
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
getImgCode();
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
domRef,
|
||||||
|
imgCode,
|
||||||
|
setImgCode,
|
||||||
|
getImgCode
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function randomNum(min: number, max: number) {
|
||||||
|
const num = Math.floor(Math.random() * (max - min) + min);
|
||||||
|
return num;
|
||||||
|
}
|
||||||
|
|
||||||
|
function randomColor(min: number, max: number) {
|
||||||
|
const r = randomNum(min, max);
|
||||||
|
const g = randomNum(min, max);
|
||||||
|
const b = randomNum(min, max);
|
||||||
|
return `rgb(${r},${g},${b})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function draw(dom: HTMLCanvasElement, width: number, height: number) {
|
||||||
|
let imgCode = '';
|
||||||
|
|
||||||
|
const NUMBER_STRING = '0123456789';
|
||||||
|
|
||||||
|
const ctx = dom.getContext('2d');
|
||||||
|
if (!ctx) return imgCode;
|
||||||
|
|
||||||
|
ctx.fillStyle = randomColor(180, 230);
|
||||||
|
ctx.fillRect(0, 0, width, height);
|
||||||
|
|
||||||
|
for (let i = 0; i < 4; i += 1) {
|
||||||
|
const text = NUMBER_STRING[randomNum(0, NUMBER_STRING.length)];
|
||||||
|
imgCode += text;
|
||||||
|
const fontSize = randomNum(18, 41);
|
||||||
|
const deg = randomNum(-30, 30);
|
||||||
|
ctx.font = `${fontSize}px Simhei`;
|
||||||
|
ctx.textBaseline = 'top';
|
||||||
|
ctx.fillStyle = randomColor(80, 150);
|
||||||
|
ctx.save();
|
||||||
|
ctx.translate(30 * i + 23, 15);
|
||||||
|
ctx.rotate((deg * Math.PI) / 180);
|
||||||
|
ctx.fillText(text, -15 + 5, -15);
|
||||||
|
ctx.restore();
|
||||||
|
}
|
||||||
|
for (let i = 0; i < 5; i += 1) {
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(randomNum(0, width), randomNum(0, height));
|
||||||
|
ctx.lineTo(randomNum(0, width), randomNum(0, height));
|
||||||
|
ctx.strokeStyle = randomColor(180, 230);
|
||||||
|
ctx.closePath();
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
for (let i = 0; i < 41; i += 1) {
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(randomNum(0, width), randomNum(0, height), 1, 0, 2 * Math.PI);
|
||||||
|
ctx.closePath();
|
||||||
|
ctx.fillStyle = randomColor(150, 200);
|
||||||
|
ctx.fill();
|
||||||
|
}
|
||||||
|
|
||||||
|
return imgCode;
|
||||||
|
}
|
23
src/layouts/common/global-header/components/github-site.vue
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<template>
|
||||||
|
<hover-container
|
||||||
|
tooltip-content="github"
|
||||||
|
class="w-40px h-full"
|
||||||
|
:inverted="theme.header.inverted"
|
||||||
|
@click="handleClickLink"
|
||||||
|
>
|
||||||
|
<icon-mdi-github class="text-20px" />
|
||||||
|
</hover-container>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { useThemeStore } from '@/store';
|
||||||
|
|
||||||
|
defineOptions({ name: 'GithubSite' });
|
||||||
|
|
||||||
|
const theme = useThemeStore();
|
||||||
|
function handleClickLink() {
|
||||||
|
window.open('https://github.com/honghuangdc/soybean-admin', '_blank');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped></style>
|
@@ -1,10 +1,23 @@
|
|||||||
import MenuCollapse from './menu-collapse.vue';
|
import MenuCollapse from './menu-collapse.vue';
|
||||||
import GlobalBreadcrumb from './global-breadcrumb.vue';
|
import GlobalBreadcrumb from './global-breadcrumb.vue';
|
||||||
import HeaderMenu from './header-menu.vue';
|
import HeaderMenu from './header-menu.vue';
|
||||||
|
import GithubSite from './github-site.vue';
|
||||||
import FullScreen from './full-screen.vue';
|
import FullScreen from './full-screen.vue';
|
||||||
import ThemeMode from './theme-mode.vue';
|
import ThemeMode from './theme-mode.vue';
|
||||||
import UserAvatar from './user-avatar.vue';
|
import UserAvatar from './user-avatar.vue';
|
||||||
|
import SystemMessage from './system-message.vue';
|
||||||
import SettingButton from './setting-button.vue';
|
import SettingButton from './setting-button.vue';
|
||||||
import ToggleLang from './toggle-lang.vue';
|
import ToggleLang from './toggle-lang.vue';
|
||||||
|
|
||||||
export { MenuCollapse, GlobalBreadcrumb, HeaderMenu, FullScreen, ThemeMode, UserAvatar, SettingButton, ToggleLang };
|
export {
|
||||||
|
MenuCollapse,
|
||||||
|
GlobalBreadcrumb,
|
||||||
|
HeaderMenu,
|
||||||
|
GithubSite,
|
||||||
|
FullScreen,
|
||||||
|
ThemeMode,
|
||||||
|
UserAvatar,
|
||||||
|
SystemMessage,
|
||||||
|
SettingButton,
|
||||||
|
ToggleLang
|
||||||
|
};
|
||||||
|
57
src/layouts/common/global-header/components/message-list.vue
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
<template>
|
||||||
|
<n-scrollbar class="max-h-360px">
|
||||||
|
<n-list>
|
||||||
|
<n-list-item
|
||||||
|
v-for="(item, index) in list"
|
||||||
|
:key="item.id"
|
||||||
|
class="hover:bg-#f6f6f6 dark:hover:bg-dark cursor-pointer"
|
||||||
|
@click="handleRead(index)"
|
||||||
|
>
|
||||||
|
<n-thing class="px-15px" :class="{ 'opacity-30': item.isRead }">
|
||||||
|
<template #avatar>
|
||||||
|
<n-avatar v-if="item.avatar" :src="item.avatar" />
|
||||||
|
<svg-icon v-else class="text-34px text-primary" :icon="item.icon" :local-icon="item.svgIcon" />
|
||||||
|
</template>
|
||||||
|
<template #header>
|
||||||
|
<n-ellipsis :line-clamp="1">
|
||||||
|
{{ item.title }}
|
||||||
|
<template #tooltip>
|
||||||
|
{{ item.title }}
|
||||||
|
</template>
|
||||||
|
</n-ellipsis>
|
||||||
|
</template>
|
||||||
|
<template v-if="item.tagTitle" #header-extra>
|
||||||
|
<n-tag v-bind="item.tagProps" size="small">{{ item.tagTitle }}</n-tag>
|
||||||
|
</template>
|
||||||
|
<template #description>
|
||||||
|
<n-ellipsis v-if="item.description" :line-clamp="2">
|
||||||
|
{{ item.description }}
|
||||||
|
</n-ellipsis>
|
||||||
|
<p>{{ item.date }}</p>
|
||||||
|
</template>
|
||||||
|
</n-thing>
|
||||||
|
</n-list-item>
|
||||||
|
</n-list>
|
||||||
|
</n-scrollbar>
|
||||||
|
</template>
|
||||||
|
<script lang="ts" setup>
|
||||||
|
defineOptions({ name: 'MessageList' });
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
list?: App.MessageList[];
|
||||||
|
}
|
||||||
|
|
||||||
|
withDefaults(defineProps<Props>(), {
|
||||||
|
list: () => []
|
||||||
|
});
|
||||||
|
|
||||||
|
interface Emits {
|
||||||
|
(e: 'read', val: number): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const emit = defineEmits<Emits>();
|
||||||
|
|
||||||
|
function handleRead(index: number) {
|
||||||
|
emit('read', index);
|
||||||
|
}
|
||||||
|
</script>
|
217
src/layouts/common/global-header/components/system-message.vue
Normal file
@@ -0,0 +1,217 @@
|
|||||||
|
<template>
|
||||||
|
<n-popover class="!p-0" trigger="click" placement="bottom">
|
||||||
|
<template #trigger>
|
||||||
|
<hover-container tooltip-content="消息通知" :inverted="theme.header.inverted" class="relative w-40px h-full">
|
||||||
|
<icon-clarity:notification-line class="text-18px" />
|
||||||
|
<n-badge
|
||||||
|
:value="count"
|
||||||
|
:max="99"
|
||||||
|
:class="[count < 10 ? '-right-2px' : '-right-10px']"
|
||||||
|
class="absolute top-10px"
|
||||||
|
/>
|
||||||
|
</hover-container>
|
||||||
|
</template>
|
||||||
|
<n-tabs
|
||||||
|
v-model:value="currentTab"
|
||||||
|
:class="[isMobile ? 'w-276px' : 'w-360px']"
|
||||||
|
type="line"
|
||||||
|
justify-content="space-evenly"
|
||||||
|
>
|
||||||
|
<n-tab-pane v-for="(item, index) in tabData" :key="item.key" :name="index">
|
||||||
|
<template #tab>
|
||||||
|
<div class="flex-x-center items-center" :class="[isMobile ? 'w-92px' : 'w-120px']">
|
||||||
|
<span class="mr-5px">{{ item.name }}</span>
|
||||||
|
<n-badge
|
||||||
|
v-bind="item.badgeProps"
|
||||||
|
:value="item.list.filter(message => !message.isRead).length"
|
||||||
|
:max="99"
|
||||||
|
show-zero
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<loading-empty-wrapper
|
||||||
|
class="h-360px"
|
||||||
|
:loading="loading"
|
||||||
|
:empty="item.list.length === 0"
|
||||||
|
placeholder-class="bg-$n-color transition-background-color duration-300 ease-in-out"
|
||||||
|
>
|
||||||
|
<message-list :list="item.list" @read="handleRead" />
|
||||||
|
</loading-empty-wrapper>
|
||||||
|
</n-tab-pane>
|
||||||
|
</n-tabs>
|
||||||
|
<div v-if="showAction" class="flex border-t border-$n-divider-color cursor-pointer">
|
||||||
|
<div class="flex-1 text-center py-10px" @click="handleClear">清空</div>
|
||||||
|
<div class="flex-1 text-center py-10px border-l border-$n-divider-color" @click="handleAllRead">全部已读</div>
|
||||||
|
<div class="flex-1 text-center py-10px border-l border-$n-divider-color" @click="handleLoadMore">查看更多</div>
|
||||||
|
</div>
|
||||||
|
</n-popover>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
import { useThemeStore } from '@/store';
|
||||||
|
import { useBasicLayout } from '@/composables';
|
||||||
|
import { useBoolean } from '@/hooks';
|
||||||
|
import MessageList from './message-list.vue';
|
||||||
|
|
||||||
|
defineOptions({ name: 'SystemMessage' });
|
||||||
|
|
||||||
|
const theme = useThemeStore();
|
||||||
|
const { isMobile } = useBasicLayout();
|
||||||
|
const { bool: loading, setBool: setLoading } = useBoolean();
|
||||||
|
|
||||||
|
const currentTab = ref(0);
|
||||||
|
|
||||||
|
const tabData = ref<App.MessageTab[]>([
|
||||||
|
{
|
||||||
|
key: 1,
|
||||||
|
name: '通知',
|
||||||
|
badgeProps: { type: 'warning' },
|
||||||
|
list: [
|
||||||
|
{ id: 1, icon: 'ri:message-3-line', title: '你收到了5条新消息', date: '2022-06-17' },
|
||||||
|
{ id: 4, icon: 'ri:message-3-line', title: 'Soybean Admin 1.0.0 版本正在筹备中', date: '2022-06-17' },
|
||||||
|
{ id: 2, icon: 'ri:message-3-line', title: 'Soybean Admin 0.9.6 版本发布了', date: '2022-06-16' },
|
||||||
|
{ id: 3, icon: 'ri:message-3-line', title: 'Soybean Admin 0.9.5 版本发布了', date: '2022-06-07' },
|
||||||
|
{
|
||||||
|
id: 5,
|
||||||
|
icon: 'ri:message-3-line',
|
||||||
|
title: '测试超长标题测试超长标题测试超长标题测试超长标题测试超长标题测试超长标题测试超长标题测试超长标题',
|
||||||
|
date: '2022-06-17'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 2,
|
||||||
|
name: '消息',
|
||||||
|
badgeProps: { type: 'error' },
|
||||||
|
list: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
title: '项目动态',
|
||||||
|
svgIcon: 'avatar',
|
||||||
|
description: 'Soybean 刚才把工作台页面随便写了一些,凑合能看了!',
|
||||||
|
date: '2021-11-07 22:45:32'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
title: '项目动态',
|
||||||
|
svgIcon: 'avatar',
|
||||||
|
description: 'Soybean 正在忙于为soybean-admin写项目说明文档!',
|
||||||
|
date: '2021-11-03 20:33:31'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
title: '项目动态',
|
||||||
|
svgIcon: 'avatar',
|
||||||
|
description: 'Soybean 准备为soybean-admin 1.0的发布做充分的准备工作!',
|
||||||
|
date: '2021-10-31 22:43:12'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
title: '项目动态',
|
||||||
|
svgIcon: 'avatar',
|
||||||
|
description: '@yanbowe 向soybean-admin提交了一个bug,多标签栏不会自适应。',
|
||||||
|
date: '2021-10-27 10:24:54'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 5,
|
||||||
|
title: '项目动态',
|
||||||
|
svgIcon: 'avatar',
|
||||||
|
description: 'Soybean 在2021年5月28日创建了开源项目soybean-admin!',
|
||||||
|
date: '2021-05-28 22:22:22'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 3,
|
||||||
|
name: '待办',
|
||||||
|
badgeProps: { type: 'info' },
|
||||||
|
list: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
icon: 'ri:calendar-todo-line',
|
||||||
|
title: '缓存主题配置',
|
||||||
|
description: '任务正在计划中',
|
||||||
|
date: '2022-06-17',
|
||||||
|
tagTitle: '未开始',
|
||||||
|
tagProps: { type: 'default' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
icon: 'ri:calendar-todo-line',
|
||||||
|
title: '添加锁屏组件、全局Iframe组件',
|
||||||
|
description: '任务正在计划中',
|
||||||
|
date: '2022-06-17',
|
||||||
|
tagTitle: '未开始',
|
||||||
|
tagProps: { type: 'default' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
icon: 'ri:calendar-todo-line',
|
||||||
|
title: '示例页面完善',
|
||||||
|
description: '任务正在计划中',
|
||||||
|
date: '2022-06-17',
|
||||||
|
tagTitle: '未开始',
|
||||||
|
tagProps: { type: 'default' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
icon: 'ri:calendar-todo-line',
|
||||||
|
title: '表单、表格示例',
|
||||||
|
description: '任务正在计划中',
|
||||||
|
date: '2022-06-17',
|
||||||
|
tagTitle: '未开始',
|
||||||
|
tagProps: { type: 'default' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 5,
|
||||||
|
icon: 'ri:calendar-todo-line',
|
||||||
|
title: '性能优化(优化递归函数)',
|
||||||
|
description: '任务正在计划中',
|
||||||
|
date: '2022-06-17',
|
||||||
|
tagTitle: '未开始',
|
||||||
|
tagProps: { type: 'default' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 6,
|
||||||
|
icon: 'ri:calendar-todo-line',
|
||||||
|
title: '精简版(新分支thin)',
|
||||||
|
description: '任务正在计划中',
|
||||||
|
date: '2022-06-17',
|
||||||
|
tagTitle: '未开始',
|
||||||
|
tagProps: { type: 'default' }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
const count = computed(() => {
|
||||||
|
return tabData.value.reduce((acc, cur) => {
|
||||||
|
return acc + cur.list.filter(item => !item.isRead).length;
|
||||||
|
}, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
const showAction = computed(() => tabData.value[currentTab.value].list.length > 0);
|
||||||
|
|
||||||
|
function handleRead(index: number) {
|
||||||
|
tabData.value[currentTab.value].list[index].isRead = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleAllRead() {
|
||||||
|
tabData.value[currentTab.value].list.forEach(item => Object.assign(item, { isRead: true }));
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleClear() {
|
||||||
|
tabData.value[currentTab.value].list = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleLoadMore() {
|
||||||
|
const { list } = tabData.value[currentTab.value];
|
||||||
|
setLoading(true);
|
||||||
|
setTimeout(() => {
|
||||||
|
list.push(...tabData.value[currentTab.value].list);
|
||||||
|
setLoading(false);
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style scoped></style>
|
@@ -7,9 +7,12 @@
|
|||||||
</div>
|
</div>
|
||||||
<header-menu v-else />
|
<header-menu v-else />
|
||||||
<div class="flex justify-end h-full">
|
<div class="flex justify-end h-full">
|
||||||
|
<global-search />
|
||||||
|
<github-site />
|
||||||
<full-screen />
|
<full-screen />
|
||||||
<theme-mode />
|
<theme-mode />
|
||||||
<toggle-lang />
|
<toggle-lang />
|
||||||
|
<system-message />
|
||||||
<setting-button v-if="showButton" />
|
<setting-button v-if="showButton" />
|
||||||
<user-avatar />
|
<user-avatar />
|
||||||
</div>
|
</div>
|
||||||
@@ -20,12 +23,15 @@
|
|||||||
import { useThemeStore } from '@/store';
|
import { useThemeStore } from '@/store';
|
||||||
import { useBasicLayout } from '@/composables';
|
import { useBasicLayout } from '@/composables';
|
||||||
import GlobalLogo from '../global-logo/index.vue';
|
import GlobalLogo from '../global-logo/index.vue';
|
||||||
|
import GlobalSearch from '../global-search/index.vue';
|
||||||
import {
|
import {
|
||||||
FullScreen,
|
FullScreen,
|
||||||
|
GithubSite,
|
||||||
GlobalBreadcrumb,
|
GlobalBreadcrumb,
|
||||||
HeaderMenu,
|
HeaderMenu,
|
||||||
MenuCollapse,
|
MenuCollapse,
|
||||||
SettingButton,
|
SettingButton,
|
||||||
|
SystemMessage,
|
||||||
ThemeMode,
|
ThemeMode,
|
||||||
UserAvatar,
|
UserAvatar,
|
||||||
ToggleLang
|
ToggleLang
|
||||||
|
3
src/layouts/common/global-search/components/index.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import SearchModal from './search-modal.vue';
|
||||||
|
|
||||||
|
export { SearchModal };
|
147
src/layouts/common/global-search/components/search-modal.vue
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
<template>
|
||||||
|
<n-modal
|
||||||
|
v-model:show="show"
|
||||||
|
:segmented="{ footer: 'soft' }"
|
||||||
|
:closable="false"
|
||||||
|
preset="card"
|
||||||
|
footer-style="padding: 0; margin: 0"
|
||||||
|
class="fixed left-0 right-0"
|
||||||
|
:class="[isMobile ? 'wh-full top-0px rounded-0' : 'w-630px top-50px']"
|
||||||
|
@after-leave="handleClose"
|
||||||
|
>
|
||||||
|
<n-input-group>
|
||||||
|
<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>
|
||||||
|
<n-button v-if="isMobile" type="primary" ghost @click="handleClose">取消</n-button>
|
||||||
|
</n-input-group>
|
||||||
|
|
||||||
|
<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 v-if="!isMobile" />
|
||||||
|
</template>
|
||||||
|
</n-modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed, nextTick, ref, shallowRef, watch } from 'vue';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
import { onKeyStroke, useDebounceFn } from '@vueuse/core';
|
||||||
|
import { useRouteStore } from '@/store';
|
||||||
|
import { useBasicLayout } from '@/composables';
|
||||||
|
import SearchResult from './search-result.vue';
|
||||||
|
import SearchFooter from './search-footer.vue';
|
||||||
|
|
||||||
|
defineOptions({ name: 'SearchModal' });
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
/** 弹窗显隐 */
|
||||||
|
value: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>();
|
||||||
|
|
||||||
|
interface Emits {
|
||||||
|
(e: 'update:value', val: boolean): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const emit = defineEmits<Emits>();
|
||||||
|
|
||||||
|
const { isMobile } = useBasicLayout();
|
||||||
|
const router = useRouter();
|
||||||
|
const routeStore = useRouteStore();
|
||||||
|
|
||||||
|
const keyword = ref('');
|
||||||
|
const activePath = ref('');
|
||||||
|
const resultOptions = shallowRef<AuthRoute.Route[]>([]);
|
||||||
|
const inputRef = ref<HTMLInputElement>();
|
||||||
|
|
||||||
|
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.searchMenus.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 routeItem = resultOptions.value.find(item => item.path === activePath.value);
|
||||||
|
if (routeItem?.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>
|
@@ -0,0 +1,64 @@
|
|||||||
|
<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)"
|
||||||
|
>
|
||||||
|
<svg-icon :icon="item.meta.icon" :local-icon="item.meta.localIcon" />
|
||||||
|
<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 { useThemeStore } from '@/store';
|
||||||
|
|
||||||
|
defineOptions({ name: 'SearchResult' });
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
value: string;
|
||||||
|
options: AuthRoute.Route[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>();
|
||||||
|
|
||||||
|
interface Emits {
|
||||||
|
(e: 'update:value', val: string): void;
|
||||||
|
(e: 'enter'): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const emit = defineEmits<Emits>();
|
||||||
|
|
||||||
|
const theme = useThemeStore();
|
||||||
|
|
||||||
|
const active = computed({
|
||||||
|
get() {
|
||||||
|
return props.value;
|
||||||
|
},
|
||||||
|
set(val: string) {
|
||||||
|
emit('update:value', val);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 鼠标移入 */
|
||||||
|
async function handleMouse(item: AuthRoute.Route) {
|
||||||
|
active.value = item.path;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleTo() {
|
||||||
|
emit('enter');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped></style>
|
30
src/layouts/common/global-search/index.vue
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<hover-container
|
||||||
|
class="w-40px h-full"
|
||||||
|
tooltip-content="搜索"
|
||||||
|
:inverted="theme.header.inverted"
|
||||||
|
@click="handleSearch"
|
||||||
|
>
|
||||||
|
<icon-uil-search class="text-20px" />
|
||||||
|
</hover-container>
|
||||||
|
<search-modal v-model:value="show" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { useThemeStore } from '@/store';
|
||||||
|
import { useBoolean } from '@/hooks';
|
||||||
|
import { SearchModal } from './components';
|
||||||
|
|
||||||
|
defineOptions({ name: 'GlobalSearch' });
|
||||||
|
|
||||||
|
const { bool: show, toggle } = useBoolean();
|
||||||
|
const theme = useThemeStore();
|
||||||
|
|
||||||
|
function handleSearch() {
|
||||||
|
toggle();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped></style>
|
@@ -1,5 +1,8 @@
|
|||||||
import 'uno.css';
|
import 'uno.css';
|
||||||
import '@soybeanjs/vue-materials/dist/style.css';
|
import '@soybeanjs/vue-materials/dist/style.css';
|
||||||
|
import 'swiper/css';
|
||||||
|
import 'swiper/css/navigation';
|
||||||
|
import 'swiper/css/pagination';
|
||||||
import 'virtual:svg-icons-register';
|
import 'virtual:svg-icons-register';
|
||||||
import '../styles/css/global.css';
|
import '../styles/css/global.css';
|
||||||
|
|
||||||
|
17
src/router/modules/about.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
const about: AuthRoute.Route = {
|
||||||
|
name: 'about',
|
||||||
|
path: '/about',
|
||||||
|
component: 'self',
|
||||||
|
meta: {
|
||||||
|
title: '关于',
|
||||||
|
i18nTitle: 'routes.about',
|
||||||
|
requiresAuth: true,
|
||||||
|
keepAlive: true,
|
||||||
|
singleLayout: 'basic',
|
||||||
|
permissions: ['super', 'admin', 'user'],
|
||||||
|
icon: 'fluent:book-information-24-regular',
|
||||||
|
order: 10
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default about;
|
38
src/router/modules/auth-demo.ts
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
const authDemo: AuthRoute.Route = {
|
||||||
|
name: 'auth-demo',
|
||||||
|
path: '/auth-demo',
|
||||||
|
component: 'basic',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
name: 'auth-demo_permission',
|
||||||
|
path: '/auth-demo/permission',
|
||||||
|
component: 'self',
|
||||||
|
meta: {
|
||||||
|
title: '权限切换',
|
||||||
|
i18nTitle: 'routes.auth-demo.permission',
|
||||||
|
requiresAuth: true,
|
||||||
|
icon: 'ic:round-construction'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'auth-demo_super',
|
||||||
|
path: '/auth-demo/super',
|
||||||
|
component: 'self',
|
||||||
|
meta: {
|
||||||
|
title: '超级管理员可见',
|
||||||
|
i18nTitle: 'routes.auth-demo.super',
|
||||||
|
requiresAuth: true,
|
||||||
|
permissions: ['super'],
|
||||||
|
icon: 'ic:round-supervisor-account'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
meta: {
|
||||||
|
title: '权限示例',
|
||||||
|
i18nTitle: 'routes.auth-demo._value',
|
||||||
|
icon: 'ic:baseline-security',
|
||||||
|
order: 5
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default authDemo;
|
48
src/router/modules/component.ts
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
const component: AuthRoute.Route = {
|
||||||
|
name: 'component',
|
||||||
|
path: '/component',
|
||||||
|
component: 'basic',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
name: 'component_button',
|
||||||
|
path: '/component/button',
|
||||||
|
component: 'self',
|
||||||
|
meta: {
|
||||||
|
title: '按钮',
|
||||||
|
i18nTitle: 'routes.component.button',
|
||||||
|
requiresAuth: true,
|
||||||
|
icon: 'mdi:button-cursor'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'component_card',
|
||||||
|
path: '/component/card',
|
||||||
|
component: 'self',
|
||||||
|
meta: {
|
||||||
|
title: '卡片',
|
||||||
|
i18nTitle: 'routes.component.card',
|
||||||
|
requiresAuth: true,
|
||||||
|
icon: 'mdi:card-outline'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'component_table',
|
||||||
|
path: '/component/table',
|
||||||
|
component: 'self',
|
||||||
|
meta: {
|
||||||
|
title: '表格',
|
||||||
|
i18nTitle: 'routes.component.table',
|
||||||
|
requiresAuth: true,
|
||||||
|
icon: 'mdi:table-large'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
meta: {
|
||||||
|
title: '组件示例',
|
||||||
|
i18nTitle: 'routes.component._value',
|
||||||
|
icon: 'cib:app-store',
|
||||||
|
order: 3
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default component;
|
37
src/router/modules/dashboard.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
const dashboard: AuthRoute.Route = {
|
||||||
|
name: 'dashboard',
|
||||||
|
path: '/dashboard',
|
||||||
|
component: 'basic',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
name: 'dashboard_analysis',
|
||||||
|
path: '/dashboard/analysis',
|
||||||
|
component: 'self',
|
||||||
|
meta: {
|
||||||
|
title: '分析页',
|
||||||
|
requiresAuth: true,
|
||||||
|
icon: 'icon-park-outline:analysis',
|
||||||
|
i18nTitle: 'routes.dashboard.analysis'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'dashboard_workbench',
|
||||||
|
path: '/dashboard/workbench',
|
||||||
|
component: 'self',
|
||||||
|
meta: {
|
||||||
|
title: '工作台',
|
||||||
|
requiresAuth: true,
|
||||||
|
icon: 'icon-park-outline:workbench',
|
||||||
|
i18nTitle: 'routes.dashboard.workbench'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
meta: {
|
||||||
|
title: '仪表盘',
|
||||||
|
icon: 'mdi:monitor-dashboard',
|
||||||
|
order: 1,
|
||||||
|
i18nTitle: 'routes.dashboard._value'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default dashboard;
|
70
src/router/modules/document.ts
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
const document: AuthRoute.Route = {
|
||||||
|
name: 'document',
|
||||||
|
path: '/document',
|
||||||
|
component: 'basic',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
name: 'document_vue',
|
||||||
|
path: '/document/vue',
|
||||||
|
component: 'self',
|
||||||
|
meta: {
|
||||||
|
title: 'vue文档',
|
||||||
|
i18nTitle: 'routes.document.vue',
|
||||||
|
requiresAuth: true,
|
||||||
|
icon: 'logos:vue'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'document_vite',
|
||||||
|
path: '/document/vite',
|
||||||
|
component: 'self',
|
||||||
|
meta: {
|
||||||
|
title: 'vite文档',
|
||||||
|
i18nTitle: 'routes.document.vite',
|
||||||
|
requiresAuth: true,
|
||||||
|
icon: 'logos:vitejs'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'document_naive',
|
||||||
|
path: '/document/naive',
|
||||||
|
component: 'self',
|
||||||
|
meta: {
|
||||||
|
title: 'naive文档',
|
||||||
|
i18nTitle: 'routes.document.naive',
|
||||||
|
requiresAuth: true,
|
||||||
|
icon: 'logos:naiveui'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'document_project',
|
||||||
|
path: '/document/project',
|
||||||
|
component: 'self',
|
||||||
|
meta: {
|
||||||
|
title: '项目文档',
|
||||||
|
i18nTitle: 'routes.document.project',
|
||||||
|
requiresAuth: true,
|
||||||
|
localIcon: 'logo'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'document_project-link',
|
||||||
|
path: '/document/project-link',
|
||||||
|
meta: {
|
||||||
|
title: '项目文档(外链)',
|
||||||
|
i18nTitle: 'routes.document.project-link',
|
||||||
|
requiresAuth: true,
|
||||||
|
localIcon: 'logo',
|
||||||
|
href: 'https://admin-docs.soybeanjs.cn/'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
meta: {
|
||||||
|
title: '文档',
|
||||||
|
i18nTitle: 'routes.document._value',
|
||||||
|
icon: 'mdi:file-document-multiple-outline',
|
||||||
|
order: 2
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default document;
|
48
src/router/modules/exception.ts
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
const exception: AuthRoute.Route = {
|
||||||
|
name: 'exception',
|
||||||
|
path: '/exception',
|
||||||
|
component: 'basic',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
name: 'exception_403',
|
||||||
|
path: '/exception/403',
|
||||||
|
component: 'self',
|
||||||
|
meta: {
|
||||||
|
title: '异常页403',
|
||||||
|
i18nTitle: 'routes.exception.403',
|
||||||
|
requiresAuth: true,
|
||||||
|
icon: 'ic:baseline-block'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'exception_404',
|
||||||
|
path: '/exception/404',
|
||||||
|
component: 'self',
|
||||||
|
meta: {
|
||||||
|
title: '异常页404',
|
||||||
|
i18nTitle: 'routes.exception.404',
|
||||||
|
requiresAuth: true,
|
||||||
|
icon: 'ic:baseline-web-asset-off'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'exception_500',
|
||||||
|
path: '/exception/500',
|
||||||
|
component: 'self',
|
||||||
|
meta: {
|
||||||
|
title: '异常页500',
|
||||||
|
i18nTitle: 'routes.exception.500',
|
||||||
|
requiresAuth: true,
|
||||||
|
icon: 'ic:baseline-wifi-off'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
meta: {
|
||||||
|
i18nTitle: 'routes.exception._value',
|
||||||
|
title: '异常页',
|
||||||
|
icon: 'ant-design:exception-outlined',
|
||||||
|
order: 7
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default exception;
|
51
src/router/modules/function.ts
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
const functionRoute: AuthRoute.Route = {
|
||||||
|
name: 'function',
|
||||||
|
path: '/function',
|
||||||
|
component: 'basic',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
name: 'function_tab',
|
||||||
|
path: '/function/tab',
|
||||||
|
component: 'self',
|
||||||
|
meta: {
|
||||||
|
title: 'Tab',
|
||||||
|
i18nTitle: 'routes.function.tab',
|
||||||
|
requiresAuth: true,
|
||||||
|
icon: 'ic:round-tab'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'function_tab-detail',
|
||||||
|
path: '/function/tab-detail',
|
||||||
|
component: 'self',
|
||||||
|
meta: {
|
||||||
|
title: 'Tab Detail',
|
||||||
|
requiresAuth: true,
|
||||||
|
hide: true,
|
||||||
|
activeMenu: 'function_tab',
|
||||||
|
icon: 'ic:round-tab'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'function_tab-multi-detail',
|
||||||
|
path: '/function/tab-multi-detail',
|
||||||
|
component: 'self',
|
||||||
|
meta: {
|
||||||
|
title: 'Tab Multi Detail',
|
||||||
|
requiresAuth: true,
|
||||||
|
hide: true,
|
||||||
|
multiTab: true,
|
||||||
|
activeMenu: 'function_tab',
|
||||||
|
icon: 'ic:round-tab'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
meta: {
|
||||||
|
title: '功能',
|
||||||
|
i18nTitle: 'routes.function._value',
|
||||||
|
icon: 'icon-park-outline:all-application',
|
||||||
|
order: 6
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default functionRoute;
|
63
src/router/modules/management.ts
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
const management: AuthRoute.Route = {
|
||||||
|
name: 'management',
|
||||||
|
path: '/management',
|
||||||
|
component: 'basic',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
name: 'management_auth',
|
||||||
|
path: '/management/auth',
|
||||||
|
component: 'self',
|
||||||
|
meta: {
|
||||||
|
title: '权限管理',
|
||||||
|
i18nTitle: 'routes.management.auth',
|
||||||
|
requiresAuth: true,
|
||||||
|
keepAlive: true,
|
||||||
|
icon: 'ic:baseline-security'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'management_role',
|
||||||
|
path: '/management/role',
|
||||||
|
component: 'self',
|
||||||
|
meta: {
|
||||||
|
title: '角色管理',
|
||||||
|
i18nTitle: 'routes.management.role',
|
||||||
|
requiresAuth: true,
|
||||||
|
keepAlive: true,
|
||||||
|
icon: 'carbon:user-role'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'management_user',
|
||||||
|
path: '/management/user',
|
||||||
|
component: 'self',
|
||||||
|
meta: {
|
||||||
|
title: '用户管理',
|
||||||
|
i18nTitle: 'routes.management.user',
|
||||||
|
requiresAuth: true,
|
||||||
|
keepAlive: true,
|
||||||
|
icon: 'ic:round-manage-accounts'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'management_route',
|
||||||
|
path: '/management/route',
|
||||||
|
component: 'self',
|
||||||
|
meta: {
|
||||||
|
title: '路由管理',
|
||||||
|
i18nTitle: 'routes.management.route',
|
||||||
|
requiresAuth: true,
|
||||||
|
keepAlive: true,
|
||||||
|
icon: 'material-symbols:route'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
meta: {
|
||||||
|
title: '系统管理',
|
||||||
|
i18nTitle: 'routes.management._value',
|
||||||
|
icon: 'carbon:cloud-service-management',
|
||||||
|
order: 9
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default management;
|
149
src/router/modules/plugin.ts
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
const plugin: AuthRoute.Route = {
|
||||||
|
name: 'plugin',
|
||||||
|
path: '/plugin',
|
||||||
|
component: 'basic',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
name: 'plugin_charts',
|
||||||
|
path: '/plugin/charts',
|
||||||
|
component: 'multi',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
name: 'plugin_charts_echarts',
|
||||||
|
path: '/plugin/charts/echarts',
|
||||||
|
component: 'self',
|
||||||
|
meta: {
|
||||||
|
title: 'ECharts',
|
||||||
|
i18nTitle: 'routes.plugin.charts.echarts',
|
||||||
|
requiresAuth: true,
|
||||||
|
icon: 'simple-icons:apacheecharts'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'plugin_charts_antv',
|
||||||
|
path: '/plugin/charts/antv',
|
||||||
|
component: 'self',
|
||||||
|
meta: {
|
||||||
|
title: 'AntV',
|
||||||
|
i18nTitle: 'routes.plugin.charts.antv',
|
||||||
|
requiresAuth: true,
|
||||||
|
icon: 'simple-icons:antdesign'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
meta: {
|
||||||
|
title: '图表',
|
||||||
|
i18nTitle: 'routes.plugin.charts._value',
|
||||||
|
icon: 'mdi:chart-areaspline'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'plugin_map',
|
||||||
|
path: '/plugin/map',
|
||||||
|
component: 'self',
|
||||||
|
meta: {
|
||||||
|
title: '地图',
|
||||||
|
i18nTitle: 'routes.plugin.map',
|
||||||
|
requiresAuth: true,
|
||||||
|
icon: 'mdi:map'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'plugin_video',
|
||||||
|
path: '/plugin/video',
|
||||||
|
component: 'self',
|
||||||
|
meta: {
|
||||||
|
title: '视频',
|
||||||
|
i18nTitle: 'routes.plugin.video',
|
||||||
|
requiresAuth: true,
|
||||||
|
icon: 'mdi:video'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'plugin_editor',
|
||||||
|
path: '/plugin/editor',
|
||||||
|
component: 'multi',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
name: 'plugin_editor_quill',
|
||||||
|
path: '/plugin/editor/quill',
|
||||||
|
component: 'self',
|
||||||
|
meta: {
|
||||||
|
title: '富文本编辑器',
|
||||||
|
i18nTitle: 'routes.plugin.editor.quill',
|
||||||
|
requiresAuth: true,
|
||||||
|
icon: 'mdi:file-document-edit-outline'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'plugin_editor_markdown',
|
||||||
|
path: '/plugin/editor/markdown',
|
||||||
|
component: 'self',
|
||||||
|
meta: {
|
||||||
|
title: 'markdown编辑器',
|
||||||
|
i18nTitle: 'routes.plugin.editor.markdown',
|
||||||
|
requiresAuth: true,
|
||||||
|
icon: 'ri:markdown-line'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
meta: {
|
||||||
|
title: '编辑器',
|
||||||
|
i18nTitle: 'routes.plugin.editor._value',
|
||||||
|
icon: 'icon-park-outline:editor'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'plugin_swiper',
|
||||||
|
path: '/plugin/swiper',
|
||||||
|
component: 'self',
|
||||||
|
meta: {
|
||||||
|
title: 'Swiper插件',
|
||||||
|
i18nTitle: 'routes.plugin.swiper',
|
||||||
|
requiresAuth: true,
|
||||||
|
icon: 'simple-icons:swiper'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'plugin_copy',
|
||||||
|
path: '/plugin/copy',
|
||||||
|
component: 'self',
|
||||||
|
meta: {
|
||||||
|
title: '剪贴板',
|
||||||
|
i18nTitle: 'routes.plugin.copy',
|
||||||
|
requiresAuth: true,
|
||||||
|
icon: 'mdi:clipboard-outline'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'plugin_icon',
|
||||||
|
path: '/plugin/icon',
|
||||||
|
component: 'self',
|
||||||
|
meta: {
|
||||||
|
title: '图标',
|
||||||
|
i18nTitle: 'routes.plugin.icon',
|
||||||
|
requiresAuth: true,
|
||||||
|
localIcon: 'custom-icon'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'plugin_print',
|
||||||
|
path: '/plugin/print',
|
||||||
|
component: 'self',
|
||||||
|
meta: {
|
||||||
|
title: '打印',
|
||||||
|
i18nTitle: 'routes.plugin.print',
|
||||||
|
requiresAuth: true,
|
||||||
|
icon: 'mdi:printer'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
meta: {
|
||||||
|
title: '插件示例',
|
||||||
|
i18nTitle: 'routes.plugin._value',
|
||||||
|
icon: 'clarity:plugin-line',
|
||||||
|
order: 4
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default plugin;
|
@@ -1 +1,2 @@
|
|||||||
export * from './auth';
|
export * from './auth';
|
||||||
|
export * from './management';
|
||||||
|
13
src/service/api/management.adapter.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
export function adapterOfFetchUserList(data: ApiUserManagement.User[] | null): UserManagement.User[] {
|
||||||
|
if (!data) return [];
|
||||||
|
|
||||||
|
return data.map((item, index) => {
|
||||||
|
const user: UserManagement.User = {
|
||||||
|
index: index + 1,
|
||||||
|
key: item.id,
|
||||||
|
...item
|
||||||
|
};
|
||||||
|
|
||||||
|
return user;
|
||||||
|
});
|
||||||
|
}
|
9
src/service/api/management.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { adapter } from '@/utils';
|
||||||
|
import { mockRequest } from '../request';
|
||||||
|
import { adapterOfFetchUserList } from './management.adapter';
|
||||||
|
|
||||||
|
/** 获取用户列表 */
|
||||||
|
export const fetchUserList = async () => {
|
||||||
|
const data = await mockRequest.post<ApiUserManagement.User[] | null>('/getAllUserList');
|
||||||
|
return adapter(adapterOfFetchUserList, data);
|
||||||
|
};
|
@@ -1,5 +1,6 @@
|
|||||||
import { nextTick } from 'vue';
|
import { nextTick } from 'vue';
|
||||||
import { defineStore } from 'pinia';
|
import { defineStore } from 'pinia';
|
||||||
|
import type { Socket } from 'socket.io-client';
|
||||||
import { LAYOUT_SCROLL_EL_ID } from '@soybeanjs/vue-materials';
|
import { LAYOUT_SCROLL_EL_ID } from '@soybeanjs/vue-materials';
|
||||||
|
|
||||||
interface AppState {
|
interface AppState {
|
||||||
@@ -17,6 +18,8 @@ interface AppState {
|
|||||||
siderCollapse: boolean;
|
siderCollapse: boolean;
|
||||||
/** vertical-mix模式下 侧边栏的固定状态 */
|
/** vertical-mix模式下 侧边栏的固定状态 */
|
||||||
mixSiderFixed: boolean;
|
mixSiderFixed: boolean;
|
||||||
|
/** socket.io 实例 */
|
||||||
|
socket: Socket | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useAppStore = defineStore('app-store', {
|
export const useAppStore = defineStore('app-store', {
|
||||||
@@ -27,7 +30,8 @@ export const useAppStore = defineStore('app-store', {
|
|||||||
reloadFlag: true,
|
reloadFlag: true,
|
||||||
settingDrawerVisible: false,
|
settingDrawerVisible: false,
|
||||||
siderCollapse: false,
|
siderCollapse: false,
|
||||||
mixSiderFixed: false
|
mixSiderFixed: false,
|
||||||
|
socket: null
|
||||||
}),
|
}),
|
||||||
actions: {
|
actions: {
|
||||||
/**
|
/**
|
||||||
@@ -97,6 +101,10 @@ export const useAppStore = defineStore('app-store', {
|
|||||||
/** 设置主体内容全屏 */
|
/** 设置主体内容全屏 */
|
||||||
setContentFull(full: boolean) {
|
setContentFull(full: boolean) {
|
||||||
this.contentFull = full;
|
this.contentFull = full;
|
||||||
|
},
|
||||||
|
/** 设置socket实例 */
|
||||||
|
setSocket<T extends Socket = Socket>(socket: T) {
|
||||||
|
this.socket = socket;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
29
src/typings/api.d.ts
vendored
@@ -21,3 +21,32 @@ declare namespace ApiRoute {
|
|||||||
home: AuthRoute.AllRouteKey;
|
home: AuthRoute.AllRouteKey;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
declare namespace ApiUserManagement {
|
||||||
|
interface User {
|
||||||
|
/** 用户id */
|
||||||
|
id: string;
|
||||||
|
/** 用户名 */
|
||||||
|
userName: string | null;
|
||||||
|
/** 用户年龄 */
|
||||||
|
age: number | null;
|
||||||
|
/**
|
||||||
|
* 用户性别
|
||||||
|
* - 0: 女
|
||||||
|
* - 1: 男
|
||||||
|
*/
|
||||||
|
gender: '0' | '1' | null;
|
||||||
|
/** 用户手机号码 */
|
||||||
|
phone: string;
|
||||||
|
/** 用户邮箱 */
|
||||||
|
email: string | null;
|
||||||
|
/**
|
||||||
|
* 用户状态
|
||||||
|
* - 1: 启用
|
||||||
|
* - 2: 禁用
|
||||||
|
* - 3: 冻结
|
||||||
|
* - 4: 软删除
|
||||||
|
*/
|
||||||
|
userStatus: '1' | '2' | '3' | '4' | null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
25
src/typings/business.d.ts
vendored
@@ -18,3 +18,28 @@ declare namespace Auth {
|
|||||||
userRole: RoleType;
|
userRole: RoleType;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
declare namespace UserManagement {
|
||||||
|
interface User extends ApiUserManagement.User {
|
||||||
|
/** 序号 */
|
||||||
|
index: number;
|
||||||
|
/** 表格的key(id) */
|
||||||
|
key: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户性别
|
||||||
|
* - 0: 女
|
||||||
|
* - 1: 男
|
||||||
|
*/
|
||||||
|
type GenderKey = NonNullable<User['gender']>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户状态
|
||||||
|
* - 1: 启用
|
||||||
|
* - 2: 禁用
|
||||||
|
* - 3: 冻结
|
||||||
|
* - 4: 软删除
|
||||||
|
*/
|
||||||
|
type UserStatusKey = NonNullable<User['userStatus']>;
|
||||||
|
}
|
||||||
|
3
src/typings/global.d.ts
vendored
@@ -24,3 +24,6 @@ declare namespace Common {
|
|||||||
/** 选项数据 */
|
/** 选项数据 */
|
||||||
type OptionWithKey<K> = { value: K; label: string };
|
type OptionWithKey<K> = { value: K; label: string };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 构建时间 */
|
||||||
|
declare const PROJECT_BUILD_TIME: string;
|
||||||
|
3
src/typings/naive-ui.d.ts
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
declare namespace NaiveUI {
|
||||||
|
type ThemeColor = 'default' | 'error' | 'primary' | 'info' | 'success' | 'warning';
|
||||||
|
}
|
9
src/typings/package.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;
|
80
src/typings/page-route.d.ts
vendored
@@ -22,11 +22,55 @@ declare namespace PageRoute {
|
|||||||
| 'constant-page'
|
| 'constant-page'
|
||||||
| 'login'
|
| 'login'
|
||||||
| 'not-found'
|
| 'not-found'
|
||||||
|
| 'about'
|
||||||
|
| 'auth-demo'
|
||||||
|
| 'auth-demo_permission'
|
||||||
|
| 'auth-demo_super'
|
||||||
|
| 'component'
|
||||||
|
| 'component_button'
|
||||||
|
| 'component_card'
|
||||||
|
| 'component_table'
|
||||||
|
| 'dashboard'
|
||||||
|
| 'dashboard_analysis'
|
||||||
|
| 'dashboard_workbench'
|
||||||
|
| 'document'
|
||||||
|
| 'document_naive'
|
||||||
|
| 'document_project-link'
|
||||||
|
| 'document_project'
|
||||||
|
| 'document_vite'
|
||||||
|
| 'document_vue'
|
||||||
|
| 'exception'
|
||||||
|
| 'exception_403'
|
||||||
|
| 'exception_404'
|
||||||
|
| 'exception_500'
|
||||||
|
| 'function'
|
||||||
|
| 'function_tab-detail'
|
||||||
|
| 'function_tab-multi-detail'
|
||||||
|
| 'function_tab'
|
||||||
|
| 'function_websocket'
|
||||||
|
| 'management'
|
||||||
|
| 'management_auth'
|
||||||
|
| 'management_role'
|
||||||
|
| 'management_route'
|
||||||
|
| 'management_user'
|
||||||
| 'multi-menu'
|
| 'multi-menu'
|
||||||
| 'multi-menu_first'
|
| 'multi-menu_first'
|
||||||
| 'multi-menu_first_second-new'
|
| 'multi-menu_first_second-new'
|
||||||
| 'multi-menu_first_second-new_third'
|
| 'multi-menu_first_second-new_third'
|
||||||
| 'multi-menu_first_second';
|
| 'multi-menu_first_second'
|
||||||
|
| 'plugin'
|
||||||
|
| 'plugin_charts'
|
||||||
|
| 'plugin_charts_antv'
|
||||||
|
| 'plugin_charts_echarts'
|
||||||
|
| 'plugin_copy'
|
||||||
|
| 'plugin_editor'
|
||||||
|
| 'plugin_editor_markdown'
|
||||||
|
| 'plugin_editor_quill'
|
||||||
|
| 'plugin_icon'
|
||||||
|
| 'plugin_map'
|
||||||
|
| 'plugin_print'
|
||||||
|
| 'plugin_swiper'
|
||||||
|
| 'plugin_video';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* last degree route key, which has the page file
|
* last degree route key, which has the page file
|
||||||
@@ -40,7 +84,41 @@ declare namespace PageRoute {
|
|||||||
| 'constant-page'
|
| 'constant-page'
|
||||||
| 'login'
|
| 'login'
|
||||||
| 'not-found'
|
| 'not-found'
|
||||||
|
| 'about'
|
||||||
|
| 'auth-demo_permission'
|
||||||
|
| 'auth-demo_super'
|
||||||
|
| 'component_button'
|
||||||
|
| 'component_card'
|
||||||
|
| 'component_table'
|
||||||
|
| 'dashboard_analysis'
|
||||||
|
| 'dashboard_workbench'
|
||||||
|
| 'document_naive'
|
||||||
|
| 'document_project-link'
|
||||||
|
| 'document_project'
|
||||||
|
| 'document_vite'
|
||||||
|
| 'document_vue'
|
||||||
|
| 'exception_403'
|
||||||
|
| 'exception_404'
|
||||||
|
| 'exception_500'
|
||||||
|
| 'function_tab-detail'
|
||||||
|
| 'function_tab-multi-detail'
|
||||||
|
| 'function_tab'
|
||||||
|
| 'function_websocket'
|
||||||
|
| 'management_auth'
|
||||||
|
| 'management_role'
|
||||||
|
| 'management_route'
|
||||||
|
| 'management_user'
|
||||||
| 'multi-menu_first_second-new_third'
|
| 'multi-menu_first_second-new_third'
|
||||||
| 'multi-menu_first_second'
|
| 'multi-menu_first_second'
|
||||||
|
| 'plugin_charts_antv'
|
||||||
|
| 'plugin_charts_echarts'
|
||||||
|
| 'plugin_copy'
|
||||||
|
| 'plugin_editor_markdown'
|
||||||
|
| 'plugin_editor_quill'
|
||||||
|
| 'plugin_icon'
|
||||||
|
| 'plugin_map'
|
||||||
|
| 'plugin_print'
|
||||||
|
| 'plugin_swiper'
|
||||||
|
| 'plugin_video'
|
||||||
>;
|
>;
|
||||||
}
|
}
|
||||||
|
19
src/views/about/components/dev-dependency.vue
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<template>
|
||||||
|
<n-card title="开发环境依赖" :bordered="false" size="small" class="rounded-8px shadow-sm">
|
||||||
|
<n-descriptions label-placement="left" bordered size="small">
|
||||||
|
<n-descriptions-item v-for="item in devDependencies" :key="item.name" :label="item.name">
|
||||||
|
{{ item.version }}
|
||||||
|
</n-descriptions-item>
|
||||||
|
</n-descriptions>
|
||||||
|
</n-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { pkgJson } from './model';
|
||||||
|
|
||||||
|
defineOptions({ name: 'DevDependency' });
|
||||||
|
|
||||||
|
const { devDependencies } = pkgJson;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped></style>
|
6
src/views/about/components/index.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import ProjectIntroduction from './project-introduction.vue';
|
||||||
|
import ProjectInfo from './project-info.vue';
|
||||||
|
import ProDependency from './pro-dependency.vue';
|
||||||
|
import DevDependency from './dev-dependency.vue';
|
||||||
|
|
||||||
|
export { ProjectIntroduction, ProjectInfo, ProDependency, DevDependency };
|
39
src/views/about/components/model.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import pkg from '~/package.json';
|
||||||
|
|
||||||
|
/** npm依赖包版本信息 */
|
||||||
|
export interface PkgVersionInfo {
|
||||||
|
name: string;
|
||||||
|
version: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Package {
|
||||||
|
name: string;
|
||||||
|
version: string;
|
||||||
|
dependencies: Record<string, string>;
|
||||||
|
devDependencies: Record<string, string>;
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PkgJson {
|
||||||
|
name: string;
|
||||||
|
version: string;
|
||||||
|
dependencies: PkgVersionInfo[];
|
||||||
|
devDependencies: PkgVersionInfo[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const pkgWithType = pkg as Package;
|
||||||
|
|
||||||
|
function transformVersionData(tuple: [string, string]): PkgVersionInfo {
|
||||||
|
const [name, version] = tuple;
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
version
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const pkgJson: PkgJson = {
|
||||||
|
name: pkgWithType.name,
|
||||||
|
version: pkgWithType.version,
|
||||||
|
dependencies: Object.entries(pkgWithType.dependencies).map(item => transformVersionData(item)),
|
||||||
|
devDependencies: Object.entries(pkgWithType.devDependencies).map(item => transformVersionData(item))
|
||||||
|
};
|
19
src/views/about/components/pro-dependency.vue
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<template>
|
||||||
|
<n-card title="生产环境依赖" :bordered="false" size="small" class="rounded-8px shadow-sm">
|
||||||
|
<n-descriptions label-placement="left" bordered size="small">
|
||||||
|
<n-descriptions-item v-for="item in dependencies" :key="item.name" :label="item.name">
|
||||||
|
{{ item.version }}
|
||||||
|
</n-descriptions-item>
|
||||||
|
</n-descriptions>
|
||||||
|
</n-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { pkgJson } from './model';
|
||||||
|
|
||||||
|
defineOptions({ name: 'ProDependency' });
|
||||||
|
|
||||||
|
const { dependencies } = pkgJson;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped></style>
|
29
src/views/about/components/project-info.vue
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
<template>
|
||||||
|
<n-card title="项目信息" :bordered="false" size="small" class="rounded-8px shadow-sm">
|
||||||
|
<n-descriptions label-placement="left" bordered size="small" :column="2">
|
||||||
|
<n-descriptions-item label="版本">
|
||||||
|
<n-tag type="primary">{{ version }}</n-tag>
|
||||||
|
</n-descriptions-item>
|
||||||
|
<n-descriptions-item label="最后编译时间">
|
||||||
|
<n-tag type="primary">{{ latestBuildTime }}</n-tag>
|
||||||
|
</n-descriptions-item>
|
||||||
|
<n-descriptions-item label="Github地址">
|
||||||
|
<a class="text-primary" href="https://github.com/honghuangdc/soybean-admin" target="_blank">Github地址</a>
|
||||||
|
</n-descriptions-item>
|
||||||
|
<n-descriptions-item label="预览地址">
|
||||||
|
<a class="text-primary" href="https://admin.soybeanjs.cn" target="_blank">预览地址</a>
|
||||||
|
</n-descriptions-item>
|
||||||
|
</n-descriptions>
|
||||||
|
</n-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { pkgJson } from './model';
|
||||||
|
|
||||||
|
defineOptions({ name: 'ProjectInfo' });
|
||||||
|
|
||||||
|
const { version } = pkgJson;
|
||||||
|
const latestBuildTime = PROJECT_BUILD_TIME;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped></style>
|
14
src/views/about/components/project-introduction.vue
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<template>
|
||||||
|
<n-card title="关于" :bordered="false" size="large" class="rounded-8px shadow-sm">
|
||||||
|
<p class="leading-24px">
|
||||||
|
Soybean Admin 是一个基于 Vue3、Vite、Naive UI、TypeScript
|
||||||
|
的中后台解决方案,它使用了最新的前端技术栈,并提炼了典型的业务模型,页面,包括二次封装组件、动态菜单、权限校验、粒子化权限控制等功能,它可以帮助你快速搭建企业级中后台项目,相信不管是从新技术使用还是其他方面,都能帮助到你。
|
||||||
|
</p>
|
||||||
|
</n-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
defineOptions({ name: 'ProjectIntroduction' });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped></style>
|
23
src/views/about/index.vue
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<template>
|
||||||
|
<n-space :vertical="true" :size="16">
|
||||||
|
<project-introduction />
|
||||||
|
<project-info />
|
||||||
|
<pro-dependency />
|
||||||
|
<dev-dependency />
|
||||||
|
</n-space>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { onActivated, onMounted } from 'vue';
|
||||||
|
import { DevDependency, ProDependency, ProjectInfo, ProjectIntroduction } from './components';
|
||||||
|
|
||||||
|
onActivated(() => {
|
||||||
|
console.log('about page activated');
|
||||||
|
});
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
console.log('about page mounted');
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped></style>
|
55
src/views/auth-demo/permission/index.vue
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
<template>
|
||||||
|
<div class="h-full">
|
||||||
|
<n-card title="权限切换" :bordered="false" class="h-full rounded-8px shadow-sm">
|
||||||
|
<div class="pb-12px">
|
||||||
|
<n-gradient-text type="primary" :size="20">当前用户的权限:{{ auth.userInfo.userRole }}</n-gradient-text>
|
||||||
|
</div>
|
||||||
|
<n-select
|
||||||
|
:value="auth.userInfo.userRole"
|
||||||
|
class="w-120px"
|
||||||
|
size="small"
|
||||||
|
:options="options"
|
||||||
|
@update:value="auth.updateUserRole"
|
||||||
|
/>
|
||||||
|
<div class="py-12px">
|
||||||
|
<n-gradient-text type="primary" :size="20">权限指令 v-permission</n-gradient-text>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<n-button v-permission="'super'" class="mr-12px">super可见</n-button>
|
||||||
|
<n-button v-permission="'admin'" class="mr-12px">admin可见</n-button>
|
||||||
|
<n-button v-permission="['admin', 'user']">admin和test可见</n-button>
|
||||||
|
</div>
|
||||||
|
<div class="py-12px">
|
||||||
|
<n-gradient-text type="primary" :size="20">权限函数 hasPermission</n-gradient-text>
|
||||||
|
</div>
|
||||||
|
<n-space>
|
||||||
|
<n-button v-if="hasPermission('super')">super可见</n-button>
|
||||||
|
<n-button v-if="hasPermission('admin')">admin可见</n-button>
|
||||||
|
<n-button v-if="hasPermission(['admin', 'user'])">admin和user可见</n-button>
|
||||||
|
</n-space>
|
||||||
|
</n-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { watch } from 'vue';
|
||||||
|
import type { SelectOption } from 'naive-ui';
|
||||||
|
import { userRoleOptions } from '@/constants';
|
||||||
|
import { useAppStore, useAuthStore } from '@/store';
|
||||||
|
import { usePermission } from '@/composables';
|
||||||
|
|
||||||
|
const app = useAppStore();
|
||||||
|
const auth = useAuthStore();
|
||||||
|
const { hasPermission } = usePermission();
|
||||||
|
|
||||||
|
const options: SelectOption[] = userRoleOptions;
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => auth.userInfo.userRole,
|
||||||
|
async () => {
|
||||||
|
app.reloadPage();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped></style>
|
9
src/views/auth-demo/super/index.vue
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<template>
|
||||||
|
<div class="h-full">
|
||||||
|
<n-card title="当前页面只有super才能看到" :bordered="false" class="h-full rounded-8px shadow-sm"></n-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts"></script>
|
||||||
|
|
||||||
|
<style scoped></style>
|
575
src/views/component/button/index.vue
Normal file
@@ -0,0 +1,575 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<n-card title="按钮" :bordered="false" class="h-full rounded-8px shadow-sm">
|
||||||
|
<n-grid cols="s:1 m:2" responsive="screen" :x-gap="16" :y-gap="16">
|
||||||
|
<n-grid-item v-for="item in buttonExample" :key="item.id">
|
||||||
|
<n-card :title="item.label" class="min-h-180px">
|
||||||
|
<p v-if="item.desc" class="pb-16px">{{ item.desc }}</p>
|
||||||
|
<n-space>
|
||||||
|
<n-button
|
||||||
|
v-for="button in item.buttons"
|
||||||
|
:key="button.id"
|
||||||
|
v-bind="button.props"
|
||||||
|
:style="`--icon-margin: ${button.props.circle ? 0 : 6}px`"
|
||||||
|
>
|
||||||
|
<template v-if="button.icon" #icon>
|
||||||
|
<svg-icon :icon="button.icon" />
|
||||||
|
</template>
|
||||||
|
{{ button.label }}
|
||||||
|
</n-button>
|
||||||
|
</n-space>
|
||||||
|
</n-card>
|
||||||
|
</n-grid-item>
|
||||||
|
<n-grid-item class="h-180px">
|
||||||
|
<n-card title="加载中" class="h-full">
|
||||||
|
<p class="pb-16px">按钮有加载状态。</p>
|
||||||
|
<n-space>
|
||||||
|
<n-button :loading="loading" type="primary" @click="startLoading">开始加载</n-button>
|
||||||
|
<n-button @click="endLoading">取消加载</n-button>
|
||||||
|
</n-space>
|
||||||
|
</n-card>
|
||||||
|
</n-grid-item>
|
||||||
|
</n-grid>
|
||||||
|
</n-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { ButtonProps } from 'naive-ui';
|
||||||
|
import { useLoading } from '@/hooks';
|
||||||
|
|
||||||
|
interface ButtonDetail {
|
||||||
|
id: number;
|
||||||
|
props: ButtonProps & { href?: string; target?: string };
|
||||||
|
label?: string;
|
||||||
|
icon?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ButtonExample {
|
||||||
|
id: number;
|
||||||
|
label: string;
|
||||||
|
buttons: ButtonDetail[];
|
||||||
|
desc?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { loading, startLoading, endLoading } = useLoading();
|
||||||
|
|
||||||
|
const buttonExample: ButtonExample[] = [
|
||||||
|
{
|
||||||
|
id: 0,
|
||||||
|
label: '基础',
|
||||||
|
buttons: [
|
||||||
|
{
|
||||||
|
id: 0,
|
||||||
|
props: {},
|
||||||
|
label: 'Default'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
props: { type: 'tertiary' },
|
||||||
|
label: 'Tertiary'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
props: { type: 'primary' },
|
||||||
|
label: 'Primary'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
props: { type: 'info' },
|
||||||
|
label: 'Info'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
props: { type: 'success' },
|
||||||
|
label: 'Success'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 5,
|
||||||
|
props: { type: 'warning' },
|
||||||
|
label: 'Warning'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 6,
|
||||||
|
props: { type: 'error' },
|
||||||
|
label: 'Error'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
desc: '按钮的 type 分别为 default、primary、info、success、warning 和 error。'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
label: '次要按钮',
|
||||||
|
buttons: [
|
||||||
|
{
|
||||||
|
id: 0,
|
||||||
|
props: { strong: true, secondary: true },
|
||||||
|
label: 'Default'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
props: { strong: true, secondary: true, type: 'tertiary' },
|
||||||
|
label: 'Tertiary'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
props: { strong: true, secondary: true, type: 'primary' },
|
||||||
|
label: 'Primary'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
props: { strong: true, secondary: true, type: 'info' },
|
||||||
|
label: 'Info'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
props: { strong: true, secondary: true, type: 'success' },
|
||||||
|
label: 'Success'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 5,
|
||||||
|
props: { strong: true, secondary: true, type: 'warning' },
|
||||||
|
label: 'Warning'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 6,
|
||||||
|
props: { strong: true, secondary: true, type: 'error' },
|
||||||
|
label: 'Error'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 7,
|
||||||
|
props: { strong: true, secondary: true, round: true },
|
||||||
|
label: 'Default'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 8,
|
||||||
|
props: { strong: true, secondary: true, round: true, type: 'tertiary' },
|
||||||
|
label: 'Tertiary'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 9,
|
||||||
|
props: { strong: true, secondary: true, round: true, type: 'primary' },
|
||||||
|
label: 'Primary'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 10,
|
||||||
|
props: { strong: true, secondary: true, round: true, type: 'info' },
|
||||||
|
label: 'Info'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 11,
|
||||||
|
props: { strong: true, secondary: true, round: true, type: 'success' },
|
||||||
|
label: 'Success'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 12,
|
||||||
|
props: { strong: true, secondary: true, round: true, type: 'warning' },
|
||||||
|
label: 'Warning'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 13,
|
||||||
|
props: { strong: true, secondary: true, round: true, type: 'error' },
|
||||||
|
label: 'Error'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
label: '次次要按钮',
|
||||||
|
buttons: [
|
||||||
|
{
|
||||||
|
id: 0,
|
||||||
|
props: { tertiary: true },
|
||||||
|
label: 'Default'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
props: { tertiary: true, type: 'primary' },
|
||||||
|
label: 'Primary'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
props: { tertiary: true, type: 'info' },
|
||||||
|
label: 'Info'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
props: { tertiary: true, type: 'success' },
|
||||||
|
label: 'Success'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
props: { tertiary: true, type: 'warning' },
|
||||||
|
label: 'Warning'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 5,
|
||||||
|
props: { tertiary: true, type: 'error' },
|
||||||
|
label: 'Error'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 6,
|
||||||
|
props: { tertiary: true, round: true },
|
||||||
|
label: 'Default'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 7,
|
||||||
|
props: { tertiary: true, round: true, type: 'primary' },
|
||||||
|
label: 'Primary'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 8,
|
||||||
|
props: { tertiary: true, round: true, type: 'info' },
|
||||||
|
label: 'Info'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 9,
|
||||||
|
props: { tertiary: true, round: true, type: 'success' },
|
||||||
|
label: 'Success'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 10,
|
||||||
|
props: { tertiary: true, round: true, type: 'warning' },
|
||||||
|
label: 'Warning'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 11,
|
||||||
|
props: { tertiary: true, round: true, type: 'error' },
|
||||||
|
label: 'Error'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
label: '次次次要按钮',
|
||||||
|
buttons: [
|
||||||
|
{
|
||||||
|
id: 0,
|
||||||
|
props: { quaternary: true },
|
||||||
|
label: 'Default'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
props: { quaternary: true, type: 'primary' },
|
||||||
|
label: 'Primary'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
props: { quaternary: true, type: 'info' },
|
||||||
|
label: 'Info'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
props: { quaternary: true, type: 'success' },
|
||||||
|
label: 'Success'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
props: { quaternary: true, type: 'warning' },
|
||||||
|
label: 'Warning'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 5,
|
||||||
|
props: { quaternary: true, type: 'error' },
|
||||||
|
label: 'Error'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 6,
|
||||||
|
props: { quaternary: true, round: true },
|
||||||
|
label: 'Default'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 7,
|
||||||
|
props: { quaternary: true, round: true, type: 'primary' },
|
||||||
|
label: 'Primary'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 8,
|
||||||
|
props: { quaternary: true, round: true, type: 'info' },
|
||||||
|
label: 'Info'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 9,
|
||||||
|
props: { quaternary: true, round: true, type: 'success' },
|
||||||
|
label: 'Success'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 10,
|
||||||
|
props: { quaternary: true, round: true, type: 'warning' },
|
||||||
|
label: 'Warning'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 11,
|
||||||
|
props: { quaternary: true, round: true, type: 'error' },
|
||||||
|
label: 'Error'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
label: '虚线按钮',
|
||||||
|
buttons: [
|
||||||
|
{
|
||||||
|
id: 0,
|
||||||
|
props: { dashed: true },
|
||||||
|
label: 'Default'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
props: { dashed: true, type: 'tertiary' },
|
||||||
|
label: 'Tertiary'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
props: { dashed: true, type: 'primary' },
|
||||||
|
label: 'Primary'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
props: { dashed: true, type: 'info' },
|
||||||
|
label: 'Info'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
props: { dashed: true, type: 'success' },
|
||||||
|
label: 'Success'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 5,
|
||||||
|
props: { dashed: true, type: 'warning' },
|
||||||
|
label: 'Warning'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 6,
|
||||||
|
props: { dashed: true, type: 'error' },
|
||||||
|
label: 'Error'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 5,
|
||||||
|
label: '尺寸',
|
||||||
|
buttons: [
|
||||||
|
{
|
||||||
|
id: 0,
|
||||||
|
props: { size: 'tiny', strong: true },
|
||||||
|
label: '小小'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
props: { size: 'small', strong: true },
|
||||||
|
label: '小'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
props: { size: 'medium', strong: true },
|
||||||
|
label: '不小'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
props: { size: 'large', strong: true },
|
||||||
|
label: '不不小'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 6,
|
||||||
|
label: '文本按钮',
|
||||||
|
buttons: [
|
||||||
|
{
|
||||||
|
id: 0,
|
||||||
|
props: { text: true },
|
||||||
|
label: '那车头依然吐着烟',
|
||||||
|
icon: 'mdi:train'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 7,
|
||||||
|
label: '自定义标签按钮',
|
||||||
|
buttons: [
|
||||||
|
{
|
||||||
|
id: 0,
|
||||||
|
props: {
|
||||||
|
text: true,
|
||||||
|
tag: 'a',
|
||||||
|
href: 'https://github.com/honghuangdc/soybean-admin',
|
||||||
|
target: '_blank',
|
||||||
|
type: 'primary'
|
||||||
|
},
|
||||||
|
label: 'soybean-admin'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
desc: '你可以把按钮渲染成不同的标签,比如 a标签 。'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 8,
|
||||||
|
label: '按钮禁用',
|
||||||
|
buttons: [
|
||||||
|
{
|
||||||
|
id: 0,
|
||||||
|
props: {
|
||||||
|
disabled: true
|
||||||
|
},
|
||||||
|
label: '不许点'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
desc: '按钮可以被禁用'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 9,
|
||||||
|
label: '图标按钮',
|
||||||
|
buttons: [
|
||||||
|
{
|
||||||
|
id: 0,
|
||||||
|
props: {
|
||||||
|
secondary: true,
|
||||||
|
strong: true
|
||||||
|
},
|
||||||
|
label: '+100元',
|
||||||
|
icon: 'mdi:cash-100'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 0,
|
||||||
|
props: {
|
||||||
|
iconPlacement: 'right',
|
||||||
|
secondary: true,
|
||||||
|
strong: true
|
||||||
|
},
|
||||||
|
label: '+100元',
|
||||||
|
icon: 'mdi:cash-100'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
desc: '在按钮上使用图标。'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 10,
|
||||||
|
label: '不同形状按钮',
|
||||||
|
buttons: [
|
||||||
|
{
|
||||||
|
id: 0,
|
||||||
|
props: {
|
||||||
|
circle: true
|
||||||
|
},
|
||||||
|
icon: 'mdi:cash-100'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
props: {
|
||||||
|
round: true
|
||||||
|
},
|
||||||
|
label: '圆角'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
props: {},
|
||||||
|
label: '方'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
desc: '按钮拥有不同的形状。'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 11,
|
||||||
|
label: '透明背景按钮',
|
||||||
|
buttons: [
|
||||||
|
{
|
||||||
|
id: 0,
|
||||||
|
props: { ghost: true },
|
||||||
|
label: 'Default'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
props: { ghost: true, type: 'tertiary' },
|
||||||
|
label: 'Tertiary'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
props: { ghost: true, type: 'primary' },
|
||||||
|
label: 'Primary'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
props: { ghost: true, type: 'info' },
|
||||||
|
label: 'Info'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
props: { ghost: true, type: 'success' },
|
||||||
|
label: 'Success'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 5,
|
||||||
|
props: { ghost: true, type: 'warning' },
|
||||||
|
label: 'Warning'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 6,
|
||||||
|
props: { ghost: true, type: 'error' },
|
||||||
|
label: 'Error'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
desc: 'Ghost 按钮有透明的背景。'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 12,
|
||||||
|
label: '自定义颜色',
|
||||||
|
buttons: [
|
||||||
|
{
|
||||||
|
id: 0,
|
||||||
|
props: {
|
||||||
|
color: '#8a2be2'
|
||||||
|
},
|
||||||
|
label: '#8a2be2',
|
||||||
|
icon: 'ic:baseline-color-lens'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
props: {
|
||||||
|
color: '#ff69b4'
|
||||||
|
},
|
||||||
|
label: '#ff69b4',
|
||||||
|
icon: 'ic:baseline-color-lens'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
props: {
|
||||||
|
color: '#8a2be2',
|
||||||
|
ghost: true
|
||||||
|
},
|
||||||
|
label: '#8a2be2',
|
||||||
|
icon: 'ic:baseline-color-lens'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
props: {
|
||||||
|
color: '#ff69b4',
|
||||||
|
ghost: true
|
||||||
|
},
|
||||||
|
label: '#ff69b4',
|
||||||
|
icon: 'ic:baseline-color-lens'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
props: {
|
||||||
|
color: '#8a2be2',
|
||||||
|
text: true
|
||||||
|
},
|
||||||
|
label: '#8a2be2',
|
||||||
|
icon: 'ic:baseline-color-lens'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 5,
|
||||||
|
props: {
|
||||||
|
color: '#ff69b4',
|
||||||
|
text: true
|
||||||
|
},
|
||||||
|
label: '#ff69b4',
|
||||||
|
icon: 'ic:baseline-color-lens'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
desc: '这两个颜色看起来像毒蘑菇。'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped></style>
|
42
src/views/component/card/index.vue
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<n-card title="卡片" :bordered="false" class="h-full rounded-8px shadow-sm">
|
||||||
|
<n-space :vertical="true">
|
||||||
|
<n-card title="基本用法">
|
||||||
|
<p class="pb-16px">基础卡片</p>
|
||||||
|
<n-card title="卡片">卡片内容</n-card>
|
||||||
|
</n-card>
|
||||||
|
<n-card title="尺寸">
|
||||||
|
<p class="pb-16px">卡片有 small、medium、large、huge 尺寸。</p>
|
||||||
|
<n-space vertical>
|
||||||
|
<n-card title="小卡片" size="small">卡片内容</n-card>
|
||||||
|
<n-card title="中卡片" size="medium">卡片内容</n-card>
|
||||||
|
<n-card title="大卡片" size="large">卡片内容</n-card>
|
||||||
|
<n-card title="超大卡片" size="huge">卡片内容</n-card>
|
||||||
|
</n-space>
|
||||||
|
</n-card>
|
||||||
|
<n-card title="文本按钮">
|
||||||
|
<p class="pb-16px">
|
||||||
|
content 和 footer 可以被 hard 或 soft 分段,action 可以被分段。分段分割线会在区域的上方出现。
|
||||||
|
</p>
|
||||||
|
<n-card
|
||||||
|
title="卡片分段示例"
|
||||||
|
:segmented="{
|
||||||
|
content: true,
|
||||||
|
footer: 'soft'
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<template #header-extra>#header-extra</template>
|
||||||
|
卡片内容
|
||||||
|
<template #footer>#footer</template>
|
||||||
|
<template #action>#action</template>
|
||||||
|
</n-card>
|
||||||
|
</n-card>
|
||||||
|
</n-space>
|
||||||
|
</n-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts"></script>
|
||||||
|
|
||||||
|
<style scoped></style>
|
124
src/views/component/table/index.vue
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
<template>
|
||||||
|
<div class="h-full overflow-hidden">
|
||||||
|
<n-card title="表格" :bordered="false" class="h-full rounded-8px shadow-sm">
|
||||||
|
<n-space :vertical="true">
|
||||||
|
<n-space>
|
||||||
|
<n-button @click="getDataSource">有数据</n-button>
|
||||||
|
<n-button @click="getEmptyDataSource">空数据</n-button>
|
||||||
|
</n-space>
|
||||||
|
<loading-empty-wrapper class="h-480px" :loading="loading" :empty="empty">
|
||||||
|
<n-data-table :columns="columns" :data="dataSource" :flex-height="true" class="h-480px" />
|
||||||
|
</loading-empty-wrapper>
|
||||||
|
</n-space>
|
||||||
|
</n-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="tsx">
|
||||||
|
import { onMounted, ref } from 'vue';
|
||||||
|
import { NSpace, NButton, NPopconfirm } from 'naive-ui';
|
||||||
|
import type { DataTableColumn } from 'naive-ui';
|
||||||
|
import { useLoadingEmpty } from '@/hooks';
|
||||||
|
import { getRandomInteger } from '@/utils';
|
||||||
|
|
||||||
|
interface DataSource {
|
||||||
|
name: string;
|
||||||
|
age: number;
|
||||||
|
address: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { loading, startLoading, endLoading, empty, setEmpty } = useLoadingEmpty();
|
||||||
|
|
||||||
|
const columns: DataTableColumn<DataSource>[] = [
|
||||||
|
{
|
||||||
|
title: 'Name',
|
||||||
|
key: 'name',
|
||||||
|
align: 'center'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Age',
|
||||||
|
key: 'age',
|
||||||
|
align: 'center'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Address',
|
||||||
|
key: 'address',
|
||||||
|
align: 'center'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'action',
|
||||||
|
title: 'Action',
|
||||||
|
align: 'center',
|
||||||
|
render: row => {
|
||||||
|
return (
|
||||||
|
<NSpace justify={'center'}>
|
||||||
|
<NButton
|
||||||
|
size={'small'}
|
||||||
|
onClick={() => {
|
||||||
|
handleEdit(row.name);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
编辑
|
||||||
|
</NButton>
|
||||||
|
<NPopconfirm
|
||||||
|
onPositiveClick={() => {
|
||||||
|
handleDelete(row.name);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{{
|
||||||
|
default: () => '确认删除',
|
||||||
|
trigger: () => <NButton size={'small'}>删除</NButton>
|
||||||
|
}}
|
||||||
|
</NPopconfirm>
|
||||||
|
</NSpace>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const dataSource = ref<DataSource[]>([]);
|
||||||
|
|
||||||
|
function createDataSource(): DataSource[] {
|
||||||
|
return Array(100)
|
||||||
|
.fill(1)
|
||||||
|
.map((_item, index) => {
|
||||||
|
return {
|
||||||
|
name: `Name${index}`,
|
||||||
|
age: getRandomInteger(30, 20),
|
||||||
|
address: '中国'
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDataSource() {
|
||||||
|
startLoading();
|
||||||
|
setTimeout(() => {
|
||||||
|
dataSource.value = createDataSource();
|
||||||
|
endLoading();
|
||||||
|
setEmpty(!dataSource.value.length);
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getEmptyDataSource() {
|
||||||
|
startLoading();
|
||||||
|
setTimeout(() => {
|
||||||
|
dataSource.value = [];
|
||||||
|
endLoading();
|
||||||
|
setEmpty(!dataSource.value.length);
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleEdit(_name: string) {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDelete(_name: string) {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
getDataSource();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped></style>
|
136
src/views/dashboard/analysis/components/bottom-part/index.vue
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
<template>
|
||||||
|
<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="h-full rounded-8px shadow-sm">
|
||||||
|
<n-timeline>
|
||||||
|
<n-timeline-item v-for="item in timelines" :key="item.type" v-bind="item" />
|
||||||
|
</n-timeline>
|
||||||
|
</n-card>
|
||||||
|
</n-grid-item>
|
||||||
|
<n-grid-item span="0:24 640:24 1024:16">
|
||||||
|
<n-card title="表格" :bordered="false" class="h-full rounded-8px shadow-sm">
|
||||||
|
<n-data-table size="small" :columns="columns" :data="tableData" />
|
||||||
|
</n-card>
|
||||||
|
</n-grid-item>
|
||||||
|
</n-grid>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { h } from 'vue';
|
||||||
|
import { NTag } from 'naive-ui';
|
||||||
|
|
||||||
|
defineOptions({ name: 'DashboardAnalysisBottomPart' });
|
||||||
|
|
||||||
|
interface TimelineData {
|
||||||
|
type: 'default' | 'info' | 'success' | 'warning' | 'error';
|
||||||
|
title: string;
|
||||||
|
content: string;
|
||||||
|
time: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TableData {
|
||||||
|
key: number;
|
||||||
|
name: string;
|
||||||
|
age: number;
|
||||||
|
address: string;
|
||||||
|
tags: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const timelines: TimelineData[] = [
|
||||||
|
{ type: 'default', title: '啊', content: '', time: '2021-10-10 20:46' },
|
||||||
|
{ type: 'success', title: '成功', content: '哪里成功', time: '2021-10-10 20:46' },
|
||||||
|
{ type: 'error', title: '错误', content: '哪里错误', time: '2021-10-10 20:46' },
|
||||||
|
{ type: 'warning', title: '警告', content: '哪里警告', time: '2021-10-10 20:46' },
|
||||||
|
{ type: 'info', title: '信息', content: '是的', time: '2021-10-10 20:46' }
|
||||||
|
];
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
title: 'Name',
|
||||||
|
key: 'name'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Age',
|
||||||
|
key: 'age'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Address',
|
||||||
|
key: 'address'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Tags',
|
||||||
|
key: 'tags',
|
||||||
|
render(row: TableData) {
|
||||||
|
const tags = row.tags.map(tagKey => {
|
||||||
|
return h(
|
||||||
|
NTag,
|
||||||
|
{
|
||||||
|
style: {
|
||||||
|
marginRight: '6px'
|
||||||
|
},
|
||||||
|
type: 'info'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
default: () => tagKey
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
return tags;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const tableData: TableData[] = [
|
||||||
|
{
|
||||||
|
key: 0,
|
||||||
|
name: 'John Brown',
|
||||||
|
age: 32,
|
||||||
|
address: 'New York No. 1 Lake Park',
|
||||||
|
tags: ['nice', 'developer']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 1,
|
||||||
|
name: 'Jim Green',
|
||||||
|
age: 42,
|
||||||
|
address: 'London No. 1 Lake Park',
|
||||||
|
tags: ['wow']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 2,
|
||||||
|
name: 'Joe Black',
|
||||||
|
age: 32,
|
||||||
|
address: 'Sidney No. 1 Lake Park',
|
||||||
|
tags: ['cool', 'teacher']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 3,
|
||||||
|
name: 'Soybean',
|
||||||
|
age: 25,
|
||||||
|
address: 'China Shenzhen',
|
||||||
|
tags: ['handsome', 'programmer']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 4,
|
||||||
|
name: 'John Brown',
|
||||||
|
age: 32,
|
||||||
|
address: 'New York No. 1 Lake Park',
|
||||||
|
tags: ['nice', 'developer']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 5,
|
||||||
|
name: 'Jim Green',
|
||||||
|
age: 42,
|
||||||
|
address: 'London No. 1 Lake Park',
|
||||||
|
tags: ['wow']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 6,
|
||||||
|
name: 'Joe Black',
|
||||||
|
age: 32,
|
||||||
|
address: 'Sidney No. 1 Lake Park',
|
||||||
|
tags: ['cool', 'teacher']
|
||||||
|
}
|
||||||
|
];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped></style>
|
@@ -0,0 +1,25 @@
|
|||||||
|
<template>
|
||||||
|
<div class="p-16px rounded-8px text-white" :style="{ backgroundImage: gradientStyle }">
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
/** 渐变开始的颜色 */
|
||||||
|
startColor?: string;
|
||||||
|
/** 渐变结束的颜色 */
|
||||||
|
endColor?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
startColor: '#56cdf3',
|
||||||
|
endColor: '#719de3'
|
||||||
|
});
|
||||||
|
|
||||||
|
const gradientStyle = computed(() => `linear-gradient(to bottom right, ${props.startColor}, ${props.endColor})`);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped></style>
|
@@ -0,0 +1,3 @@
|
|||||||
|
import GradientBg from './gradient-bg.vue';
|
||||||
|
|
||||||
|
export { GradientBg };
|
72
src/views/dashboard/analysis/components/data-card/index.vue
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
<template>
|
||||||
|
<n-card :bordered="false" class="h-full rounded-8px shadow-sm">
|
||||||
|
<n-grid cols="s:1 m:2 l:4" responsive="screen" :x-gap="16" :y-gap="16">
|
||||||
|
<n-grid-item v-for="item in cardData" :key="item.id">
|
||||||
|
<gradient-bg class="h-100px" :start-color="item.colors[0]" :end-color="item.colors[1]">
|
||||||
|
<h3 class="text-16px">{{ item.title }}</h3>
|
||||||
|
<div class="flex justify-between pt-12px">
|
||||||
|
<svg-icon :icon="item.icon" class="text-32px" />
|
||||||
|
<count-to
|
||||||
|
:prefix="item.unit"
|
||||||
|
:start-value="1"
|
||||||
|
:end-value="item.value"
|
||||||
|
class="text-30px text-white dark:text-dark"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</gradient-bg>
|
||||||
|
</n-grid-item>
|
||||||
|
</n-grid>
|
||||||
|
</n-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { GradientBg } from './components';
|
||||||
|
|
||||||
|
defineOptions({ name: 'DashboardAnalysisDataCard' });
|
||||||
|
|
||||||
|
interface CardData {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
value: number;
|
||||||
|
unit: string;
|
||||||
|
colors: [string, string];
|
||||||
|
icon: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cardData: CardData[] = [
|
||||||
|
{
|
||||||
|
id: 'visit',
|
||||||
|
title: '访问量',
|
||||||
|
value: 1000000,
|
||||||
|
unit: '',
|
||||||
|
colors: ['#ec4786', '#b955a4'],
|
||||||
|
icon: 'ant-design:bar-chart-outlined'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'amount',
|
||||||
|
title: '成交额',
|
||||||
|
value: 234567.89,
|
||||||
|
unit: '$',
|
||||||
|
colors: ['#865ec0', '#5144b4'],
|
||||||
|
icon: 'ant-design:money-collect-outlined'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'download',
|
||||||
|
title: '下载数',
|
||||||
|
value: 666666,
|
||||||
|
unit: '',
|
||||||
|
colors: ['#56cdf3', '#719de3'],
|
||||||
|
icon: 'carbon:document-download'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'trade',
|
||||||
|
title: '成交数',
|
||||||
|
value: 999999,
|
||||||
|
unit: '',
|
||||||
|
colors: ['#fcbc25', '#f68057'],
|
||||||
|
icon: 'ant-design:trademark-circle-outlined'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped></style>
|
5
src/views/dashboard/analysis/components/index.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import TopChart from './top-chart/index.vue';
|
||||||
|
import DataCard from './data-card/index.vue';
|
||||||
|
import BottomPart from './bottom-part/index.vue';
|
||||||
|
|
||||||
|
export { TopChart, DataCard, BottomPart };
|
184
src/views/dashboard/analysis/components/top-chart/index.vue
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
<template>
|
||||||
|
<n-grid :x-gap="16" :y-gap="16" :item-responsive="true">
|
||||||
|
<n-grid-item span="0:24 640:24 1024:6">
|
||||||
|
<n-card :bordered="false" class="rounded-8px shadow-sm">
|
||||||
|
<div class="w-full h-360px py-12px">
|
||||||
|
<h3 class="text-16px font-bold">Dashboard</h3>
|
||||||
|
<p class="text-#aaa">Overview Of Lasted Month</p>
|
||||||
|
<h3 class="pt-32px text-24px font-bold">
|
||||||
|
<count-to prefix="$" :start-value="0" :end-value="7754" />
|
||||||
|
</h3>
|
||||||
|
<p class="text-#aaa">Current Month Earnings</p>
|
||||||
|
<h3 class="pt-32px text-24px font-bold">
|
||||||
|
<count-to :start-value="0" :end-value="1234" />
|
||||||
|
</h3>
|
||||||
|
<p class="text-#aaa">Current Month Sales</p>
|
||||||
|
<n-button class="mt-24px whitespace-pre-wrap" type="primary">Last Month Summary</n-button>
|
||||||
|
</div>
|
||||||
|
</n-card>
|
||||||
|
</n-grid-item>
|
||||||
|
<n-grid-item span="0:24 640:24 1024:10">
|
||||||
|
<n-card :bordered="false" class="rounded-8px shadow-sm">
|
||||||
|
<div ref="lineRef" class="w-full h-360px"></div>
|
||||||
|
</n-card>
|
||||||
|
</n-grid-item>
|
||||||
|
<n-grid-item span="0:24 640:24 1024:8">
|
||||||
|
<n-card :bordered="false" class="rounded-8px shadow-sm">
|
||||||
|
<div ref="pieRef" class="w-full h-360px"></div>
|
||||||
|
</n-card>
|
||||||
|
</n-grid-item>
|
||||||
|
</n-grid>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import type { Ref } from 'vue';
|
||||||
|
import { type ECOption, useEcharts } from '@/composables';
|
||||||
|
|
||||||
|
defineOptions({ name: 'DashboardAnalysisTopCard' });
|
||||||
|
|
||||||
|
const lineOptions = ref<ECOption>({
|
||||||
|
tooltip: {
|
||||||
|
trigger: 'axis',
|
||||||
|
axisPointer: {
|
||||||
|
type: 'cross',
|
||||||
|
label: {
|
||||||
|
backgroundColor: '#6a7985'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
data: ['下载量', '注册数']
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
left: '3%',
|
||||||
|
right: '4%',
|
||||||
|
bottom: '3%',
|
||||||
|
containLabel: true
|
||||||
|
},
|
||||||
|
xAxis: [
|
||||||
|
{
|
||||||
|
type: 'category',
|
||||||
|
boundaryGap: false,
|
||||||
|
data: ['06:00', '08:00', '10:00', '12:00', '14:00', '16:00', '18:00', '20:00', '22:00', '24:00']
|
||||||
|
}
|
||||||
|
],
|
||||||
|
yAxis: [
|
||||||
|
{
|
||||||
|
type: 'value'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
color: '#8e9dff',
|
||||||
|
name: '下载量',
|
||||||
|
type: 'line',
|
||||||
|
smooth: true,
|
||||||
|
stack: 'Total',
|
||||||
|
areaStyle: {
|
||||||
|
color: {
|
||||||
|
type: 'linear',
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
x2: 0,
|
||||||
|
y2: 1,
|
||||||
|
colorStops: [
|
||||||
|
{
|
||||||
|
offset: 0.25,
|
||||||
|
color: '#8e9dff'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
offset: 1,
|
||||||
|
color: '#fff'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
emphasis: {
|
||||||
|
focus: 'series'
|
||||||
|
},
|
||||||
|
data: [4623, 6145, 6268, 6411, 1890, 4251, 2978, 3880, 3606, 4311]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
color: '#26deca',
|
||||||
|
name: '注册数',
|
||||||
|
type: 'line',
|
||||||
|
smooth: true,
|
||||||
|
stack: 'Total',
|
||||||
|
areaStyle: {
|
||||||
|
color: {
|
||||||
|
type: 'linear',
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
x2: 0,
|
||||||
|
y2: 1,
|
||||||
|
colorStops: [
|
||||||
|
{
|
||||||
|
offset: 0.25,
|
||||||
|
color: '#26deca'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
offset: 1,
|
||||||
|
color: '#fff'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
emphasis: {
|
||||||
|
focus: 'series'
|
||||||
|
},
|
||||||
|
data: [2208, 2016, 2916, 4512, 8281, 2008, 1963, 2367, 2956, 678]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}) as Ref<ECOption>;
|
||||||
|
const { domRef: lineRef } = useEcharts(lineOptions);
|
||||||
|
|
||||||
|
const pieOptions = ref<ECOption>({
|
||||||
|
tooltip: {
|
||||||
|
trigger: 'item'
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
bottom: '1%',
|
||||||
|
left: 'center',
|
||||||
|
itemStyle: {
|
||||||
|
borderWidth: 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
color: ['#5da8ff', '#8e9dff', '#fedc69', '#26deca'],
|
||||||
|
name: '时间安排',
|
||||||
|
type: 'pie',
|
||||||
|
radius: ['45%', '75%'],
|
||||||
|
avoidLabelOverlap: false,
|
||||||
|
itemStyle: {
|
||||||
|
borderRadius: 10,
|
||||||
|
borderColor: '#fff',
|
||||||
|
borderWidth: 1
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
show: false,
|
||||||
|
position: 'center'
|
||||||
|
},
|
||||||
|
emphasis: {
|
||||||
|
label: {
|
||||||
|
show: true,
|
||||||
|
fontSize: '12'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
labelLine: {
|
||||||
|
show: false
|
||||||
|
},
|
||||||
|
data: [
|
||||||
|
{ value: 20, name: '学习' },
|
||||||
|
{ value: 10, name: '娱乐' },
|
||||||
|
{ value: 30, name: '工作' },
|
||||||
|
{ value: 40, name: '休息' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}) as Ref<ECOption>;
|
||||||
|
const { domRef: pieRef } = useEcharts(pieOptions);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped></style>
|