mirror of
				https://github.com/soybeanjs/soybean-admin.git
				synced 2025-10-31 13:53:43 +08:00 
			
		
		
		
	Compare commits
	
		
			1 Commits
		
	
	
		
			v1.2.7
			...
			thin-v0.10
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | d32e296473 | 
							
								
								
									
										2
									
								
								.env
									
									
									
									
									
								
							
							
						
						
									
										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=/dashboard/analysis | VITE_ROUTE_HOME_PATH=/multi-menu/first/second | ||||||
|  |  | ||||||
| # iconify图标作为组件的前缀 | # iconify图标作为组件的前缀 | ||||||
| VITE_ICON_PREFFIX=icon | VITE_ICON_PREFFIX=icon | ||||||
|   | |||||||
| @@ -1,10 +1 @@ | |||||||
| 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 | ||||||
|   | |||||||
							
								
								
									
										1195
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						
									
										1195
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										16
									
								
								Makefile
									
									
									
									
									
								
							
							
						
						
									
										16
									
								
								Makefile
									
									
									
									
									
								
							| @@ -1,16 +0,0 @@ | |||||||
| 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 |  | ||||||
| @@ -1,8 +0,0 @@ | |||||||
| 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,2 +1 @@ | |||||||
| export * from './define'; |  | ||||||
| export * from './proxy'; | export * from './proxy'; | ||||||
|   | |||||||
| @@ -1,6 +0,0 @@ | |||||||
| import ViteCompression from 'vite-plugin-compression'; |  | ||||||
|  |  | ||||||
| export default (viteEnv: ImportMetaEnv) => { |  | ||||||
|   const { VITE_COMPRESS_TYPE = 'gzip' } = viteEnv; |  | ||||||
|   return ViteCompression({ algorithm: VITE_COMPRESS_TYPE }); |  | ||||||
| }; |  | ||||||
| @@ -2,14 +2,10 @@ 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 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插件 | ||||||
| @@ -26,19 +22,9 @@ export function setupVitePlugins(viteEnv: ImportMetaEnv): (PluginOption | Plugin | |||||||
|     VueDevtools(), |     VueDevtools(), | ||||||
|     ...unplugin(viteEnv), |     ...unplugin(viteEnv), | ||||||
|     unocss(), |     unocss(), | ||||||
|     mock(viteEnv), |     mock(viteEnv) | ||||||
|     progress() |  | ||||||
|   ]; |   ]; | ||||||
|  |  | ||||||
|   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()); | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -1,31 +0,0 @@ | |||||||
| 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' |  | ||||||
|         } |  | ||||||
|       ] |  | ||||||
|     } |  | ||||||
|   }); |  | ||||||
| } |  | ||||||
| @@ -1,7 +0,0 @@ | |||||||
| import { visualizer } from 'rollup-plugin-visualizer'; |  | ||||||
|  |  | ||||||
| export default visualizer({ |  | ||||||
|   gzipSize: true, |  | ||||||
|   brotliSize: true, |  | ||||||
|   open: true |  | ||||||
| }); |  | ||||||
| @@ -1,32 +0,0 @@ | |||||||
| 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 |  | ||||||
| @@ -1,24 +0,0 @@ | |||||||
| 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 |  | ||||||
| @@ -1,54 +0,0 @@ | |||||||
| 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,5 +1,4 @@ | |||||||
| import auth from './auth'; | import auth from './auth'; | ||||||
| import route from './route'; | import route from './route'; | ||||||
| import management from './management'; |  | ||||||
|  |  | ||||||
| export default [...auth, ...route, ...management]; | export default [...auth, ...route]; | ||||||
|   | |||||||
| @@ -1,33 +0,0 @@ | |||||||
| 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 = 'dashboard_analysis'; |       const routeHomeName: AuthRoute.LastDegreeRouteKey = 'multi-menu_first_second'; | ||||||
|  |  | ||||||
|       const role = userModel.find(item => item.userId === userId)?.userRole || 'user'; |       const role = userModel.find(item => item.userId === userId)?.userRole || 'user'; | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										1084
									
								
								mock/model/route.ts
									
									
									
									
									
								
							
							
						
						
									
										1084
									
								
								mock/model/route.ts
									
									
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										29
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										29
									
								
								package.json
									
									
									
									
									
								
							| @@ -49,14 +49,10 @@ | |||||||
|     "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.1.2", |     "@vueuse/core": "10.1.2", | ||||||
| @@ -65,30 +61,21 @@ | |||||||
|     "colord": "2.9.3", |     "colord": "2.9.3", | ||||||
|     "crypto-js": "4.1.1", |     "crypto-js": "4.1.1", | ||||||
|     "dayjs": "1.11.8", |     "dayjs": "1.11.8", | ||||||
|     "echarts": "5.4.2", |  | ||||||
|     "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.4", |     "pinia": "2.1.4", | ||||||
|     "print-js": "1.6.0", |  | ||||||
|     "qs": "6.11.2", |     "qs": "6.11.2", | ||||||
|     "swiper": "9.4.1", |  | ||||||
|     "ua-parser-js": "1.0.35", |     "ua-parser-js": "1.0.35", | ||||||
|     "vditor": "3.9.3", |  | ||||||
|     "vue": "3.3.4", |     "vue": "3.3.4", | ||||||
|     "vue-i18n": "9.2.2", |     "vue-i18n": "9.2.2", | ||||||
|     "vue-router": "4.2.2", |     "vue-router": "4.2.2" | ||||||
|     "vuedraggable": "4.1.0", |  | ||||||
|     "wangeditor": "4.7.15", |  | ||||||
|     "xgplayer": "3.0.4" |  | ||||||
|   }, |   }, | ||||||
|   "devDependencies": { |   "devDependencies": { | ||||||
|     "@amap/amap-jsapi-types": "0.0.13", |  | ||||||
|     "@iconify/json": "2.2.78", |     "@iconify/json": "2.2.78", | ||||||
|     "@iconify/vue": "4.1.1", |     "@iconify/vue": "4.1.1", | ||||||
|     "@soybeanjs/cli": "0.6.2", |     "@soybeanjs/cli": "0.6.2", | ||||||
|     "@soybeanjs/vite-plugin-vue-page-route": "0.0.5", |     "@soybeanjs/vite-plugin-vue-page-route": "0.0.5", | ||||||
|     "@types/bmapgl": "0.0.7", |  | ||||||
|     "@types/crypto-js": "4.1.1", |     "@types/crypto-js": "4.1.1", | ||||||
|     "@types/node": "20.3.1", |     "@types/node": "20.3.1", | ||||||
|     "@types/qs": "6.9.7", |     "@types/qs": "6.9.7", | ||||||
| @@ -102,18 +89,13 @@ | |||||||
|     "eslint": "8.42.0", |     "eslint": "8.42.0", | ||||||
|     "eslint-config-soybeanjs": "0.4.9", |     "eslint-config-soybeanjs": "0.4.9", | ||||||
|     "mockjs": "1.1.0", |     "mockjs": "1.1.0", | ||||||
|     "rollup-plugin-visualizer": "5.9.2", |  | ||||||
|     "sass": "1.63.4", |     "sass": "1.63.4", | ||||||
|     "simple-git-hooks": "2.8.1", |  | ||||||
|     "tsx": "3.12.7", |     "tsx": "3.12.7", | ||||||
|     "typescript": "5.1.3", |     "typescript": "5.1.3", | ||||||
|     "unplugin-icons": "0.16.3", |     "unplugin-icons": "0.16.3", | ||||||
|     "unplugin-vue-components": "0.25.1", |     "unplugin-vue-components": "0.25.1", | ||||||
|     "vite": "4.3.9", |     "vite": "4.3.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.4", |  | ||||||
|     "vite-plugin-svg-icons": "2.0.1", |     "vite-plugin-svg-icons": "2.0.1", | ||||||
|     "vite-plugin-vue-devtools": "0.2.0", |     "vite-plugin-vue-devtools": "0.2.0", | ||||||
|     "vue-tsc": "1.6.5" |     "vue-tsc": "1.6.5" | ||||||
| @@ -122,12 +104,5 @@ | |||||||
|     "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 soy lint-staged" |  | ||||||
|   }, |  | ||||||
|   "soybean": { |  | ||||||
|     "useSoybeanToken": true |  | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										2275
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										2275
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -1,116 +0,0 @@ | |||||||
| <template> |  | ||||||
|   <span>{{ value }}</span> |  | ||||||
| </template> |  | ||||||
| <script lang="ts" setup> |  | ||||||
| import { computed, onMounted, ref, watch, watchEffect } from 'vue'; |  | ||||||
| import { TransitionPresets, useTransition } from '@vueuse/core'; |  | ||||||
| import { isNumber } from '@/utils'; |  | ||||||
|  |  | ||||||
| defineOptions({ name: 'CountTo' }); |  | ||||||
|  |  | ||||||
| type TansitionKey = keyof typeof TransitionPresets; |  | ||||||
|  |  | ||||||
| interface Props { |  | ||||||
|   /** 初始值 */ |  | ||||||
|   startValue?: number; |  | ||||||
|   /** 结束值 */ |  | ||||||
|   endValue?: number; |  | ||||||
|   /** 动画时长 */ |  | ||||||
|   duration?: number; |  | ||||||
|   /** 自动动画 */ |  | ||||||
|   autoplay?: boolean; |  | ||||||
|   /** 进制 */ |  | ||||||
|   decimals?: number; |  | ||||||
|   /** 前缀 */ |  | ||||||
|   prefix?: string; |  | ||||||
|   /** 后缀 */ |  | ||||||
|   suffix?: string; |  | ||||||
|   /** 分割符号 */ |  | ||||||
|   separator?: string; |  | ||||||
|   /** 小数点 */ |  | ||||||
|   decimal?: string; |  | ||||||
|   /** 使用缓冲动画函数 */ |  | ||||||
|   useEasing?: boolean; |  | ||||||
|   /** 缓冲动画函数类型 */ |  | ||||||
|   transition?: TansitionKey; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| const props = withDefaults(defineProps<Props>(), { |  | ||||||
|   startValue: 0, |  | ||||||
|   endValue: 2021, |  | ||||||
|   duration: 1500, |  | ||||||
|   autoplay: true, |  | ||||||
|   decimals: 0, |  | ||||||
|   prefix: '', |  | ||||||
|   suffix: '', |  | ||||||
|   separator: ',', |  | ||||||
|   decimal: '.', |  | ||||||
|   useEasing: true, |  | ||||||
|   transition: 'linear' |  | ||||||
| }); |  | ||||||
|  |  | ||||||
| interface Emits { |  | ||||||
|   (e: 'on-started'): void; |  | ||||||
|   (e: 'on-finished'): void; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| const emit = defineEmits<Emits>(); |  | ||||||
|  |  | ||||||
| const source = ref(props.startValue); |  | ||||||
| let outputValue = useTransition(source); |  | ||||||
| const value = computed(() => formatNumber(outputValue.value)); |  | ||||||
| const disabled = ref(false); |  | ||||||
|  |  | ||||||
| function run() { |  | ||||||
|   outputValue = useTransition(source, { |  | ||||||
|     disabled, |  | ||||||
|     duration: props.duration, |  | ||||||
|     onStarted: () => emit('on-started'), |  | ||||||
|     onFinished: () => emit('on-finished'), |  | ||||||
|     ...(props.useEasing ? { transition: TransitionPresets[props.transition] } : {}) |  | ||||||
|   }); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| function start() { |  | ||||||
|   run(); |  | ||||||
|   source.value = props.endValue; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| function formatNumber(num: number | string) { |  | ||||||
|   if (num !== 0 && !num) { |  | ||||||
|     return ''; |  | ||||||
|   } |  | ||||||
|   const { decimals, decimal, separator, suffix, prefix } = props; |  | ||||||
|   let number = Number(num).toFixed(decimals); |  | ||||||
|   number = String(number); |  | ||||||
|  |  | ||||||
|   const x = number.split('.'); |  | ||||||
|   let x1 = x[0]; |  | ||||||
|   const x2 = x.length > 1 ? decimal + x[1] : ''; |  | ||||||
|   const rgx = /(\d+)(\d{3})/; |  | ||||||
|   if (separator && !isNumber(separator)) { |  | ||||||
|     while (rgx.test(x1)) { |  | ||||||
|       x1 = x1.replace(rgx, `$1${separator}$2`); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|   return prefix + x1 + x2 + suffix; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| watch([() => props.startValue, () => props.endValue], () => { |  | ||||||
|   if (props.autoplay) { |  | ||||||
|     start(); |  | ||||||
|   } |  | ||||||
| }); |  | ||||||
|  |  | ||||||
| watchEffect(() => { |  | ||||||
|   source.value = props.startValue; |  | ||||||
| }); |  | ||||||
|  |  | ||||||
| onMounted(() => { |  | ||||||
|   if (props.autoplay) { |  | ||||||
|     start(); |  | ||||||
|   } |  | ||||||
| }); |  | ||||||
| </script> |  | ||||||
|  |  | ||||||
| <style scoped></style> |  | ||||||
| @@ -1,17 +0,0 @@ | |||||||
| <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> |  | ||||||
| @@ -1,77 +0,0 @@ | |||||||
| <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> |  | ||||||
| @@ -1,42 +0,0 @@ | |||||||
| <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> |  | ||||||
| @@ -1,23 +0,0 @@ | |||||||
| <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> |  | ||||||
| @@ -1,174 +0,0 @@ | |||||||
| 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) { | ||||||
|       window.console.warn('没有传递图标名称,请确保给icon或localIcon传递有效值!'); |       throw Error('没有传递图标名称,请确保给icon或localIcon传递有效值!'); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     return () => h(SvgIcon, { icon, localIcon, style }); |     return () => h(SvgIcon, { icon, localIcon, style }); | ||||||
|   | |||||||
| @@ -2,5 +2,4 @@ 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'; | ||||||
|   | |||||||
| @@ -1,3 +1,2 @@ | |||||||
| export * from './service'; | export * from './service'; | ||||||
| export * from './regexp'; | export * from './regexp'; | ||||||
| export * from './map-sdk'; |  | ||||||
|   | |||||||
| @@ -1,8 +0,0 @@ | |||||||
| /** 百度地图sdk地址 */ |  | ||||||
| export const BAIDU_MAP_SDK_URL = `https://api.map.baidu.com/getscript?v=3.0&ak=KSezYymXPth1DIGILRX3oYN9PxbOQQmU&services=&t=20210201100830&s=1`; |  | ||||||
|  |  | ||||||
| /** 高德地图sdk地址 */ |  | ||||||
| export const GAODE_MAP_SDK_URL = 'https://webapi.amap.com/maps?v=2.0&key=e7bd02bd504062087e6563daf4d6721d'; |  | ||||||
|  |  | ||||||
| /** 腾讯地图sdk地址 */ |  | ||||||
| export const TENCENT_MAP_SDK_URL = 'https://map.qq.com/api/gljs?v=1.exp&key=A6DBZ-KXPLW-JKSRY-ONZF4-CPHY3-K6BL7'; |  | ||||||
| @@ -14,19 +14,3 @@ export const userRoleLabels: Record<Auth.RoleType, string> = { | |||||||
|   user: '普通用户' |   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,5 +1,4 @@ | |||||||
| 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, useImageVerify }; | export { useCountDown, useSmsCode }; | ||||||
|   | |||||||
| @@ -1,87 +0,0 @@ | |||||||
| 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; |  | ||||||
| } |  | ||||||
| @@ -1,151 +0,0 @@ | |||||||
| import { ref, reactive } from 'vue'; |  | ||||||
| import type { Ref } from 'vue'; |  | ||||||
| import type { DataTableBaseColumn, DataTableSelectionColumn, DataTableExpandColumn, PaginationProps } from 'naive-ui'; |  | ||||||
| import type { TableColumnGroup, InternalRowData } from 'naive-ui/es/data-table/src/interface'; |  | ||||||
| import { useLoadingEmpty } from '../common'; |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * 表格分页参数 |  | ||||||
|  */ |  | ||||||
| type PaginationParams = Pick<PaginationProps, 'page' | 'pageSize'>; |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * 表格请求接口的参数 |  | ||||||
|  */ |  | ||||||
| type ApiParams = Record<string, unknown> & PaginationParams; |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * 表格请求接口的结果 |  | ||||||
|  * @description 这里用属性list来表示后端接口返回的表格数据 |  | ||||||
|  */ |  | ||||||
| type ApiData<TableData = Record<string, unknown>> = Record<string, unknown> & { list: TableData[] }; |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * 表格接口的请求函数 |  | ||||||
|  */ |  | ||||||
| type ApiFn<Params = ApiParams, TableData = Record<string, unknown>> = ( |  | ||||||
|   params: Params |  | ||||||
| ) => Promise<Service.RequestResult<ApiData<TableData>>>; |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * 表格接口请求后转换后的数据 |  | ||||||
|  */ |  | ||||||
| type TransformedTableData<TableData = Record<string, unknown>> = { |  | ||||||
|   data: TableData[]; |  | ||||||
|   pageNum: number; |  | ||||||
|   pageSize: number; |  | ||||||
|   total: number; |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * 表格的列 |  | ||||||
|  */ |  | ||||||
| type DataTableColumn<T = InternalRowData> = |  | ||||||
|   | (Omit<TableColumnGroup<T>, 'key'> & { key: keyof T }) |  | ||||||
|   | (Omit<DataTableBaseColumn<T>, 'key'> & { key: keyof T }) |  | ||||||
|   | DataTableSelectionColumn<T> |  | ||||||
|   | DataTableExpandColumn<T>; |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * 表格数据转换器 |  | ||||||
|  * @description 将不同接口的表格数据转换成统一的类型 |  | ||||||
|  */ |  | ||||||
| type Transformer<TableData = Record<string, unknown>> = ( |  | ||||||
|   apiData: ApiData<TableData> |  | ||||||
| ) => TransformedTableData<TableData>; |  | ||||||
|  |  | ||||||
| type TableParams<TableData = Record<string, unknown>, Params = ApiParams> = { |  | ||||||
|   apiFn: ApiFn<Params, TableData>; |  | ||||||
|   apiParams: Params; |  | ||||||
|   transformer: Transformer<TableData>; |  | ||||||
|   columns: DataTableColumn<TableData>[]; |  | ||||||
|   pagination?: PaginationProps; |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| export function useTable<TableData extends Record<string, unknown>, Params extends ApiParams>( |  | ||||||
|   params: TableParams<TableData, Params>, |  | ||||||
|   immediate = true |  | ||||||
| ) { |  | ||||||
|   const { loading, startLoading, endLoading, empty, setEmpty } = useLoadingEmpty(); |  | ||||||
|   const data: Ref<TableData[]> = ref([]); |  | ||||||
|  |  | ||||||
|   function updateData(update: TableData[]) { |  | ||||||
|     data.value = update; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   let dataSource: TableData[] = []; |  | ||||||
|   function setDataSource(source: TableData[]) { |  | ||||||
|     dataSource = source; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   function resetData() { |  | ||||||
|     data.value = dataSource; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   const columns = ref(params.columns) as Ref<DataTableColumn<TableData>[]>; |  | ||||||
|  |  | ||||||
|   const pagination = reactive({ |  | ||||||
|     ...getPagination(params.pagination), |  | ||||||
|     onChange: (page: number) => { |  | ||||||
|       pagination.page = page; |  | ||||||
|     }, |  | ||||||
|     onUpdatePageSize: (pageSize: number) => { |  | ||||||
|       pagination.pageSize = pageSize; |  | ||||||
|       pagination.page = 1; |  | ||||||
|     } |  | ||||||
|   }) as PaginationProps; |  | ||||||
|  |  | ||||||
|   function updatePagination(update: Partial<PaginationProps>) { |  | ||||||
|     Object.assign(pagination, update); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   async function getData() { |  | ||||||
|     const apiParams: Params = { ...params.apiParams }; |  | ||||||
|     apiParams.page = apiParams.page || pagination.page; |  | ||||||
|     apiParams.pageSize = apiParams.pageSize || pagination.pageSize; |  | ||||||
|  |  | ||||||
|     startLoading(); |  | ||||||
|     const { data: apiData } = await params.apiFn(apiParams); |  | ||||||
|  |  | ||||||
|     if (apiData) { |  | ||||||
|       const transformedData = params.transformer(apiData); |  | ||||||
|  |  | ||||||
|       updateData(transformedData.data); |  | ||||||
|  |  | ||||||
|       setDataSource(transformedData.data); |  | ||||||
|  |  | ||||||
|       setEmpty(transformedData.data.length === 0); |  | ||||||
|  |  | ||||||
|       updatePagination({ page: transformedData.pageNum, pageSize: transformedData.pageSize }); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     endLoading(); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   if (immediate) { |  | ||||||
|     getData(); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   return { |  | ||||||
|     data, |  | ||||||
|     columns, |  | ||||||
|     loading, |  | ||||||
|     empty, |  | ||||||
|     pagination, |  | ||||||
|     getData, |  | ||||||
|     updateData, |  | ||||||
|     resetData |  | ||||||
|   }; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| function getPagination(pagination?: Partial<PaginationProps>) { |  | ||||||
|   const defaultPagination: Partial<PaginationProps> = { |  | ||||||
|     page: 1, |  | ||||||
|     pageSize: 10, |  | ||||||
|     showSizePicker: true, |  | ||||||
|     pageSizes: [10, 15, 20, 25, 30] |  | ||||||
|   }; |  | ||||||
|   Object.assign(defaultPagination, pagination); |  | ||||||
|  |  | ||||||
|   return defaultPagination; |  | ||||||
| } |  | ||||||
| @@ -1,23 +0,0 @@ | |||||||
| <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,23 +1,10 @@ | |||||||
| 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 { | export { MenuCollapse, GlobalBreadcrumb, HeaderMenu, FullScreen, ThemeMode, UserAvatar, SettingButton, ToggleLang }; | ||||||
|   MenuCollapse, |  | ||||||
|   GlobalBreadcrumb, |  | ||||||
|   HeaderMenu, |  | ||||||
|   GithubSite, |  | ||||||
|   FullScreen, |  | ||||||
|   ThemeMode, |  | ||||||
|   UserAvatar, |  | ||||||
|   SystemMessage, |  | ||||||
|   SettingButton, |  | ||||||
|   ToggleLang |  | ||||||
| }; |  | ||||||
|   | |||||||
| @@ -1,57 +0,0 @@ | |||||||
| <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> |  | ||||||
| @@ -1,217 +0,0 @@ | |||||||
| <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,12 +7,9 @@ | |||||||
|     </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> | ||||||
| @@ -23,15 +20,12 @@ | |||||||
| 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 | ||||||
|   | |||||||
| @@ -1,3 +0,0 @@ | |||||||
| import SearchModal from './search-modal.vue'; |  | ||||||
|  |  | ||||||
| export { SearchModal }; |  | ||||||
| @@ -1,27 +0,0 @@ | |||||||
| <template> |  | ||||||
|   <div class="px-24px h-44px flex-y-center"> |  | ||||||
|     <span class="mr-14px flex-y-center"> |  | ||||||
|       <icon-mdi-keyboard-return class="icon text-20px p-2px mr-6px" /> |  | ||||||
|       <span>确认</span> |  | ||||||
|     </span> |  | ||||||
|     <span class="mr-14px flex-y-center"> |  | ||||||
|       <icon-mdi-arrow-up-thin class="icon text-20px p-2px mr-5px" /> |  | ||||||
|       <icon-mdi-arrow-down-thin class="icon text-20px p-2px mr-6px" /> |  | ||||||
|       <span>切换</span> |  | ||||||
|     </span> |  | ||||||
|     <span class="flex-y-center"> |  | ||||||
|       <icon-mdi-keyboard-esc class="icon text-20px p-2px mr-6px" /> |  | ||||||
|       <span>关闭</span> |  | ||||||
|     </span> |  | ||||||
|   </div> |  | ||||||
| </template> |  | ||||||
|  |  | ||||||
| <script lang="ts" setup> |  | ||||||
| defineOptions({ name: 'SearchFooter' }); |  | ||||||
| </script> |  | ||||||
|  |  | ||||||
| <style lang="scss" scoped> |  | ||||||
| .icon { |  | ||||||
|   box-shadow: inset 0 -2px #cdcde6, inset 0 0 1px 1px #fff, 0 1px 2px 1px #1e235a66; |  | ||||||
| } |  | ||||||
| </style> |  | ||||||
| @@ -1,147 +0,0 @@ | |||||||
| <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> |  | ||||||
| @@ -1,64 +0,0 @@ | |||||||
| <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> |  | ||||||
| @@ -1,30 +0,0 @@ | |||||||
| <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,8 +1,5 @@ | |||||||
| 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'; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,17 +0,0 @@ | |||||||
| const about1: AuthRoute.Route = { |  | ||||||
|   name: 'about', |  | ||||||
|   path: '/about', |  | ||||||
|   component: 'self', |  | ||||||
|   meta: { |  | ||||||
|     title: '关于', |  | ||||||
|     i18nTitle: 'message.routes.about', |  | ||||||
|     requiresAuth: true, |  | ||||||
|     keepAlive: true, |  | ||||||
|     singleLayout: 'basic', |  | ||||||
|     permissions: ['super', 'admin', 'user'], |  | ||||||
|     icon: 'fluent:book-information-24-regular', |  | ||||||
|     order: 10 |  | ||||||
|   } |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| export default about1; |  | ||||||
| @@ -1,38 +0,0 @@ | |||||||
| 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: 'message.routes.auth-demo.permission', |  | ||||||
|         requiresAuth: true, |  | ||||||
|         icon: 'ic:round-construction' |  | ||||||
|       } |  | ||||||
|     }, |  | ||||||
|     { |  | ||||||
|       name: 'auth-demo_super', |  | ||||||
|       path: '/auth-demo/super', |  | ||||||
|       component: 'self', |  | ||||||
|       meta: { |  | ||||||
|         title: '超级管理员可见', |  | ||||||
|         i18nTitle: 'message.routes.auth-demo.super', |  | ||||||
|         requiresAuth: true, |  | ||||||
|         permissions: ['super'], |  | ||||||
|         icon: 'ic:round-supervisor-account' |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|   ], |  | ||||||
|   meta: { |  | ||||||
|     title: '权限示例', |  | ||||||
|     i18nTitle: 'message.routes.auth-demo._value', |  | ||||||
|     icon: 'ic:baseline-security', |  | ||||||
|     order: 5 |  | ||||||
|   } |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| export default authDemo; |  | ||||||
| @@ -1,48 +0,0 @@ | |||||||
| const component: AuthRoute.Route = { |  | ||||||
|   name: 'component', |  | ||||||
|   path: '/component', |  | ||||||
|   component: 'basic', |  | ||||||
|   children: [ |  | ||||||
|     { |  | ||||||
|       name: 'component_button', |  | ||||||
|       path: '/component/button', |  | ||||||
|       component: 'self', |  | ||||||
|       meta: { |  | ||||||
|         title: '按钮', |  | ||||||
|         i18nTitle: 'message.routes.component.button', |  | ||||||
|         requiresAuth: true, |  | ||||||
|         icon: 'mdi:button-cursor' |  | ||||||
|       } |  | ||||||
|     }, |  | ||||||
|     { |  | ||||||
|       name: 'component_card', |  | ||||||
|       path: '/component/card', |  | ||||||
|       component: 'self', |  | ||||||
|       meta: { |  | ||||||
|         title: '卡片', |  | ||||||
|         i18nTitle: 'message.routes.component.card', |  | ||||||
|         requiresAuth: true, |  | ||||||
|         icon: 'mdi:card-outline' |  | ||||||
|       } |  | ||||||
|     }, |  | ||||||
|     { |  | ||||||
|       name: 'component_table', |  | ||||||
|       path: '/component/table', |  | ||||||
|       component: 'self', |  | ||||||
|       meta: { |  | ||||||
|         title: '表格', |  | ||||||
|         i18nTitle: 'message.routes.component.table', |  | ||||||
|         requiresAuth: true, |  | ||||||
|         icon: 'mdi:table-large' |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|   ], |  | ||||||
|   meta: { |  | ||||||
|     title: '组件示例', |  | ||||||
|     i18nTitle: 'message.routes.component._value', |  | ||||||
|     icon: 'cib:app-store', |  | ||||||
|     order: 3 |  | ||||||
|   } |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| export default component; |  | ||||||
| @@ -1,37 +0,0 @@ | |||||||
| 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: 'message.routes.dashboard.analysis' |  | ||||||
|       } |  | ||||||
|     }, |  | ||||||
|     { |  | ||||||
|       name: 'dashboard_workbench', |  | ||||||
|       path: '/dashboard/workbench', |  | ||||||
|       component: 'self', |  | ||||||
|       meta: { |  | ||||||
|         title: '工作台', |  | ||||||
|         requiresAuth: true, |  | ||||||
|         icon: 'icon-park-outline:workbench', |  | ||||||
|         i18nTitle: 'message.routes.dashboard.workbench' |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|   ], |  | ||||||
|   meta: { |  | ||||||
|     title: '仪表盘', |  | ||||||
|     icon: 'mdi:monitor-dashboard', |  | ||||||
|     order: 1, |  | ||||||
|     i18nTitle: 'message.routes.dashboard._value' |  | ||||||
|   } |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| export default dashboard; |  | ||||||
| @@ -1,70 +0,0 @@ | |||||||
| const document: AuthRoute.Route = { |  | ||||||
|   name: 'document', |  | ||||||
|   path: '/document', |  | ||||||
|   component: 'basic', |  | ||||||
|   children: [ |  | ||||||
|     { |  | ||||||
|       name: 'document_vue', |  | ||||||
|       path: '/document/vue', |  | ||||||
|       component: 'self', |  | ||||||
|       meta: { |  | ||||||
|         title: 'vue文档', |  | ||||||
|         i18nTitle: 'message.routes.document.vue', |  | ||||||
|         requiresAuth: true, |  | ||||||
|         icon: 'logos:vue' |  | ||||||
|       } |  | ||||||
|     }, |  | ||||||
|     { |  | ||||||
|       name: 'document_vite', |  | ||||||
|       path: '/document/vite', |  | ||||||
|       component: 'self', |  | ||||||
|       meta: { |  | ||||||
|         title: 'vite文档', |  | ||||||
|         i18nTitle: 'message.routes.document.vite', |  | ||||||
|         requiresAuth: true, |  | ||||||
|         icon: 'logos:vitejs' |  | ||||||
|       } |  | ||||||
|     }, |  | ||||||
|     { |  | ||||||
|       name: 'document_naive', |  | ||||||
|       path: '/document/naive', |  | ||||||
|       component: 'self', |  | ||||||
|       meta: { |  | ||||||
|         title: 'naive文档', |  | ||||||
|         i18nTitle: 'message.routes.document.naive', |  | ||||||
|         requiresAuth: true, |  | ||||||
|         icon: 'logos:naiveui' |  | ||||||
|       } |  | ||||||
|     }, |  | ||||||
|     { |  | ||||||
|       name: 'document_project', |  | ||||||
|       path: '/document/project', |  | ||||||
|       component: 'self', |  | ||||||
|       meta: { |  | ||||||
|         title: '项目文档', |  | ||||||
|         i18nTitle: 'message.routes.document.project', |  | ||||||
|         requiresAuth: true, |  | ||||||
|         localIcon: 'logo' |  | ||||||
|       } |  | ||||||
|     }, |  | ||||||
|     { |  | ||||||
|       name: 'document_project-link', |  | ||||||
|       path: '/document/project-link', |  | ||||||
|       meta: { |  | ||||||
|         title: '项目文档(外链)', |  | ||||||
|         i18nTitle: 'message.routes.document.project-link', |  | ||||||
|         requiresAuth: true, |  | ||||||
|         localIcon: 'logo', |  | ||||||
|         href: 'https://docs.soybean.pro/' |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|   ], |  | ||||||
|   meta: { |  | ||||||
|     title: '文档', |  | ||||||
|     i18nTitle: 'message.routes.document._value', |  | ||||||
|     icon: 'mdi:file-document-multiple-outline', |  | ||||||
|     order: 2 |  | ||||||
|   } |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| export default document; |  | ||||||
| @@ -1,48 +0,0 @@ | |||||||
| const exception: AuthRoute.Route = { |  | ||||||
|   name: 'exception', |  | ||||||
|   path: '/exception', |  | ||||||
|   component: 'basic', |  | ||||||
|   children: [ |  | ||||||
|     { |  | ||||||
|       name: 'exception_403', |  | ||||||
|       path: '/exception/403', |  | ||||||
|       component: 'self', |  | ||||||
|       meta: { |  | ||||||
|         title: '异常页403', |  | ||||||
|         i18nTitle: 'message.routes.exception.403', |  | ||||||
|         requiresAuth: true, |  | ||||||
|         icon: 'ic:baseline-block' |  | ||||||
|       } |  | ||||||
|     }, |  | ||||||
|     { |  | ||||||
|       name: 'exception_404', |  | ||||||
|       path: '/exception/404', |  | ||||||
|       component: 'self', |  | ||||||
|       meta: { |  | ||||||
|         title: '异常页404', |  | ||||||
|         i18nTitle: 'message.routes.exception.404', |  | ||||||
|         requiresAuth: true, |  | ||||||
|         icon: 'ic:baseline-web-asset-off' |  | ||||||
|       } |  | ||||||
|     }, |  | ||||||
|     { |  | ||||||
|       name: 'exception_500', |  | ||||||
|       path: '/exception/500', |  | ||||||
|       component: 'self', |  | ||||||
|       meta: { |  | ||||||
|         title: '异常页500', |  | ||||||
|         i18nTitle: 'message.routes.exception.500', |  | ||||||
|         requiresAuth: true, |  | ||||||
|         icon: 'ic:baseline-wifi-off' |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|   ], |  | ||||||
|   meta: { |  | ||||||
|     i18nTitle: 'message.routes.exception._value', |  | ||||||
|     title: '异常页', |  | ||||||
|     icon: 'ant-design:exception-outlined', |  | ||||||
|     order: 7 |  | ||||||
|   } |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| export default exception; |  | ||||||
| @@ -1,51 +0,0 @@ | |||||||
| const functionRoute: AuthRoute.Route = { |  | ||||||
|   name: 'function', |  | ||||||
|   path: '/function', |  | ||||||
|   component: 'basic', |  | ||||||
|   children: [ |  | ||||||
|     { |  | ||||||
|       name: 'function_tab', |  | ||||||
|       path: '/function/tab', |  | ||||||
|       component: 'self', |  | ||||||
|       meta: { |  | ||||||
|         title: 'Tab', |  | ||||||
|         i18nTitle: 'message.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: 'message.routes.function._value', |  | ||||||
|     icon: 'icon-park-outline:all-application', |  | ||||||
|     order: 6 |  | ||||||
|   } |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| export default functionRoute; |  | ||||||
| @@ -1,59 +0,0 @@ | |||||||
| const management: AuthRoute.Route = { |  | ||||||
|   name: 'management', |  | ||||||
|   path: '/management', |  | ||||||
|   component: 'basic', |  | ||||||
|   children: [ |  | ||||||
|     { |  | ||||||
|       name: 'management_auth', |  | ||||||
|       path: '/management/auth', |  | ||||||
|       component: 'self', |  | ||||||
|       meta: { |  | ||||||
|         title: '权限管理', |  | ||||||
|         i18nTitle: 'message.routes.management.auth', |  | ||||||
|         requiresAuth: true, |  | ||||||
|         icon: 'ic:baseline-security' |  | ||||||
|       } |  | ||||||
|     }, |  | ||||||
|     { |  | ||||||
|       name: 'management_role', |  | ||||||
|       path: '/management/role', |  | ||||||
|       component: 'self', |  | ||||||
|       meta: { |  | ||||||
|         title: '角色管理', |  | ||||||
|         i18nTitle: 'message.routes.management.role', |  | ||||||
|         requiresAuth: true, |  | ||||||
|         icon: 'carbon:user-role' |  | ||||||
|       } |  | ||||||
|     }, |  | ||||||
|     { |  | ||||||
|       name: 'management_user', |  | ||||||
|       path: '/management/user', |  | ||||||
|       component: 'self', |  | ||||||
|       meta: { |  | ||||||
|         title: '用户管理', |  | ||||||
|         i18nTitle: 'message.routes.management.user', |  | ||||||
|         requiresAuth: true, |  | ||||||
|         icon: 'ic:round-manage-accounts' |  | ||||||
|       } |  | ||||||
|     }, |  | ||||||
|     { |  | ||||||
|       name: 'management_route', |  | ||||||
|       path: '/management/route', |  | ||||||
|       component: 'self', |  | ||||||
|       meta: { |  | ||||||
|         title: '路由管理', |  | ||||||
|         i18nTitle: 'message.routes.management.route', |  | ||||||
|         requiresAuth: true, |  | ||||||
|         icon: 'material-symbols:route' |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|   ], |  | ||||||
|   meta: { |  | ||||||
|     title: '系统管理', |  | ||||||
|     i18nTitle: 'message.routes.management._value', |  | ||||||
|     icon: 'carbon:cloud-service-management', |  | ||||||
|     order: 9 |  | ||||||
|   } |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| export default management; |  | ||||||
| @@ -1,149 +0,0 @@ | |||||||
| 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: 'message.routes.plugin.charts.echarts', |  | ||||||
|             requiresAuth: true, |  | ||||||
|             icon: 'simple-icons:apacheecharts' |  | ||||||
|           } |  | ||||||
|         }, |  | ||||||
|         { |  | ||||||
|           name: 'plugin_charts_antv', |  | ||||||
|           path: '/plugin/charts/antv', |  | ||||||
|           component: 'self', |  | ||||||
|           meta: { |  | ||||||
|             title: 'AntV', |  | ||||||
|             i18nTitle: 'message.routes.plugin.charts.antv', |  | ||||||
|             requiresAuth: true, |  | ||||||
|             icon: 'simple-icons:antdesign' |  | ||||||
|           } |  | ||||||
|         } |  | ||||||
|       ], |  | ||||||
|       meta: { |  | ||||||
|         title: '图表', |  | ||||||
|         i18nTitle: 'message.routes.plugin.charts._value', |  | ||||||
|         icon: 'mdi:chart-areaspline' |  | ||||||
|       } |  | ||||||
|     }, |  | ||||||
|     { |  | ||||||
|       name: 'plugin_map', |  | ||||||
|       path: '/plugin/map', |  | ||||||
|       component: 'self', |  | ||||||
|       meta: { |  | ||||||
|         title: '地图', |  | ||||||
|         i18nTitle: 'message.routes.plugin.map', |  | ||||||
|         requiresAuth: true, |  | ||||||
|         icon: 'mdi:map' |  | ||||||
|       } |  | ||||||
|     }, |  | ||||||
|     { |  | ||||||
|       name: 'plugin_video', |  | ||||||
|       path: '/plugin/video', |  | ||||||
|       component: 'self', |  | ||||||
|       meta: { |  | ||||||
|         title: '视频', |  | ||||||
|         i18nTitle: 'message.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: 'message.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: 'message.routes.plugin.editor.markdown', |  | ||||||
|             requiresAuth: true, |  | ||||||
|             icon: 'ri:markdown-line' |  | ||||||
|           } |  | ||||||
|         } |  | ||||||
|       ], |  | ||||||
|       meta: { |  | ||||||
|         title: '编辑器', |  | ||||||
|         i18nTitle: 'message.routes.plugin.editor._value', |  | ||||||
|         icon: 'icon-park-outline:editor' |  | ||||||
|       } |  | ||||||
|     }, |  | ||||||
|     { |  | ||||||
|       name: 'plugin_swiper', |  | ||||||
|       path: '/plugin/swiper', |  | ||||||
|       component: 'self', |  | ||||||
|       meta: { |  | ||||||
|         title: 'Swiper插件', |  | ||||||
|         i18nTitle: 'message.routes.plugin.swiper', |  | ||||||
|         requiresAuth: true, |  | ||||||
|         icon: 'simple-icons:swiper' |  | ||||||
|       } |  | ||||||
|     }, |  | ||||||
|     { |  | ||||||
|       name: 'plugin_copy', |  | ||||||
|       path: '/plugin/copy', |  | ||||||
|       component: 'self', |  | ||||||
|       meta: { |  | ||||||
|         title: '剪贴板', |  | ||||||
|         i18nTitle: 'message.routes.plugin.copy', |  | ||||||
|         requiresAuth: true, |  | ||||||
|         icon: 'mdi:clipboard-outline' |  | ||||||
|       } |  | ||||||
|     }, |  | ||||||
|     { |  | ||||||
|       name: 'plugin_icon', |  | ||||||
|       path: '/plugin/icon', |  | ||||||
|       component: 'self', |  | ||||||
|       meta: { |  | ||||||
|         title: '图标', |  | ||||||
|         i18nTitle: 'message.routes.plugin.icon', |  | ||||||
|         requiresAuth: true, |  | ||||||
|         localIcon: 'custom-icon' |  | ||||||
|       } |  | ||||||
|     }, |  | ||||||
|     { |  | ||||||
|       name: 'plugin_print', |  | ||||||
|       path: '/plugin/print', |  | ||||||
|       component: 'self', |  | ||||||
|       meta: { |  | ||||||
|         title: '打印', |  | ||||||
|         i18nTitle: 'message.routes.plugin.print', |  | ||||||
|         requiresAuth: true, |  | ||||||
|         icon: 'mdi:printer' |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|   ], |  | ||||||
|   meta: { |  | ||||||
|     title: '插件示例', |  | ||||||
|     i18nTitle: 'message.routes.plugin._value', |  | ||||||
|     icon: 'clarity:plugin-line', |  | ||||||
|     order: 4 |  | ||||||
|   } |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| export default plugin; |  | ||||||
| @@ -1,2 +1 @@ | |||||||
| export * from './auth'; | export * from './auth'; | ||||||
| export * from './management'; |  | ||||||
|   | |||||||
| @@ -1,13 +0,0 @@ | |||||||
| 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; |  | ||||||
|   }); |  | ||||||
| } |  | ||||||
| @@ -1,9 +0,0 @@ | |||||||
| 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); |  | ||||||
| }; |  | ||||||
							
								
								
									
										29
									
								
								src/typings/api.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										29
									
								
								src/typings/api.d.ts
									
									
									
									
										vendored
									
									
								
							| @@ -21,32 +21,3 @@ 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
									
									
								
							
							
						
						
									
										25
									
								
								src/typings/business.d.ts
									
									
									
									
										vendored
									
									
								
							| @@ -18,28 +18,3 @@ 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
									
									
								
							
							
						
						
									
										3
									
								
								src/typings/global.d.ts
									
									
									
									
										vendored
									
									
								
							| @@ -16,6 +16,3 @@ 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
									
									
								
							
							
						
						
									
										3
									
								
								src/typings/naive-ui.d.ts
									
									
									
									
										vendored
									
									
								
							| @@ -1,3 +0,0 @@ | |||||||
| declare namespace NaiveUI { |  | ||||||
|   type ThemeColor = 'default' | 'error' | 'primary' | 'info' | 'success' | 'warning'; |  | ||||||
| } |  | ||||||
							
								
								
									
										9
									
								
								src/typings/package.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										9
									
								
								src/typings/package.d.ts
									
									
									
									
										vendored
									
									
								
							| @@ -1,9 +0,0 @@ | |||||||
| /// <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; |  | ||||||
							
								
								
									
										78
									
								
								src/typings/page-route.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										78
									
								
								src/typings/page-route.d.ts
									
									
									
									
										vendored
									
									
								
							| @@ -22,54 +22,11 @@ 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' |  | ||||||
|     | '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 | ||||||
| @@ -83,40 +40,7 @@ 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' |  | ||||||
|     | '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' |  | ||||||
|   >; |   >; | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,19 +0,0 @@ | |||||||
| <template> |  | ||||||
|   <n-card title="开发环境依赖" :bordered="false" size="small" class="rounded-16px 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> |  | ||||||
| @@ -1,6 +0,0 @@ | |||||||
| 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 }; |  | ||||||
| @@ -1,39 +0,0 @@ | |||||||
| 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)) |  | ||||||
| }; |  | ||||||
| @@ -1,19 +0,0 @@ | |||||||
| <template> |  | ||||||
|   <n-card title="生产环境依赖" :bordered="false" size="small" class="rounded-16px 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> |  | ||||||
| @@ -1,29 +0,0 @@ | |||||||
| <template> |  | ||||||
|   <n-card title="项目信息" :bordered="false" size="small" class="rounded-16px 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://soybean.pro" 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> |  | ||||||
| @@ -1,14 +0,0 @@ | |||||||
| <template> |  | ||||||
|   <n-card title="关于" :bordered="false" size="large" class="rounded-16px 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> |  | ||||||
| @@ -1,14 +0,0 @@ | |||||||
| <template> |  | ||||||
|   <n-space :vertical="true" :size="16"> |  | ||||||
|     <project-introduction /> |  | ||||||
|     <project-info /> |  | ||||||
|     <pro-dependency /> |  | ||||||
|     <dev-dependency /> |  | ||||||
|   </n-space> |  | ||||||
| </template> |  | ||||||
|  |  | ||||||
| <script setup lang="ts"> |  | ||||||
| import { DevDependency, ProDependency, ProjectInfo, ProjectIntroduction } from './components'; |  | ||||||
| </script> |  | ||||||
|  |  | ||||||
| <style scoped></style> |  | ||||||
| @@ -1,55 +0,0 @@ | |||||||
| <template> |  | ||||||
|   <div class="h-full"> |  | ||||||
|     <n-card title="权限切换" class="h-full shadow-sm rounded-16px"> |  | ||||||
|       <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> |  | ||||||
| @@ -1,9 +0,0 @@ | |||||||
| <template> |  | ||||||
|   <div class="h-full"> |  | ||||||
|     <n-card title="当前页面只有super才能看到" class="h-full shadow-sm rounded-16px"></n-card> |  | ||||||
|   </div> |  | ||||||
| </template> |  | ||||||
|  |  | ||||||
| <script setup lang="ts"></script> |  | ||||||
|  |  | ||||||
| <style scoped></style> |  | ||||||
| @@ -1,575 +0,0 @@ | |||||||
| <template> |  | ||||||
|   <div> |  | ||||||
|     <n-card title="按钮" class="h-full shadow-sm rounded-16px"> |  | ||||||
|       <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> |  | ||||||
| @@ -1,42 +0,0 @@ | |||||||
| <template> |  | ||||||
|   <div> |  | ||||||
|     <n-card title="卡片" class="h-full shadow-sm rounded-16px"> |  | ||||||
|       <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> |  | ||||||
| @@ -1,107 +0,0 @@ | |||||||
| <template> |  | ||||||
|   <div class="h-full overflow-hidden"> |  | ||||||
|     <n-card title="表格" class="h-full shadow-sm rounded-16px"> |  | ||||||
|       <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[] = [ |  | ||||||
|   { |  | ||||||
|     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: () => { |  | ||||||
|       return ( |  | ||||||
|         <NSpace justify={'center'}> |  | ||||||
|           <NButton size={'small'} onClick={() => {}}> |  | ||||||
|             编辑 |  | ||||||
|           </NButton> |  | ||||||
|           <NPopconfirm onPositiveClick={() => {}}> |  | ||||||
|             {{ |  | ||||||
|               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); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| onMounted(() => { |  | ||||||
|   getDataSource(); |  | ||||||
| }); |  | ||||||
| </script> |  | ||||||
|  |  | ||||||
| <style scoped></style> |  | ||||||
| @@ -1,136 +0,0 @@ | |||||||
| <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-16px 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-16px 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> |  | ||||||
| @@ -1,25 +0,0 @@ | |||||||
| <template> |  | ||||||
|   <div class="p-16px rounded-16px 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> |  | ||||||
| @@ -1,3 +0,0 @@ | |||||||
| import GradientBg from './gradient-bg.vue'; |  | ||||||
|  |  | ||||||
| export { GradientBg }; |  | ||||||
| @@ -1,70 +0,0 @@ | |||||||
| <template> |  | ||||||
|   <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> |  | ||||||
| </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> |  | ||||||
| @@ -1,5 +0,0 @@ | |||||||
| 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 }; |  | ||||||
| @@ -1,184 +0,0 @@ | |||||||
| <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-16px 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-16px 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-16px 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> |  | ||||||
| @@ -1,13 +0,0 @@ | |||||||
| <template> |  | ||||||
|   <n-space :vertical="true" :size="16"> |  | ||||||
|     <top-chart /> |  | ||||||
|     <data-card /> |  | ||||||
|     <bottom-part /> |  | ||||||
|   </n-space> |  | ||||||
| </template> |  | ||||||
|  |  | ||||||
| <script lang="ts" setup> |  | ||||||
| import { BottomPart, DataCard, TopChart } from './components'; |  | ||||||
| </script> |  | ||||||
|  |  | ||||||
| <style scoped></style> |  | ||||||
| @@ -1,4 +0,0 @@ | |||||||
| import WorkbenchHeader from './workbench-header/index.vue'; |  | ||||||
| import WorkbenchMain from './workbench-main/index.vue'; |  | ||||||
|  |  | ||||||
| export { WorkbenchHeader, WorkbenchMain }; |  | ||||||
| @@ -1,50 +0,0 @@ | |||||||
| <template> |  | ||||||
|   <n-card :bordered="false" class="rounded-16px shadow-sm"> |  | ||||||
|     <div class="flex-y-center justify-between"> |  | ||||||
|       <div class="flex-y-center"> |  | ||||||
|         <icon-local-avatar class="text-70px" /> |  | ||||||
|         <div class="pl-12px"> |  | ||||||
|           <h3 class="text-18px font-semibold">早安,{{ auth.userInfo.userName }}, 今天又是充满活力的一天!</h3> |  | ||||||
|           <p class="leading-30px text-#999">今日多云转晴,20℃ - 25℃!</p> |  | ||||||
|         </div> |  | ||||||
|       </div> |  | ||||||
|       <n-space :size="24" :wrap="false"> |  | ||||||
|         <n-statistic v-for="item in statisticData" :key="item.id" class="whitespace-nowrap" v-bind="item"></n-statistic> |  | ||||||
|       </n-space> |  | ||||||
|     </div> |  | ||||||
|   </n-card> |  | ||||||
| </template> |  | ||||||
|  |  | ||||||
| <script setup lang="ts"> |  | ||||||
| import { useAuthStore } from '@/store'; |  | ||||||
|  |  | ||||||
| defineOptions({ name: 'DashboardWorkbenchHeader' }); |  | ||||||
|  |  | ||||||
| const auth = useAuthStore(); |  | ||||||
|  |  | ||||||
| interface StatisticData { |  | ||||||
|   id: number; |  | ||||||
|   label: string; |  | ||||||
|   value: string; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| const statisticData: StatisticData[] = [ |  | ||||||
|   { |  | ||||||
|     id: 0, |  | ||||||
|     label: '项目数', |  | ||||||
|     value: '25' |  | ||||||
|   }, |  | ||||||
|   { |  | ||||||
|     id: 1, |  | ||||||
|     label: '待办', |  | ||||||
|     value: '4/16' |  | ||||||
|   }, |  | ||||||
|   { |  | ||||||
|     id: 2, |  | ||||||
|     label: '消息', |  | ||||||
|     value: '12' |  | ||||||
|   } |  | ||||||
| ]; |  | ||||||
| </script> |  | ||||||
|  |  | ||||||
| <style scoped></style> |  | ||||||
| @@ -1,4 +0,0 @@ | |||||||
| import TechnologyCard from './technology-card.vue'; |  | ||||||
| import ShortcutsCard from './shortcuts-card.vue'; |  | ||||||
|  |  | ||||||
| export { TechnologyCard, ShortcutsCard }; |  | ||||||
| @@ -1,25 +0,0 @@ | |||||||
| <template> |  | ||||||
|   <div |  | ||||||
|     class="flex-col-center h-120px p-12px border-1px border-#efeff5 dark:border-#ffffff17 rounded-4px hover:shadow-sm cursor-pointer" |  | ||||||
|   > |  | ||||||
|     <svg-icon :icon="icon" :style="{ color: iconColor }" class="text-30px" /> |  | ||||||
|     <p class="py-8px text-16px">{{ label }}</p> |  | ||||||
|   </div> |  | ||||||
| </template> |  | ||||||
|  |  | ||||||
| <script setup lang="ts"> |  | ||||||
| defineOptions({ name: 'DashboardWorkbenchMainShortcutsCard' }); |  | ||||||
|  |  | ||||||
| interface Props { |  | ||||||
|   /** 快捷操作名称 */ |  | ||||||
|   label: string; |  | ||||||
|   /** 图标 */ |  | ||||||
|   icon: string; |  | ||||||
|   /** 图标颜色 */ |  | ||||||
|   iconColor: string; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| defineProps<Props>(); |  | ||||||
| </script> |  | ||||||
|  |  | ||||||
| <style scoped></style> |  | ||||||
| @@ -1,42 +0,0 @@ | |||||||
| <template> |  | ||||||
|   <div |  | ||||||
|     class="h-120px p-4px border-1px border-#efeff5 dark:border-#ffffff17 rounded-4px hover:shadow-sm cursor-pointer" |  | ||||||
|     @click="handleOpenSite" |  | ||||||
|   > |  | ||||||
|     <header class="flex-y-center"> |  | ||||||
|       <svg-icon :icon="icon" :style="{ color: iconColor }" class="text-30px" /> |  | ||||||
|       <h3 class="pl-12px text-18px font-semibold">{{ name }}</h3> |  | ||||||
|     </header> |  | ||||||
|     <p class="py-8px h-56px text-#999">{{ description }}</p> |  | ||||||
|     <div class="flex justify-end"> |  | ||||||
|       <span>{{ author }}</span> |  | ||||||
|     </div> |  | ||||||
|   </div> |  | ||||||
| </template> |  | ||||||
|  |  | ||||||
| <script setup lang="ts"> |  | ||||||
| defineOptions({ name: 'DashboardWorkbenchMainTechnologyCard' }); |  | ||||||
|  |  | ||||||
| interface Props { |  | ||||||
|   /** 技术名称 */ |  | ||||||
|   name: string; |  | ||||||
|   /** 技术描述 */ |  | ||||||
|   description: string; |  | ||||||
|   /** 技术作者 */ |  | ||||||
|   author: string; |  | ||||||
|   /** 技术官网 */ |  | ||||||
|   site: string; |  | ||||||
|   /** 技术图标 */ |  | ||||||
|   icon: string; |  | ||||||
|   /** 图标颜色 */ |  | ||||||
|   iconColor?: string; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| const props = defineProps<Props>(); |  | ||||||
|  |  | ||||||
| function handleOpenSite() { |  | ||||||
|   window.open(props.site, '_blank'); |  | ||||||
| } |  | ||||||
| </script> |  | ||||||
|  |  | ||||||
| <style scoped></style> |  | ||||||
| @@ -1,146 +0,0 @@ | |||||||
| <template> |  | ||||||
|   <n-grid :item-responsive="true" :x-gap="16" :y-gap="16"> |  | ||||||
|     <n-grid-item span="0:24 640:24 1024:16"> |  | ||||||
|       <n-space :vertical="true" :size="16"> |  | ||||||
|         <n-card title="项目主要技术栈" :bordered="false" size="small" class="shadow-sm rounded-16px"> |  | ||||||
|           <template #header-extra> |  | ||||||
|             <a class="text-primary" href="javascript:;">更多技术栈</a> |  | ||||||
|           </template> |  | ||||||
|           <n-grid :item-responsive="true" responsive="screen" cols="m:2 l:3" :x-gap="8" :y-gap="8"> |  | ||||||
|             <n-grid-item v-for="item in technology" :key="item.id"> |  | ||||||
|               <technology-card v-bind="item" /> |  | ||||||
|             </n-grid-item> |  | ||||||
|           </n-grid> |  | ||||||
|         </n-card> |  | ||||||
|         <n-card title="动态" :bordered="false" size="small" class="shadow-sm rounded-16px"> |  | ||||||
|           <template #header-extra> |  | ||||||
|             <a class="text-primary" href="javascript:;">更多动态</a> |  | ||||||
|           </template> |  | ||||||
|           <n-list> |  | ||||||
|             <n-list-item v-for="item in activity" :key="item.id"> |  | ||||||
|               <template #prefix> |  | ||||||
|                 <icon-local-avatar class="text-48px" /> |  | ||||||
|               </template> |  | ||||||
|               <n-thing :title="item.content" :description="item.time" /> |  | ||||||
|             </n-list-item> |  | ||||||
|           </n-list> |  | ||||||
|         </n-card> |  | ||||||
|       </n-space> |  | ||||||
|     </n-grid-item> |  | ||||||
|     <n-grid-item span="0:24 640:24 1024:8"> |  | ||||||
|       <n-space :vertical="true" :size="16"> |  | ||||||
|         <n-card title="快捷操作" :bordered="false" size="small" class="shadow-sm rounded-16px"> |  | ||||||
|           <n-grid :item-responsive="true" responsive="screen" cols="m:2 l:3" :x-gap="8" :y-gap="8"> |  | ||||||
|             <n-grid-item v-for="item in shortcuts" :key="item.id"> |  | ||||||
|               <shortcuts-card v-bind="item" /> |  | ||||||
|             </n-grid-item> |  | ||||||
|           </n-grid> |  | ||||||
|         </n-card> |  | ||||||
|         <n-card title="创意" :bordered="false" size="small" class="shadow-sm rounded-16px"> |  | ||||||
|           <div class="flex-center h-380px"> |  | ||||||
|             <icon-local-banner class="text-400px sm:text-320px text-primary" /> |  | ||||||
|           </div> |  | ||||||
|         </n-card> |  | ||||||
|       </n-space> |  | ||||||
|     </n-grid-item> |  | ||||||
|   </n-grid> |  | ||||||
| </template> |  | ||||||
|  |  | ||||||
| <script setup lang="ts"> |  | ||||||
| import { ShortcutsCard, TechnologyCard } from './components'; |  | ||||||
|  |  | ||||||
| defineOptions({ name: 'DashboardWorkbenchMain' }); |  | ||||||
|  |  | ||||||
| interface Technology { |  | ||||||
|   id: number; |  | ||||||
|   name: string; |  | ||||||
|   description: string; |  | ||||||
|   author: string; |  | ||||||
|   site: string; |  | ||||||
|   icon: string; |  | ||||||
|   iconColor?: string; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| const technology: Technology[] = [ |  | ||||||
|   { |  | ||||||
|     id: 0, |  | ||||||
|     name: 'Vue', |  | ||||||
|     description: '一套用于构建用户界面的渐进式框架', |  | ||||||
|     author: '尤雨溪 - Evan You', |  | ||||||
|     site: 'https://v3.cn.vuejs.org/', |  | ||||||
|     icon: 'logos:vue' |  | ||||||
|   }, |  | ||||||
|   { |  | ||||||
|     id: 1, |  | ||||||
|     name: 'TypeScript', |  | ||||||
|     description: 'JavaScript类型的超集,它可以编译成纯JavaScript', |  | ||||||
|     author: '微软 - Microsoft', |  | ||||||
|     site: 'https://www.typescriptlang.org/', |  | ||||||
|     icon: 'logos:typescript-icon' |  | ||||||
|   }, |  | ||||||
|   { |  | ||||||
|     id: 2, |  | ||||||
|     name: 'Vite', |  | ||||||
|     description: '下一代前端开发与构建工具', |  | ||||||
|     author: '尤雨溪 - Evan You', |  | ||||||
|     site: 'https://vitejs.cn/', |  | ||||||
|     icon: 'logos:vitejs' |  | ||||||
|   }, |  | ||||||
|   { |  | ||||||
|     id: 3, |  | ||||||
|     name: 'NaiveUI', |  | ||||||
|     description: '一个 Vue 3 组件库', |  | ||||||
|     author: '图森未来 - TuSimple', |  | ||||||
|     site: 'https://www.naiveui.com/zh-CN/os-theme', |  | ||||||
|     icon: 'logos:naiveui' |  | ||||||
|   }, |  | ||||||
|   { |  | ||||||
|     id: 4, |  | ||||||
|     name: 'UnoCSS', |  | ||||||
|     description: '下一代实用优先的CSS框架', |  | ||||||
|     author: 'Anthony Fu', |  | ||||||
|     site: 'https://uno.antfu.me/?s=', |  | ||||||
|     icon: 'logos:unocss' |  | ||||||
|   }, |  | ||||||
|   { |  | ||||||
|     id: 5, |  | ||||||
|     name: 'Pinia', |  | ||||||
|     description: 'vue状态管理框架,支持vue2、vue3', |  | ||||||
|     author: 'Posva', |  | ||||||
|     site: 'https://pinia.esm.dev/', |  | ||||||
|     icon: 'noto:pineapple' |  | ||||||
|   } |  | ||||||
| ]; |  | ||||||
|  |  | ||||||
| interface Activity { |  | ||||||
|   id: number; |  | ||||||
|   content: string; |  | ||||||
|   time: string; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| const activity: Activity[] = [ |  | ||||||
|   { id: 4, content: 'Soybean 刚才把工作台页面随便写了一些,凑合能看了!', time: '2021-11-07 22:45:32' }, |  | ||||||
|   { id: 3, content: 'Soybean 正在忙于为soybean-admin写项目说明文档!', time: '2021-11-03 20:33:31' }, |  | ||||||
|   { id: 2, content: 'Soybean 准备为soybean-admin 1.0的发布做充分的准备工作!', time: '2021-10-31 22:43:12' }, |  | ||||||
|   { id: 1, content: '@yanbowe 向soybean-admin提交了一个bug,多标签栏不会自适应。', time: '2021-10-27 10:24:54' }, |  | ||||||
|   { id: 0, content: 'Soybean 在2021年5月28日创建了开源项目soybean-admin!', time: '2021-05-28 22:22:22' } |  | ||||||
| ]; |  | ||||||
|  |  | ||||||
| interface Shortcuts { |  | ||||||
|   id: number; |  | ||||||
|   label: string; |  | ||||||
|   icon: string; |  | ||||||
|   iconColor: string; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| const shortcuts: Shortcuts[] = [ |  | ||||||
|   { id: 0, label: '主控台', icon: 'mdi:desktop-mac-dashboard', iconColor: '#409eff' }, |  | ||||||
|   { id: 1, label: '系统管理', icon: 'ic:outline-settings', iconColor: '#7238d1' }, |  | ||||||
|   { id: 2, label: '权限管理', icon: 'mdi:family-tree', iconColor: '#f56c6c' }, |  | ||||||
|   { id: 3, label: '组件', icon: 'fluent:app-store-24-filled', iconColor: '#19a2f1' }, |  | ||||||
|   { id: 4, label: '表格', icon: 'mdi:table-large', iconColor: '#fab251' }, |  | ||||||
|   { id: 5, label: '图表', icon: 'mdi:chart-areaspline', iconColor: '#8aca6b' } |  | ||||||
| ]; |  | ||||||
| </script> |  | ||||||
|  |  | ||||||
| <style scoped></style> |  | ||||||
| @@ -1,12 +0,0 @@ | |||||||
| <template> |  | ||||||
|   <n-space :vertical="true" :size="16"> |  | ||||||
|     <workbench-header /> |  | ||||||
|     <workbench-main /> |  | ||||||
|   </n-space> |  | ||||||
| </template> |  | ||||||
|  |  | ||||||
| <script lang="ts" setup> |  | ||||||
| import { WorkbenchHeader, WorkbenchMain } from './components'; |  | ||||||
| </script> |  | ||||||
|  |  | ||||||
| <style scoped></style> |  | ||||||
| @@ -1,13 +0,0 @@ | |||||||
| <template> |  | ||||||
|   <div class="h-full"> |  | ||||||
|     <iframe class="wh-full" :src="src"></iframe> |  | ||||||
|   </div> |  | ||||||
| </template> |  | ||||||
|  |  | ||||||
| <script setup lang="ts"> |  | ||||||
| import { ref } from 'vue'; |  | ||||||
|  |  | ||||||
| const src = ref('https://www.naiveui.com/zh-CN/os-theme/docs/introduction'); |  | ||||||
| </script> |  | ||||||
|  |  | ||||||
| <style scoped></style> |  | ||||||
| @@ -1,7 +0,0 @@ | |||||||
| <template> |  | ||||||
|   <div></div> |  | ||||||
| </template> |  | ||||||
|  |  | ||||||
| <script setup lang="ts"></script> |  | ||||||
|  |  | ||||||
| <style scoped></style> |  | ||||||
| @@ -1,13 +0,0 @@ | |||||||
| <template> |  | ||||||
|   <div class="h-full"> |  | ||||||
|     <iframe class="wh-full" :src="src"></iframe> |  | ||||||
|   </div> |  | ||||||
| </template> |  | ||||||
|  |  | ||||||
| <script setup lang="ts"> |  | ||||||
| import { ref } from 'vue'; |  | ||||||
|  |  | ||||||
| const src = ref('https://docs.soybean.pro/'); |  | ||||||
| </script> |  | ||||||
|  |  | ||||||
| <style scoped></style> |  | ||||||
| @@ -1,13 +0,0 @@ | |||||||
| <template> |  | ||||||
|   <div class="h-full"> |  | ||||||
|     <iframe class="wh-full" :src="src"></iframe> |  | ||||||
|   </div> |  | ||||||
| </template> |  | ||||||
|  |  | ||||||
| <script setup lang="ts"> |  | ||||||
| import { ref } from 'vue'; |  | ||||||
|  |  | ||||||
| const src = ref('https://cn.vitejs.dev/'); |  | ||||||
| </script> |  | ||||||
|  |  | ||||||
| <style scoped></style> |  | ||||||
| @@ -1,13 +0,0 @@ | |||||||
| <template> |  | ||||||
|   <div class="h-full"> |  | ||||||
|     <iframe class="wh-full" :src="src"></iframe> |  | ||||||
|   </div> |  | ||||||
| </template> |  | ||||||
|  |  | ||||||
| <script setup lang="ts"> |  | ||||||
| import { ref } from 'vue'; |  | ||||||
|  |  | ||||||
| const src = ref('https://v3.cn.vuejs.org/'); |  | ||||||
| </script> |  | ||||||
|  |  | ||||||
| <style scoped></style> |  | ||||||
| @@ -1,7 +0,0 @@ | |||||||
| <template> |  | ||||||
|   <exception-base type="403" /> |  | ||||||
| </template> |  | ||||||
|  |  | ||||||
| <script lang="ts" setup></script> |  | ||||||
|  |  | ||||||
| <style scoped></style> |  | ||||||
| @@ -1,7 +0,0 @@ | |||||||
| <template> |  | ||||||
|   <exception-base type="404" /> |  | ||||||
| </template> |  | ||||||
|  |  | ||||||
| <script lang="ts" setup></script> |  | ||||||
|  |  | ||||||
| <style scoped></style> |  | ||||||
| @@ -1,7 +0,0 @@ | |||||||
| <template> |  | ||||||
|   <exception-base type="500" /> |  | ||||||
| </template> |  | ||||||
|  |  | ||||||
| <script lang="ts" setup></script> |  | ||||||
|  |  | ||||||
| <style scoped></style> |  | ||||||
| @@ -1,27 +0,0 @@ | |||||||
| <template> |  | ||||||
|   <n-space :vertical="true" :size="16"> |  | ||||||
|     <n-card title="Tab Detail" :bordered="false" size="small" class="shadow-sm rounded-16px"> |  | ||||||
|       <n-space :vertical="true" :size="12"> |  | ||||||
|         <div>当前路由的描述数据(meta):</div> |  | ||||||
|         <div>{{ route.meta }}</div> |  | ||||||
|         <n-button @click="handleToTab">返回Tab</n-button> |  | ||||||
|       </n-space> |  | ||||||
|     </n-card> |  | ||||||
|   </n-space> |  | ||||||
| </template> |  | ||||||
|  |  | ||||||
| <script setup lang="ts"> |  | ||||||
| import { useRoute } from 'vue-router'; |  | ||||||
| import { routeName } from '@/router'; |  | ||||||
| import { useRouterPush } from '@/composables'; |  | ||||||
|  |  | ||||||
| const { routerPush } = useRouterPush(); |  | ||||||
|  |  | ||||||
| const route = useRoute(); |  | ||||||
|  |  | ||||||
| function handleToTab() { |  | ||||||
|   routerPush({ name: routeName('function_tab') }); |  | ||||||
| } |  | ||||||
| </script> |  | ||||||
|  |  | ||||||
| <style scoped></style> |  | ||||||
| @@ -1,28 +0,0 @@ | |||||||
| <template> |  | ||||||
|   <n-space :vertical="true" :size="16"> |  | ||||||
|     <n-card title="Tab Detail" :bordered="false" size="small" class="shadow-sm rounded-16px"> |  | ||||||
|       <n-space :vertical="true" :size="12"> |  | ||||||
|         <div>当前路由的描述数据(meta):</div> |  | ||||||
|         <div>{{ route.meta }}</div> |  | ||||||
|         <div>当前路由的查询数据(query):</div> |  | ||||||
|         <div>{{ route.query }}</div> |  | ||||||
|         <n-button @click="handleToTab">返回Tab</n-button> |  | ||||||
|       </n-space> |  | ||||||
|     </n-card> |  | ||||||
|   </n-space> |  | ||||||
| </template> |  | ||||||
|  |  | ||||||
| <script setup lang="ts"> |  | ||||||
| import { useRoute } from 'vue-router'; |  | ||||||
| import { routeName } from '@/router'; |  | ||||||
| import { useRouterPush } from '@/composables'; |  | ||||||
|  |  | ||||||
| const route = useRoute(); |  | ||||||
| const { routerPush } = useRouterPush(); |  | ||||||
|  |  | ||||||
| function handleToTab() { |  | ||||||
|   routerPush({ name: routeName('function_tab') }); |  | ||||||
| } |  | ||||||
| </script> |  | ||||||
|  |  | ||||||
| <style scoped></style> |  | ||||||
| @@ -1,44 +0,0 @@ | |||||||
| <template> |  | ||||||
|   <n-space :vertical="true" :size="16"> |  | ||||||
|     <n-card title="Tab Home" :bordered="false" size="small" class="shadow-sm rounded-16px"> |  | ||||||
|       <n-space :vertical="true" :size="12"> |  | ||||||
|         <n-button @click="handleToTabDetail">跳转Tab Detail</n-button> |  | ||||||
|         <n-button @click="handleToTabMultiDetail(1)">跳转Tab Multi Detail 1</n-button> |  | ||||||
|         <n-button @click="handleToTabMultiDetail(2)">跳转Tab Multi Detail 2</n-button> |  | ||||||
|         <n-input-group> |  | ||||||
|           <n-input v-model:value="title" /> |  | ||||||
|           <n-button type="primary" @click="handleSetTitle">设置当前Tab页标题</n-button> |  | ||||||
|         </n-input-group> |  | ||||||
|       </n-space> |  | ||||||
|     </n-card> |  | ||||||
|   </n-space> |  | ||||||
| </template> |  | ||||||
|  |  | ||||||
| <script setup lang="ts"> |  | ||||||
| import { ref } from 'vue'; |  | ||||||
| import { routeName } from '@/router'; |  | ||||||
| import { useTabStore } from '@/store'; |  | ||||||
| import { useRouterPush } from '@/composables'; |  | ||||||
|  |  | ||||||
| const { routerPush } = useRouterPush(); |  | ||||||
| const tabStore = useTabStore(); |  | ||||||
| const title = ref(''); |  | ||||||
|  |  | ||||||
| function handleToTabDetail() { |  | ||||||
|   routerPush({ name: routeName('function_tab-detail'), query: { name: 'abc' }, hash: '#DEMO_HASH' }); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| function handleToTabMultiDetail(num: number) { |  | ||||||
|   routerPush({ name: routeName('function_tab-multi-detail'), query: { name: 'abc', num }, hash: '#DEMO_HASH' }); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| function handleSetTitle() { |  | ||||||
|   if (!title.value) { |  | ||||||
|     window.$message?.warning('请输入要设置的标题名称'); |  | ||||||
|   } else { |  | ||||||
|     tabStore.setActiveTabTitle(title.value); |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| </script> |  | ||||||
|  |  | ||||||
| <style scoped></style> |  | ||||||
| @@ -10,39 +10,6 @@ export const views: Record< | |||||||
|   'constant-page': () => import('./_builtin/constant-page/index.vue'), |   'constant-page': () => import('./_builtin/constant-page/index.vue'), | ||||||
|   login: () => import('./_builtin/login/index.vue'), |   login: () => import('./_builtin/login/index.vue'), | ||||||
|   'not-found': () => import('./_builtin/not-found/index.vue'), |   'not-found': () => import('./_builtin/not-found/index.vue'), | ||||||
|   about: () => import('./about/index.vue'), |  | ||||||
|   'auth-demo_permission': () => import('./auth-demo/permission/index.vue'), |  | ||||||
|   'auth-demo_super': () => import('./auth-demo/super/index.vue'), |  | ||||||
|   component_button: () => import('./component/button/index.vue'), |  | ||||||
|   component_card: () => import('./component/card/index.vue'), |  | ||||||
|   component_table: () => import('./component/table/index.vue'), |  | ||||||
|   dashboard_analysis: () => import('./dashboard/analysis/index.vue'), |  | ||||||
|   dashboard_workbench: () => import('./dashboard/workbench/index.vue'), |  | ||||||
|   document_naive: () => import('./document/naive/index.vue'), |  | ||||||
|   'document_project-link': () => import('./document/project-link/index.vue'), |  | ||||||
|   document_project: () => import('./document/project/index.vue'), |  | ||||||
|   document_vite: () => import('./document/vite/index.vue'), |  | ||||||
|   document_vue: () => import('./document/vue/index.vue'), |  | ||||||
|   exception_403: () => import('./exception/403/index.vue'), |  | ||||||
|   exception_404: () => import('./exception/404/index.vue'), |  | ||||||
|   exception_500: () => import('./exception/500/index.vue'), |  | ||||||
|   'function_tab-detail': () => import('./function/tab-detail/index.vue'), |  | ||||||
|   'function_tab-multi-detail': () => import('./function/tab-multi-detail/index.vue'), |  | ||||||
|   function_tab: () => import('./function/tab/index.vue'), |  | ||||||
|   management_auth: () => import('./management/auth/index.vue'), |  | ||||||
|   management_role: () => import('./management/role/index.vue'), |  | ||||||
|   management_route: () => import('./management/route/index.vue'), |  | ||||||
|   management_user: () => import('./management/user/index.vue'), |  | ||||||
|   'multi-menu_first_second-new_third': () => import('./multi-menu/first/second-new/third/index.vue'), |   'multi-menu_first_second-new_third': () => import('./multi-menu/first/second-new/third/index.vue'), | ||||||
|   'multi-menu_first_second': () => import('./multi-menu/first/second/index.vue'), |   'multi-menu_first_second': () => import('./multi-menu/first/second/index.vue') | ||||||
|   plugin_charts_antv: () => import('./plugin/charts/antv/index.vue'), |  | ||||||
|   plugin_charts_echarts: () => import('./plugin/charts/echarts/index.vue'), |  | ||||||
|   plugin_copy: () => import('./plugin/copy/index.vue'), |  | ||||||
|   plugin_editor_markdown: () => import('./plugin/editor/markdown/index.vue'), |  | ||||||
|   plugin_editor_quill: () => import('./plugin/editor/quill/index.vue'), |  | ||||||
|   plugin_icon: () => import('./plugin/icon/index.vue'), |  | ||||||
|   plugin_map: () => import('./plugin/map/index.vue'), |  | ||||||
|   plugin_print: () => import('./plugin/print/index.vue'), |  | ||||||
|   plugin_swiper: () => import('./plugin/swiper/index.vue'), |  | ||||||
|   plugin_video: () => import('./plugin/video/index.vue') |  | ||||||
| }; | }; | ||||||
|   | |||||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user