mirror of
https://github.com/chansee97/nova-admin.git
synced 2025-04-05 19:41:59 +08:00
Merge branch 'updata-fetch-layout' of https://github.com/JuneOY/nova-admin into updata-fetch-layout
This commit is contained in:
commit
d5b1a47cbb
3
.env
3
.env
@ -9,3 +9,6 @@ VITE_AUTH_ROUTE_MODE=static
|
||||
|
||||
# 设置登陆后跳转地址
|
||||
VITE_HOME_PATH = /dashboard/workbench
|
||||
|
||||
# 本地存储前缀
|
||||
VITE_STORAGE_PREFIX=
|
||||
|
@ -1,5 +1,5 @@
|
||||
# 是否开启压缩资源
|
||||
VITE_COMPRESS_OPEN=N
|
||||
VITE_BUILD_COMPRESS=N
|
||||
|
||||
# 压缩算法 gzip | brotliCompress | deflate | deflateRaw
|
||||
VITE_COMPRESS_TYPE=gzip
|
||||
|
6
.env.test
Normal file
6
.env.test
Normal file
@ -0,0 +1,6 @@
|
||||
# 是否开启压缩资源
|
||||
VITE_BUILD_COMPRESS=N
|
||||
|
||||
# 压缩算法 gzip | brotliCompress | deflate | deflateRaw
|
||||
VITE_COMPRESS_TYPE=gzip
|
||||
|
1
.github/FUNDING.yml
vendored
Normal file
1
.github/FUNDING.yml
vendored
Normal file
@ -0,0 +1 @@
|
||||
custom: ['https://cdn.jsdelivr.net/gh/chansee97/static/sponsor-wechat.png', 'https://cdn.jsdelivr.net/gh/chansee97/static/sponsor-alipay.png']
|
42
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
Normal file
42
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
Normal file
@ -0,0 +1,42 @@
|
||||
name: 🐞 Bug report
|
||||
description: Create a report to help us improve
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for taking the time to fill out this bug report!
|
||||
|
||||
- type: textarea
|
||||
id: bug-description
|
||||
attributes:
|
||||
label: Description
|
||||
description: Please explain clearly how the bug reappears. If possible, it is best to add the cause of the problem.
|
||||
placeholder: bug description
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: expected
|
||||
attributes:
|
||||
label: Expected
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: additional-comments
|
||||
attributes:
|
||||
label: Additional comments
|
||||
description: e.g. some background/context of how you ran into this bug.
|
||||
|
||||
- type: checkboxes
|
||||
id: checkboxes
|
||||
attributes:
|
||||
label: Validations
|
||||
description: Before submitting the issue, please make sure you do the following
|
||||
options:
|
||||
- label: Ensure this issue not a bug proposal.
|
||||
required: true
|
||||
- label: Read the [docs](https://nova-admin-docs.netlify.app/).
|
||||
required: true
|
||||
- label: Check that there isn't [already an issue](https://github.com/chansee97/nova-admin/issues) that descript the same thing to avoid creating a duplicate.
|
||||
required: true
|
1
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
1
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@ -0,0 +1 @@
|
||||
blank_issues_enabled: false
|
45
.github/ISSUE_TEMPLATE/feature-request.yml
vendored
Normal file
45
.github/ISSUE_TEMPLATE/feature-request.yml
vendored
Normal file
@ -0,0 +1,45 @@
|
||||
name: ✨ New feature
|
||||
|
||||
description: Propose a new feature to be added to Nova-admin
|
||||
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for your interest in the project and taking the time to fill out this feature report!
|
||||
|
||||
- type: textarea
|
||||
id: feature-description
|
||||
attributes:
|
||||
label: Description
|
||||
description: Clear and concise description of the problem. Please make the reason and usecases as detailed as possible. If you intend to submit a PR for this issue, tell us in the description. Thanks!
|
||||
placeholder: As a developer using Nova-admin I want [goal / wish] so that [benefit]...
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: suggested-solution
|
||||
attributes:
|
||||
label: Suggestion
|
||||
description: In module [xy] we could provide following implementation...
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: additional-context
|
||||
attributes:
|
||||
label: Additional context
|
||||
description: Any other context or screenshots about the feature request here.
|
||||
|
||||
- type: checkboxes
|
||||
id: checkboxes
|
||||
attributes:
|
||||
label: Validations
|
||||
description: Before submitting the issue, please make sure you do the following
|
||||
options:
|
||||
- label: Ensure this issue not a feature proposal.
|
||||
required: true
|
||||
- label: Read the [docs](https://nova-admin-docs.netlify.app/).
|
||||
required: true
|
||||
- label: Check that there isn't [already an issue](https://github.com/chansee97/nova-admin/issues) that descript the same thing to avoid creating a duplicate.
|
||||
required: true
|
31
.github/ISSUE_TEMPLATE/others.yml
vendored
Normal file
31
.github/ISSUE_TEMPLATE/others.yml
vendored
Normal file
@ -0,0 +1,31 @@
|
||||
name: 👓 Others
|
||||
|
||||
description: Create an issue for Nova-admin
|
||||
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for your interest in the project and taking the time to create this issue!
|
||||
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: Description
|
||||
description: Clear and concise description of the issue. Thanks!
|
||||
placeholder: There are some thing I want to ...
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: checkboxes
|
||||
id: checkboxes
|
||||
attributes:
|
||||
label: Validations
|
||||
description: Before submitting the issue, please make sure you do the following
|
||||
options:
|
||||
- label: Ensure this issue neither a bug report nor a feature proposal.
|
||||
required: true
|
||||
- label: Read the [docs](https://nova-admin-docs.netlify.app/).
|
||||
required: true
|
||||
- label: Check that there isn't [already an issue](https://github.com/chansee97/nova-admin/issues) that descript the same thing to avoid creating a duplicate.
|
||||
required: true
|
25
.github/workflows/release.yml
vendored
Normal file
25
.github/workflows/release.yml
vendored
Normal file
@ -0,0 +1,25 @@
|
||||
name: Release
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16.x
|
||||
|
||||
- run: npx changelogithub # or changelogithub@0.12 if ensure the stable result
|
||||
env:
|
||||
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
|
11
.vscode/settings.json
vendored
11
.vscode/settings.json
vendored
@ -65,5 +65,14 @@
|
||||
"jsonc",
|
||||
"yaml",
|
||||
"toml"
|
||||
]
|
||||
],
|
||||
"i18n-ally.sourceLanguage": "zh_CN",
|
||||
"i18n-ally.displayLanguage": "zh_CN",
|
||||
"i18n-ally.enabledFrameworks": ["vue"],
|
||||
"i18n-ally.editor.preferEditor": true,
|
||||
"i18n-ally.keystyle": "nested",
|
||||
"i18n-ally.localesPaths": [
|
||||
"locales"
|
||||
],
|
||||
"commentTranslate.source": "Google"
|
||||
}
|
||||
|
23
README.md
23
README.md
@ -31,6 +31,7 @@
|
||||
- Dark theme adaptation, maintaining the Naive style for interface aesthetics.
|
||||
- Only performs eslint validation during submission without excessive restrictions for simpler development.
|
||||
- Flexible and configurable interface style layout.
|
||||
- Multilanguage (i18n) support.
|
||||
|
||||
## Project preview
|
||||
|
||||
@ -46,13 +47,16 @@
|
||||
- [Gitee](https://gitee.com/chansee97/nova-admin)
|
||||
- [Github](https://github.com/chansee97/nova-admin)
|
||||
|
||||
## Related projects
|
||||
## Interface document
|
||||
|
||||
- [Nova-admin-nest](https://github.com/chansee97/nove-admin-nest) (under development) Nova-Admin supporting background project based on TS, NestJs, typeorm
|
||||
This project uses ApiFox for interface mock, check the online documentation for more interface details
|
||||
[online aipfox docs](https://apifox.com/apidoc/shared-2b1abeb5-6e78-425e-a4ff-d7277ae83bf0)
|
||||
|
||||
## Install and use
|
||||
|
||||
The local development environment is recommended to use pnpm 8.x, Node.js 18.x
|
||||
The local development environment is recommended to use pnpm 8.x, Node.js must be version 20.x.
|
||||
|
||||
It is recommended to directly download the compressed package from [Releases](https://github.com/chansee97/nova-admin/releases)
|
||||
|
||||
```bash
|
||||
# install dependencies
|
||||
@ -66,10 +70,9 @@ pnpm build
|
||||
|
||||
```
|
||||
|
||||
## Interface document
|
||||
## Related projects
|
||||
|
||||
This project uses ApiFox for interface mock, check the online documentation for more interface details
|
||||
[online aipfox docs](https://apifox.com/apidoc/shared-2b1abeb5-6e78-425e-a4ff-d7277ae83bf0)
|
||||
- [Nova-admin-nest](https://github.com/chansee97/nove-admin-nest) (under development) Nova-Admin supporting background project based on TS, NestJs, typeorm
|
||||
|
||||
## Learn to communicate
|
||||
|
||||
@ -93,9 +96,13 @@ If you feel that this project is helpful for your work or study, please help me
|
||||
| :--: |:--: |
|
||||
| <img src="https://cdn.jsdelivr.net/gh/chansee97/static/sponsor-wechat.png" width=170> | <img src="https://cdn.jsdelivr.net/gh/chansee97/static/sponsor-alipay.png" width=170>|
|
||||
|
||||
## [Contributors](https://github.com/chansee97/nova-admin/graphs/contributors)
|
||||
## Contributors
|
||||
|
||||

|
||||
Thanks for all their contributions!
|
||||
|
||||
<a href="https://github.com/chansee97/nova-admin/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=chansee97/nova-admin" alt="contributors" />
|
||||
</a>
|
||||
|
||||
## Star History
|
||||
|
||||
|
@ -31,6 +31,7 @@
|
||||
- 黑暗主题适配, 界面样式保持Naive风格
|
||||
- 仅在提交时进行eslint校验,没有过多限制,开发更简便
|
||||
- 界面样式布局灵活可配置
|
||||
- 多语言(i18n)支持
|
||||
|
||||
## 项目预览
|
||||
|
||||
@ -46,13 +47,16 @@
|
||||
- [Gitee](https://gitee.com/chansee97/nova-admin)
|
||||
- [Github](https://github.com/chansee97/nova-admin)
|
||||
|
||||
## 相关项目
|
||||
## 接口文档
|
||||
|
||||
- [Nova-admin-nest](https://github.com/chansee97/nove-admin-nest) (开发中)基于TS, NestJs, typeorm的Nova-Admin配套后台项目
|
||||
本项目使用ApiFox进行接口mock,查看在线文档以了解更多接口详情
|
||||
[在线apifox文档](https://apifox.com/apidoc/shared-2b1abeb5-6e78-425e-a4ff-d7277ae83bf0)
|
||||
|
||||
## 安装使用
|
||||
|
||||
本地开发环境建议使用 pnpm 8.x 、Node.js 18.x
|
||||
本地开发环境建议使用 pnpm 8.x 、Node.js 必须 20.x
|
||||
|
||||
推荐直接下载[Releases](https://github.com/chansee97/nova-admin/releases)压缩包
|
||||
|
||||
```bash
|
||||
# install dependencies
|
||||
@ -66,10 +70,9 @@ pnpm build
|
||||
|
||||
```
|
||||
|
||||
## 接口文档
|
||||
## 相关项目
|
||||
|
||||
本项目使用ApiFox进行接口mock,查看在线文档以了解更多接口详情
|
||||
[在线apifox文档](https://apifox.com/apidoc/shared-2b1abeb5-6e78-425e-a4ff-d7277ae83bf0)
|
||||
- [Nova-admin-nest](https://github.com/chansee97/nove-admin-nest) (开发中)基于TS, NestJs, typeorm的Nova-Admin配套后台项目
|
||||
|
||||
## 学习交流
|
||||
|
||||
@ -93,9 +96,13 @@ Nova-Admin 是完全开源免费的项目,目前仍然在优化迭代中,旨
|
||||
| :--: |:--: |
|
||||
| <img src="https://cdn.jsdelivr.net/gh/chansee97/static/sponsor-wechat.png" width=170> | <img src="https://cdn.jsdelivr.net/gh/chansee97/static/sponsor-alipay.png" width=170>|
|
||||
|
||||
## [贡献者](https://github.com/chansee97/nova-admin/graphs/contributors)
|
||||
## 贡献者
|
||||
|
||||

|
||||
感谢他们的所做的一切贡献!
|
||||
|
||||
<a href="https://github.com/chansee97/nova-admin/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=chansee97/nova-admin" alt="contributors" />
|
||||
</a>
|
||||
|
||||
## Star 历史
|
||||
|
||||
|
@ -1,4 +1,3 @@
|
||||
import path from 'node:path'
|
||||
import UnoCSS from '@unocss/vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import vueJsx from '@vitejs/plugin-vue-jsx'
|
||||
@ -7,13 +6,11 @@ import AutoImport from 'unplugin-auto-import/vite'
|
||||
import { NaiveUiResolver } from 'unplugin-vue-components/resolvers'
|
||||
import viteCompression from 'vite-plugin-compression'
|
||||
import Icons from 'unplugin-icons/vite'
|
||||
import { FileSystemIconLoader } from 'unplugin-icons/loaders'
|
||||
|
||||
// https://github.com/antfu/unplugin-icons
|
||||
import IconsResolver from 'unplugin-icons/resolver'
|
||||
|
||||
// https://github.com/vbenjs/vite-plugin-svg-icons/blob/main/README.zh_CN.md
|
||||
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons'
|
||||
|
||||
/**
|
||||
* @description: 设置vite插件配置
|
||||
* @param {*} env - 环境变量配置
|
||||
@ -30,7 +27,7 @@ export function createVitePlugins(env: ImportMetaEnv) {
|
||||
|
||||
// auto import api of lib
|
||||
AutoImport({
|
||||
imports: ['vue', 'vue-router', 'pinia', '@vueuse/core'],
|
||||
imports: ['vue', 'vue-router', 'pinia', '@vueuse/core', 'vue-i18n'],
|
||||
include: [
|
||||
/\.[tj]sx?$/,
|
||||
/\.vue$/,
|
||||
@ -43,28 +40,31 @@ export function createVitePlugins(env: ImportMetaEnv) {
|
||||
// auto import components lib
|
||||
Components({
|
||||
dts: 'src/typings/components.d.ts',
|
||||
resolvers: [IconsResolver(), NaiveUiResolver()],
|
||||
resolvers: [
|
||||
IconsResolver({
|
||||
prefix: false,
|
||||
customCollections: [
|
||||
'svg-icons',
|
||||
],
|
||||
}),
|
||||
NaiveUiResolver(),
|
||||
],
|
||||
}),
|
||||
|
||||
// auto import iconify's icons
|
||||
Icons({
|
||||
defaultStyle: 'display:inline-block',
|
||||
compiler: 'vue3',
|
||||
customCollections: {
|
||||
'svg-icons': FileSystemIconLoader(
|
||||
'src/assets/svg-icons',
|
||||
svg => svg.replace(/^<svg /, '<svg fill="currentColor" width="1.2em" height="1.2em"'),
|
||||
),
|
||||
},
|
||||
}),
|
||||
|
||||
// auto use svg icon
|
||||
createSvgIconsPlugin({
|
||||
// 指定需要缓存的图标文件夹
|
||||
iconDirs: [path.resolve(__dirname, '../src/assets/icons')],
|
||||
// 指定symbolId格式
|
||||
symbolId: 'icon-[dir]-[name]',
|
||||
// inject: 'body-last',
|
||||
// customDomId: '__svg__icons__dom__',
|
||||
}),
|
||||
|
||||
]
|
||||
// use compression
|
||||
if (env.VITE_COMPRESS_OPEN === 'Y') {
|
||||
if (env.VITE_BUILD_COMPRESS === 'Y') {
|
||||
const { VITE_COMPRESS_TYPE = 'gzip' } = env
|
||||
plugins.push(viteCompression({
|
||||
algorithm: VITE_COMPRESS_TYPE, // 压缩算法
|
||||
|
145
locales/en_US.json
Normal file
145
locales/en_US.json
Normal file
@ -0,0 +1,145 @@
|
||||
{
|
||||
"common": {
|
||||
"cancel": "Cancel",
|
||||
"confirm": "Confirm",
|
||||
"close": "Closure",
|
||||
"reload": "Refresh"
|
||||
},
|
||||
"app": {
|
||||
"loginOut": "Login out",
|
||||
"loginOutContent": "Confirm to log out of current account?",
|
||||
"loginOutTitle": "Sign out",
|
||||
"userCenter": "Personal center",
|
||||
"lignt": "Light",
|
||||
"dark": "Dark",
|
||||
"system": "System",
|
||||
"backTop": "Back to top",
|
||||
"toggleSider": "Toggle sidebar",
|
||||
"BreadcrumbIcon": "Breadcrumbs icon",
|
||||
"blackAndWhite": "Black and white mode",
|
||||
"bottomCopyright": "Bottom copyright",
|
||||
"breadcrumb": "Bread crumbs",
|
||||
"colorWeak": "Color Weakness Mode",
|
||||
"interfaceDisplay": "Interface display",
|
||||
"logoDisplay": "LOGO display",
|
||||
"messages": "Messages",
|
||||
"multitab": "Display multiple tabs",
|
||||
"notifications": "Notify",
|
||||
"notificationsTips": "Notification",
|
||||
"pageTransition": "Page transition",
|
||||
"reset": "Reset",
|
||||
"resetSettingContent": "Confirm to reset all settings?",
|
||||
"resetSettingMeaasge": "Reset successful",
|
||||
"resetSettingTitle": "Reset settings",
|
||||
"searchPlaceholder": "Search page/path",
|
||||
"setting": "Setting",
|
||||
"systemSetting": "System settings",
|
||||
"themeColor": "Theme color",
|
||||
"themeSetting": "Theme settings",
|
||||
"todos": "Todos",
|
||||
"toggleFullScreen": "Toggle full screen",
|
||||
"topProgress": "Top progress",
|
||||
"transitionFadeBottom": "Bottom fade",
|
||||
"transitionFadeScale": "Scale fade",
|
||||
"transitionFadeSlide": "Side fade",
|
||||
"transitionNull": "No transition",
|
||||
"transitionSoft": "Soft",
|
||||
"transitionZoomFade": "Expand fade out",
|
||||
"transitionZoomOut": "Zoom out",
|
||||
"watermake": "Watermark",
|
||||
"closeOther": "Close other",
|
||||
"closeAll": "Close all",
|
||||
"closeLeft": "Close left",
|
||||
"closeRight": "Close right",
|
||||
"backHome": "Back to the homepage",
|
||||
"getRouteError": "Failed to obtain route, please try again later.",
|
||||
"layoutSetting": "Layout settings",
|
||||
"leftMenu": "Left menu",
|
||||
"topMenu": "Top menu"
|
||||
},
|
||||
"login": {
|
||||
"signInTitle": "Login",
|
||||
"accountRuleTip": "Please enter account",
|
||||
"passwordRuleTip": "Please enter password",
|
||||
"or": "Or",
|
||||
"rememberMe": "Remember me",
|
||||
"forgotPassword": "Forget the password?",
|
||||
"signIn": "Sign in",
|
||||
"signUp": "Sign up",
|
||||
"noAccountText": "Don't have an account?",
|
||||
"accountPlaceholder": "Enter the account number",
|
||||
"checkPasswordPlaceholder": "Please enter password again",
|
||||
"checkPasswordRuleTip": "Please confirm password again",
|
||||
"haveAccountText": "Do you have an account?",
|
||||
"passwordPlaceholder": "Enter password",
|
||||
"readAndAgree": "I have read and agree",
|
||||
"registerTitle": "Register",
|
||||
"userAgreement": "User Agreement",
|
||||
"resetPassword": "Reset password",
|
||||
"resetPasswordPlaceholder": "Enter account/mobile phone number",
|
||||
"resetPasswordRuleTip": "Please enter your account/mobile phone number",
|
||||
"resetPasswordTitle": "Reset"
|
||||
},
|
||||
"route": {
|
||||
"appRoot": "Home",
|
||||
"cardList": "Card list",
|
||||
"commonList": "Common list",
|
||||
"dashboard": "Dashboard",
|
||||
"demo": "Function example",
|
||||
"fetch": "Request example",
|
||||
"list": "List",
|
||||
"monitor": "Monitoring",
|
||||
"test": "Multi-level menu",
|
||||
"test2": "Multi-level menu subpage",
|
||||
"test2Detail": "Details page of multi-level menu",
|
||||
"test3": "multi-level menu",
|
||||
"test4": "Multi-level menu 3-1",
|
||||
"workbench": "Workbench",
|
||||
"QRCode": "QR code",
|
||||
"about": "About",
|
||||
"clipboard": "Clipboard",
|
||||
"demo403": "403",
|
||||
"demo404": "404",
|
||||
"demo500": "500",
|
||||
"dictionarySetting": "Dictionary settings",
|
||||
"docments": "Document",
|
||||
"docmentsVite": "Vite",
|
||||
"docmentsVue": "Vue",
|
||||
"docmentsVueuse": "VueUse (external link)",
|
||||
"echarts": "Echarts",
|
||||
"editor": "Editor",
|
||||
"editorMd": "MarkDown editor",
|
||||
"editorRich": "Rich text editor",
|
||||
"error": "Exception page",
|
||||
"icons": "Icon",
|
||||
"justSuper": "Supervisible",
|
||||
"map": "Map",
|
||||
"menuSetting": "Menu Settings",
|
||||
"permission": "Permissions",
|
||||
"permissionDemo": "Permissions example",
|
||||
"setting": "System settings",
|
||||
"userCenter": "Personal Center",
|
||||
"accountSetting": "User settings"
|
||||
},
|
||||
"http": {
|
||||
"400": "Syntax error in the request",
|
||||
"401": "User unauthorized",
|
||||
"403": "Server refused access",
|
||||
"404": "Requested resource does not exist",
|
||||
"405": "Request method not allowed",
|
||||
"408": "Network request timed out",
|
||||
"500": "Internal server error",
|
||||
"501": "Server not implemented the requested functionality",
|
||||
"502": "Bad gateway",
|
||||
"503": "Service unavailable",
|
||||
"504": "Gateway timeout",
|
||||
"505": "HTTP version not supported for this request",
|
||||
"defaultTip": "Request error"
|
||||
},
|
||||
"components": {
|
||||
"iconSelector": {
|
||||
"inputPlaceholder": "Select target icon",
|
||||
"searchPlaceholder": "Search icon"
|
||||
}
|
||||
}
|
||||
}
|
145
locales/zh_CN.json
Normal file
145
locales/zh_CN.json
Normal file
@ -0,0 +1,145 @@
|
||||
{
|
||||
"common": {
|
||||
"confirm": "确认",
|
||||
"cancel": "取消",
|
||||
"reload": "刷新",
|
||||
"close": "关闭"
|
||||
},
|
||||
"app": {
|
||||
"loginOut": "退出登录",
|
||||
"loginOutTitle": "退出登录",
|
||||
"loginOutContent": "确认退出当前账号?",
|
||||
"userCenter": "个人中心",
|
||||
"lignt": "浅色",
|
||||
"dark": "深色",
|
||||
"system": "跟随系统",
|
||||
"backTop": "返回顶部",
|
||||
"toggleSider": "切换侧边栏",
|
||||
"toggleFullScreen": "切换全屏",
|
||||
"notificationsTips": "消息通知",
|
||||
"notifications": "通知",
|
||||
"messages": "消息",
|
||||
"todos": "待办",
|
||||
"searchPlaceholder": "搜索页面/路径",
|
||||
"resetSettingTitle": "重置设置",
|
||||
"resetSettingContent": "确认重置所有设置?",
|
||||
"resetSettingMeaasge": "重置成功",
|
||||
"reset": "重置",
|
||||
"setting": "设置",
|
||||
"themeSetting": "主题设置",
|
||||
"colorWeak": "色弱模式",
|
||||
"blackAndWhite": "黑白模式",
|
||||
"themeColor": "主题色",
|
||||
"pageTransition": "页面过渡",
|
||||
"transitionNull": "无过渡",
|
||||
"transitionFadeSlide": "侧边淡出",
|
||||
"transitionFadeBottom": "底边淡出",
|
||||
"transitionFadeScale": "收缩淡出",
|
||||
"transitionZoomFade": "扩大淡出",
|
||||
"transitionZoomOut": "收缩",
|
||||
"transitionSoft": "柔和",
|
||||
"systemSetting": "系统设置",
|
||||
"interfaceDisplay": "界面显示",
|
||||
"logoDisplay": "LOGO显示",
|
||||
"topProgress": "顶部进度",
|
||||
"multitab": "多页签显示",
|
||||
"bottomCopyright": "底部版权",
|
||||
"breadcrumb": "面包屑",
|
||||
"BreadcrumbIcon": "面包屑图标",
|
||||
"watermake": "水印",
|
||||
"closeOther": "关闭其他",
|
||||
"closeLeft": "关闭左侧",
|
||||
"closeRight": "关闭右侧",
|
||||
"closeAll": "全部关闭",
|
||||
"backHome": "回到首页",
|
||||
"getRouteError": "获取路由失败,请稍后再试",
|
||||
"layoutSetting": "布局设置",
|
||||
"leftMenu": "左侧菜单",
|
||||
"topMenu": "顶部菜单"
|
||||
},
|
||||
"http": {
|
||||
"400": "请求出现语法错误",
|
||||
"401": "用户未授权",
|
||||
"403": "服务器拒绝访问",
|
||||
"404": "请求的资源不存在",
|
||||
"405": "请求方法未允许",
|
||||
"408": "网络请求超时",
|
||||
"500": "服务器内部错误",
|
||||
"501": "服务器未实现请求功能",
|
||||
"502": "错误网关",
|
||||
"503": "服务不可用",
|
||||
"504": "网关超时",
|
||||
"505": "http版本不支持该请求",
|
||||
"defaultTip": "请求错误"
|
||||
},
|
||||
"components": {
|
||||
"iconSelector": {
|
||||
"inputPlaceholder": "选择目标图标",
|
||||
"searchPlaceholder": "搜索图标"
|
||||
}
|
||||
},
|
||||
"login": {
|
||||
"signInTitle": "登录",
|
||||
"accountPlaceholder": "输入账号",
|
||||
"passwordPlaceholder": "输入密码",
|
||||
"accountRuleTip": "请输入账户",
|
||||
"passwordRuleTip": "请输入密码",
|
||||
"or": "其他",
|
||||
"signIn": "登录",
|
||||
"rememberMe": "记住我",
|
||||
"forgotPassword": "忘记密码?",
|
||||
"signUp": "注册",
|
||||
"noAccountText": "你没有账户?",
|
||||
"haveAccountText": "已有账号?",
|
||||
"checkPasswordRuleTip": "请再次确认密码",
|
||||
"registerTitle": "注册",
|
||||
"checkPasswordPlaceholder": "请再次输入密码",
|
||||
"readAndAgree": "我已阅读并同意",
|
||||
"userAgreement": "用户协议",
|
||||
"resetPasswordTitle": "重置密码",
|
||||
"resetPasswordPlaceholder": "输入账号/手机号码",
|
||||
"resetPasswordRuleTip": "请输入账号/手机号码",
|
||||
"resetPassword": "重置密码"
|
||||
},
|
||||
"route": {
|
||||
"appRoot": "首页",
|
||||
"dashboard": "仪表盘",
|
||||
"workbench": "工作台",
|
||||
"monitor": "监控页",
|
||||
"test": "多级菜单演示",
|
||||
"test2": "多级菜单子页",
|
||||
"test2Detail": "多级菜单的详情页",
|
||||
"test3": "多级菜单",
|
||||
"test4": "多级菜单3-1",
|
||||
"list": "列表页",
|
||||
"commonList": "常用列表",
|
||||
"cardList": "卡片列表",
|
||||
"demo": "功能示例",
|
||||
"fetch": "请求示例",
|
||||
"echarts": "Echarts示例",
|
||||
"map": "地图",
|
||||
"editor": "编辑器",
|
||||
"editorMd": "MarkDown编辑器",
|
||||
"editorRich": "富文本编辑器",
|
||||
"clipboard": "剪贴板",
|
||||
"icons": "图标",
|
||||
"QRCode": "二维码",
|
||||
"docments": "文档",
|
||||
"docmentsVue": "Vue",
|
||||
"docmentsVite": "Vite",
|
||||
"docmentsVueuse": "VueUse(外链)",
|
||||
"permission": "权限",
|
||||
"permissionDemo": "权限示例",
|
||||
"justSuper": "super可见",
|
||||
"error": "异常页",
|
||||
"demo403": "403",
|
||||
"demo404": "404",
|
||||
"demo500": "500",
|
||||
"setting": "系统设置",
|
||||
"accountSetting": "用户设置",
|
||||
"dictionarySetting": "字典设置",
|
||||
"menuSetting": "菜单设置",
|
||||
"userCenter": "个人中心",
|
||||
"about": "关于"
|
||||
}
|
||||
}
|
37
package.json
37
package.json
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "nova-admin",
|
||||
"type": "module",
|
||||
"version": "0.1.0",
|
||||
"version": "0.9.0",
|
||||
"private": true,
|
||||
"description": "",
|
||||
"author": {
|
||||
@ -46,55 +46,56 @@
|
||||
"preview": "vite preview --port 9981",
|
||||
"lint": "eslint .",
|
||||
"lint:fix": "eslint . --fix",
|
||||
"lint:check": "npx @eslint/config-inspector",
|
||||
"sizecheck": "npx vite-bundle-visualizer"
|
||||
},
|
||||
"dependencies": {
|
||||
"@alova/scene-vue": "^1.4.5",
|
||||
"@alova/scene-vue": "^1.4.6",
|
||||
"@tinymce/tinymce-vue": "^5.1.1",
|
||||
"@vueuse/core": "^10.9.0",
|
||||
"alova": "^2.17.1",
|
||||
"chroma-js": "^2.4.2",
|
||||
"alova": "^2.19.0",
|
||||
"colord": "^2.9.3",
|
||||
"echarts": "^5.5.0",
|
||||
"md-editor-v3": "^4.11.3",
|
||||
"nprogress": "^0.2.0",
|
||||
"performant-array-to-tree": "^1.11.0",
|
||||
"pinia": "^2.1.7",
|
||||
"pinia-plugin-persist": "^1.0.0",
|
||||
"qs": "^6.12.0",
|
||||
"radash": "^12.1.0",
|
||||
"vue": "^3.4.21",
|
||||
"vue-i18n": "^9.11.1",
|
||||
"vue-router": "^4.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@antfu/eslint-config": "^2.8.3",
|
||||
"@antfu/eslint-config": "^2.13.3",
|
||||
"@iconify-json/icon-park-outline": "^1.1.15",
|
||||
"@iconify/vue": "^4.1.1",
|
||||
"@types/chroma-js": "^2.4.4",
|
||||
"@types/node": "^20.11.28",
|
||||
"@types/nprogress": "^0.2.3",
|
||||
"@types/qs": "^6.9.12",
|
||||
"@types/node": "^20.12.7",
|
||||
"@types/qs": "^6.9.14",
|
||||
"@vitejs/plugin-vue": "^5.0.4",
|
||||
"@vitejs/plugin-vue-jsx": "^3.1.0",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint": "^9.0.0",
|
||||
"lint-staged": "^15.2.2",
|
||||
"naive-ui": "^2.38.1",
|
||||
"sass": "^1.72.0",
|
||||
"simple-git-hooks": "^2.10.0",
|
||||
"typescript": "^5.4.2",
|
||||
"unocss": "^0.58.6",
|
||||
"sass": "^1.75.0",
|
||||
"simple-git-hooks": "^2.11.1",
|
||||
"typescript": "^5.4.5",
|
||||
"unocss": "^0.59.1",
|
||||
"unplugin-auto-import": "^0.17.5",
|
||||
"unplugin-icons": "^0.18.5",
|
||||
"unplugin-vue-components": "^0.26.0",
|
||||
"vite": "^5.1.6",
|
||||
"vite": "^5.2.8",
|
||||
"vite-bundle-visualizer": "^1.1.0",
|
||||
"vite-plugin-compression": "^0.5.1",
|
||||
"vite-plugin-svg-icons": "^2.0.1",
|
||||
"vue-tsc": "^2.0.6"
|
||||
"vue-tsc": "^2.0.12"
|
||||
},
|
||||
"simple-git-hooks": {
|
||||
"pre-commit": "pnpm lint-staged"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*": "eslint --fix"
|
||||
},
|
||||
"volta": {
|
||||
"node": "20.12.2"
|
||||
}
|
||||
}
|
||||
|
12
src/App.vue
12
src/App.vue
@ -1,16 +1,20 @@
|
||||
<script setup lang="ts">
|
||||
import { darkTheme, dateZhCN, zhCN } from 'naive-ui'
|
||||
import { darkTheme } from 'naive-ui'
|
||||
import { useAppStore } from './store'
|
||||
import { naiveI18nOptions } from '@/utils'
|
||||
|
||||
const locale = zhCN
|
||||
const dateLocale = dateZhCN
|
||||
const appStore = useAppStore()
|
||||
|
||||
const naiveLocale = computed(() => {
|
||||
return naiveI18nOptions[appStore.lang] ? naiveI18nOptions[appStore.lang] : naiveI18nOptions.enUS
|
||||
},
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-config-provider
|
||||
class="wh-full" inline-theme-disabled :theme="appStore.colorMode === 'dark' ? darkTheme : null"
|
||||
:locale="locale" :date-locale="dateLocale" :theme-overrides="appStore.theme"
|
||||
:locale="naiveLocale.locale" :date-locale="naiveLocale.dateLocale" :theme-overrides="appStore.theme"
|
||||
>
|
||||
<naive-provider>
|
||||
<router-view />
|
||||
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 2.0 KiB |
Before Width: | Height: | Size: 902 B After Width: | Height: | Size: 902 B |
@ -18,7 +18,6 @@
|
||||
<style scoped>
|
||||
.el {
|
||||
color: var(--n-text-color);
|
||||
background-color: var(--card-color);
|
||||
transition: 0.3s var(--cubic-bezier-ease-in-out);
|
||||
}
|
||||
.el:hover {
|
||||
|
@ -1,32 +1,39 @@
|
||||
<script setup lang="ts">
|
||||
import { NFlex, NText } from 'naive-ui'
|
||||
import { NFlex } from 'naive-ui'
|
||||
import { useAppStore } from '@/store'
|
||||
import { renderIcon } from '@/utils'
|
||||
import IconSun from '~icons/icon-park-outline/sun-one'
|
||||
import IconMoon from '~icons/icon-park-outline/moon'
|
||||
import IconAuto from '~icons/icon-park-outline/laptop-computer'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const appStore = useAppStore()
|
||||
const options = [
|
||||
{
|
||||
label: 'Light',
|
||||
value: 'light',
|
||||
icon: 'icon-park-outline:sun-one',
|
||||
},
|
||||
{
|
||||
label: 'Dark',
|
||||
value: 'dark',
|
||||
icon: 'icon-park-outline:moon',
|
||||
},
|
||||
{
|
||||
label: 'System',
|
||||
value: 'auto',
|
||||
icon: 'icon-park-outline:laptop-computer',
|
||||
},
|
||||
]
|
||||
|
||||
const options = computed(() => {
|
||||
return [
|
||||
{
|
||||
label: t('app.lignt'),
|
||||
value: 'light',
|
||||
icon: IconSun,
|
||||
},
|
||||
{
|
||||
label: t('app.dark'),
|
||||
value: 'dark',
|
||||
icon: IconMoon,
|
||||
},
|
||||
{
|
||||
label: t('app.system'),
|
||||
value: 'auto',
|
||||
icon: IconAuto,
|
||||
},
|
||||
]
|
||||
})
|
||||
|
||||
function renderLabel(option: any) {
|
||||
return h(NFlex, { align: 'center' }, {
|
||||
default: () => [
|
||||
renderIcon(option.icon)(),
|
||||
h(NText, { depth: 3 }, { default: () => option.value }),
|
||||
h(option.icon),
|
||||
option.label,
|
||||
],
|
||||
})
|
||||
}
|
||||
@ -35,9 +42,9 @@ function renderLabel(option: any) {
|
||||
<template>
|
||||
<n-popselect :value="appStore.storeColorMode" :render-label="renderLabel" :options="options" trigger="click" @update:value="appStore.setColorMode">
|
||||
<CommonWrapper>
|
||||
<i-icon-park-outline-moon v-if="appStore.storeColorMode === 'dark'" />
|
||||
<i-icon-park-outline-sun-one v-if="appStore.storeColorMode === 'light'" />
|
||||
<i-icon-park-outline-laptop-computer v-if="appStore.storeColorMode === 'auto'" />
|
||||
<icon-park-outline-moon v-if="appStore.storeColorMode === 'dark'" />
|
||||
<icon-park-outline-sun-one v-if="appStore.storeColorMode === 'light'" />
|
||||
<icon-park-outline-laptop-computer v-if="appStore.storeColorMode === 'auto'" />
|
||||
</CommonWrapper>
|
||||
</n-popselect>
|
||||
</template>
|
||||
|
@ -30,7 +30,7 @@ const router = useRouter()
|
||||
type="primary"
|
||||
@click="router.push('/')"
|
||||
>
|
||||
回到首页
|
||||
{{ $t('app.backHome') }}
|
||||
</n-button>
|
||||
</div>
|
||||
</template>
|
||||
|
16
src/components/common/HelpInfo.vue
Normal file
16
src/components/common/HelpInfo.vue
Normal file
@ -0,0 +1,16 @@
|
||||
<script setup lang="ts">
|
||||
interface Props {
|
||||
message: string
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-tooltip :show-arrow="false" trigger="hover">
|
||||
<template #trigger>
|
||||
<icon-park-outline-help />
|
||||
</template>
|
||||
{{ props.message }}
|
||||
</n-tooltip>
|
||||
</template>
|
@ -1,42 +1,49 @@
|
||||
<script setup lang="ts">
|
||||
import { icons } from './icons'
|
||||
|
||||
const currentIcon = ref('')
|
||||
interface Props {
|
||||
disabled?: boolean
|
||||
}
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
disabled: false,
|
||||
})
|
||||
|
||||
const value = defineModel('value', { type: String })
|
||||
const searchValue = ref('')
|
||||
const showPopover = ref(false)
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const iconList = computed(() => icons.filter(item => item.includes(searchValue.value)))
|
||||
|
||||
function handleSelectIcon(icon: string) {
|
||||
currentIcon.value = icon
|
||||
value.value = icon
|
||||
showPopover.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-popover v-model:show="showPopover" placement="bottom" trigger="click">
|
||||
<n-popover v-model:show="showPopover" placement="bottom" trigger="click" :disabled="props.disabled">
|
||||
<template #trigger>
|
||||
<n-input v-model:value="currentIcon" readonly placeholder="选择目标图标">
|
||||
<n-input :value="value" readonly :placeholder="t('components.iconSelector.inputPlaceholder')">
|
||||
<template #suffix>
|
||||
<e-icon :icon="currentIcon || 'icon-park-outline:all-application'" />
|
||||
<nova-icon :icon="value" />
|
||||
</template>
|
||||
</n-input>
|
||||
</template>
|
||||
<template #header>
|
||||
<n-input v-model:value="searchValue" type="text" placeholder="搜索图标" />
|
||||
<n-input v-model:value="searchValue" type="text" :placeholder="t('components.iconSelector.searchPlaceholder')" />
|
||||
</template>
|
||||
<div class="w-400px">
|
||||
<div v-if="iconList.length > 0" class="grid grid-cols-9 h-auto overflow-auto gap-1">
|
||||
<div
|
||||
v-for="(item, index) in iconList"
|
||||
:key="index"
|
||||
class="border border-gray-200 m-2px p-5px flex-center"
|
||||
v-for="(item, index) in iconList" :key="index" class="border border-gray-200 m-2px p-5px flex-center"
|
||||
@click="handleSelectIcon(item)"
|
||||
>
|
||||
<e-icon :icon="item" :size="24" />
|
||||
<nova-icon :icon="item" :size="24" />
|
||||
</div>
|
||||
</div>
|
||||
<n-empty v-else class="w-full" description="没有符合条件的图标" />
|
||||
<n-empty v-else class="w-full" />
|
||||
</div>
|
||||
</n-popover>
|
||||
</template>
|
25
src/components/common/LangsSwitch.vue
Normal file
25
src/components/common/LangsSwitch.vue
Normal file
@ -0,0 +1,25 @@
|
||||
<script setup lang="ts">
|
||||
import { useAppStore } from '@/store'
|
||||
|
||||
const appStore = useAppStore()
|
||||
const options = [
|
||||
{
|
||||
label: 'English',
|
||||
value: 'enUS',
|
||||
},
|
||||
{
|
||||
label: '中文',
|
||||
value: 'zhCN',
|
||||
},
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-popselect :value="appStore.lang" :options="options" trigger="click" @update:value="appStore.setAppLang">
|
||||
<CommonWrapper>
|
||||
<icon-park-outline-translate />
|
||||
</CommonWrapper>
|
||||
</n-popselect>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
@ -2,19 +2,23 @@
|
||||
import { Icon } from '@iconify/vue'
|
||||
|
||||
interface iconPorps {
|
||||
/* 图标名称 */
|
||||
icon?: string
|
||||
/* 图标颜色 */
|
||||
color?: string
|
||||
/* 图标大小 */
|
||||
size?: number
|
||||
/* 图标深度 */
|
||||
depth?: 1 | 2 | 3 | 4 | 5
|
||||
}
|
||||
const props = withDefaults(defineProps<iconPorps>(), {
|
||||
size: 18,
|
||||
icon: 'icon-park-outline:baby-feet',
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-icon
|
||||
v-if="props.icon"
|
||||
:size="props.size"
|
||||
:depth="props.depth"
|
||||
:color="props.color"
|
@ -9,24 +9,6 @@ const emit = defineEmits(['change'])
|
||||
const page = ref(1)
|
||||
const pageSize = ref(10)
|
||||
const displayOrder: Array<'pages' | 'size-picker' | 'quick-jumper'> = ['size-picker', 'pages']
|
||||
const pageSizes = [
|
||||
{
|
||||
label: '10 每页',
|
||||
value: 10,
|
||||
},
|
||||
{
|
||||
label: '20 每页',
|
||||
value: 20,
|
||||
},
|
||||
{
|
||||
label: '30 每页',
|
||||
value: 30,
|
||||
},
|
||||
{
|
||||
label: '50 每页',
|
||||
value: 50,
|
||||
},
|
||||
]
|
||||
|
||||
function changePage() {
|
||||
emit('change', page.value, pageSize.value)
|
||||
@ -41,7 +23,6 @@ function changePage() {
|
||||
:item-count="props.count"
|
||||
:display-order="displayOrder"
|
||||
show-size-picker
|
||||
:page-sizes="pageSizes"
|
||||
@update-page="changePage"
|
||||
@update-page-size="changePage"
|
||||
/>
|
@ -1,31 +1,17 @@
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<{
|
||||
modelValue: string
|
||||
maxLength?: string
|
||||
}>()
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
const text = useVModel(props, 'modelValue', emit)
|
||||
const modelValue = defineModel<string>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="text" class="copy-wrap">
|
||||
<n-ellipsis :style="{ 'max-width': maxLength || '12em' }">
|
||||
{{ text }}
|
||||
<div v-if="modelValue" class="inline-flex items-center gap-0.5em">
|
||||
<n-ellipsis :style="{ 'max-width': props.maxLength || '12em' }">
|
||||
{{ modelValue }}
|
||||
</n-ellipsis>
|
||||
<span v-copy="text" class="copy_icon">
|
||||
<i-icon-park-outline-copy />
|
||||
<span v-copy="modelValue" class="cursor-pointer">
|
||||
<icon-park-outline-copy />
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.copy-wrap{
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap:0.5em;
|
||||
}
|
||||
.copy_icon{
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
|
@ -1,31 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
const props = defineProps({
|
||||
prefix: {
|
||||
type: String,
|
||||
default: 'icon',
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
size: {
|
||||
type: Number,
|
||||
default: 18,
|
||||
},
|
||||
})
|
||||
const symbolId = computed(() => `#${props.prefix}-${props.name}`)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
:width="`${props.size}px`"
|
||||
:height="`${props.size}px`"
|
||||
class="inline"
|
||||
>
|
||||
<use
|
||||
:xlink:href="symbolId"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
12
src/constants/Regex.ts
Normal file
12
src/constants/Regex.ts
Normal file
@ -0,0 +1,12 @@
|
||||
/**
|
||||
* @description Some common rules
|
||||
* @link https://any-rule.vercel.app/
|
||||
*/
|
||||
|
||||
export enum Regex {
|
||||
Url = '^(((ht|f)tps?):\\\/\\\/)?([^!@#$%^&*?.\\s-]([^!@#$%^&*?.\\s]{0,63}[^!@#$%^&*?.\\s])?\\.)+[a-z]{2,6}\\\/?',
|
||||
|
||||
Email = '^(([^<>()[\\]\\\\.,;:\\s@"]+(\\.[^<>()[\\]\\\\.,;:\\s@"]+)*)|(".+"))@((\\[[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\])|(([a-zA-Z\\-0-9]+\\.)+[a-zA-Z]{2,}))$',
|
||||
|
||||
RouteName = '^[\\w_!@#$%^&*~-]+$',
|
||||
}
|
9
src/constants/User.ts
Normal file
9
src/constants/User.ts
Normal file
@ -0,0 +1,9 @@
|
||||
// export const genderLabels: Record<NonNullable<CommonList.GenderType>, string> = {
|
||||
// 0: '女',
|
||||
// 1: '男',
|
||||
// }
|
||||
/** Gender */
|
||||
export enum Gender {
|
||||
male,
|
||||
female,
|
||||
}
|
@ -1,5 +0,0 @@
|
||||
/** 用户性别 */
|
||||
export const genderLabels: Record<NonNullable<CommonList.GenderType>, string> = {
|
||||
0: '女',
|
||||
1: '男',
|
||||
}
|
@ -1 +1,2 @@
|
||||
export * from './business'
|
||||
export * from './Regex'
|
||||
export * from './User'
|
||||
|
@ -1,4 +1,4 @@
|
||||
export * from './useBoolean'
|
||||
export * from './useLoading'
|
||||
export * from './useEcharts'
|
||||
export * from './useSystem'
|
||||
export * from './usePermission'
|
||||
|
@ -5,12 +5,10 @@
|
||||
<n-tooltip placement="left" trigger="hover">
|
||||
<template #trigger>
|
||||
<div wh-full flex-center>
|
||||
<i-icon-park-outline-to-top />
|
||||
<icon-park-outline-to-top />
|
||||
</div>
|
||||
</template>
|
||||
<span>返回顶部</span>
|
||||
<span>{{ $t('app.backTop') }}</span>
|
||||
</n-tooltip>
|
||||
</n-back-top>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
|
56
src/layouts/components/common/LayoutSelector.vue
Normal file
56
src/layouts/components/common/LayoutSelector.vue
Normal file
@ -0,0 +1,56 @@
|
||||
<script setup lang="ts">
|
||||
import type { LayoutMode } from '@/store/app'
|
||||
|
||||
const value = defineModel<LayoutMode>('value', { required: true })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex-center gap-4">
|
||||
<n-tooltip placement="bottom" trigger="hover">
|
||||
<template #trigger>
|
||||
<n-el
|
||||
:class="{
|
||||
'outline outline-2': value === 'leftMenu',
|
||||
}"
|
||||
class="grid grid-cols-[20%_1fr] grid-rows-[20%_1fr] outline-[var(--primary-color)] hover:(outline outline-2)"
|
||||
@click="value = 'leftMenu'"
|
||||
>
|
||||
<div class="bg-[var(--primary-color)] row-span-2" />
|
||||
<div class="bg-[var(--primary-color-suppl)]" />
|
||||
<div class="bg-[var(--divider-color)]" />
|
||||
</n-el>
|
||||
</template>
|
||||
<span> {{ $t('app.leftMenu') }} </span>
|
||||
</n-tooltip>
|
||||
|
||||
<n-tooltip placement="bottom" trigger="hover">
|
||||
<template #trigger>
|
||||
<n-el
|
||||
:class="{
|
||||
'outline outline-2': value === 'topMenu',
|
||||
}"
|
||||
class="grid grid-rows-[30%_1fr] outline-[var(--primary-color)] hover:(outline outline-2)"
|
||||
@click="value = 'topMenu'"
|
||||
>
|
||||
<div class="bg-[var(--primary-color)]" />
|
||||
<div class="bg-[var(--divider-color)]" />
|
||||
</n-el>
|
||||
</template>
|
||||
<span> {{ $t('app.topMenu') }} </span>
|
||||
</n-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.grid{
|
||||
height: 60px;
|
||||
width: 86px;
|
||||
gap:0.4em;
|
||||
padding: 0.4em;
|
||||
box-shadow: var(--box-shadow-1);
|
||||
border-radius: var(--border-radius);
|
||||
}
|
||||
.grid > div{
|
||||
border-radius: var(--border-radius);
|
||||
}
|
||||
</style>
|
@ -21,7 +21,7 @@ interface Emits {
|
||||
</n-ellipsis>
|
||||
</template>
|
||||
<template #avatar>
|
||||
<e-icon :icon="item.icon" :size="30" class="c-primary" />
|
||||
<nova-icon :icon="item.icon" :size="30" class="c-primary" />
|
||||
</template>
|
||||
<template v-if="item.tagTitle" #header-extra>
|
||||
<n-tag :bordered="false" :type="item.tagType" size="small">
|
||||
|
@ -21,8 +21,8 @@ const appStore = useAppStore()
|
||||
class="flex-center gap-2 cursor-pointer split"
|
||||
@click="router.push(item.path)"
|
||||
>
|
||||
<e-icon v-if="appStore.showBreadcrumbIcon" :icon="item.meta.icon" />
|
||||
<span class="whitespace-nowrap">{{ item.meta.title }}</span>
|
||||
<nova-icon v-if="appStore.showBreadcrumbIcon" :icon="item.meta.icon" />
|
||||
<span class="whitespace-nowrap">{{ $t(`route.${String(item.name)}`, item.meta.title) }}</span>
|
||||
</n-el>
|
||||
</TransitionGroup>
|
||||
</template>
|
||||
@ -34,7 +34,6 @@ const appStore = useAppStore()
|
||||
}
|
||||
|
||||
.list-move,
|
||||
/* 对移动中的元素应用的过渡 */
|
||||
.list-enter-active,
|
||||
.list-leave-active {
|
||||
transition: all 0.3s ease;
|
||||
@ -42,7 +41,7 @@ const appStore = useAppStore()
|
||||
|
||||
.list-enter-from,.list-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(-30px);
|
||||
transform: translateX(-30px);
|
||||
}
|
||||
|
||||
.list-leave-active {
|
||||
|
@ -8,11 +8,11 @@ const appStore = useAppStore()
|
||||
<n-tooltip placement="bottom" trigger="hover">
|
||||
<template #trigger>
|
||||
<CommonWrapper @click="appStore.toggleCollapse()">
|
||||
<i-icon-park-outline-menu-unfold v-if="appStore.collapsed" />
|
||||
<i-icon-park-outline-menu-fold v-else />
|
||||
<icon-park-outline-menu-unfold v-if="appStore.collapsed" />
|
||||
<icon-park-outline-menu-fold v-else />
|
||||
</CommonWrapper>
|
||||
</template>
|
||||
<span>切换侧边栏</span>
|
||||
<span>{{ $t('app.toggleSider') }}</span>
|
||||
</n-tooltip>
|
||||
</template>
|
||||
|
||||
|
@ -8,11 +8,11 @@ const appStore = useAppStore()
|
||||
<n-tooltip placement="bottom" trigger="hover">
|
||||
<template #trigger>
|
||||
<CommonWrapper @click="appStore.toggleFullScreen()">
|
||||
<i-icon-park-outline-off-screen-two v-if="appStore.fullScreen" />
|
||||
<i-icon-park-outline-full-screen-two v-else />
|
||||
<icon-park-outline-off-screen-two v-if="appStore.fullScreen" />
|
||||
<icon-park-outline-full-screen-two v-else />
|
||||
</CommonWrapper>
|
||||
</template>
|
||||
<span>全屏</span>
|
||||
<span>{{ $t('app.toggleFullScreen') }}</span>
|
||||
</n-tooltip>
|
||||
</template>
|
||||
|
||||
|
@ -81,11 +81,10 @@ const MassageData = ref<Message.List[]>([
|
||||
])
|
||||
const currentTab = ref(0)
|
||||
function handleRead(id: number) {
|
||||
// MassageData.value[currentTab.value].list[index].isRead = true
|
||||
const data = MassageData.value.find(i => i.id === id)
|
||||
if (data)
|
||||
data.isRead = true
|
||||
window.$message.success(`已读id: ${id}`)
|
||||
window.$message.success(`id: ${id}`)
|
||||
}
|
||||
const massageCount = computed(() => {
|
||||
return MassageData.value.filter(i => !i.isRead).length
|
||||
@ -102,18 +101,18 @@ const groupMessage = computed(() => {
|
||||
<template #trigger>
|
||||
<CommonWrapper>
|
||||
<n-badge :value="massageCount" :max="99" style="color: unset">
|
||||
<i-icon-park-outline-remind />
|
||||
<icon-park-outline-remind />
|
||||
</n-badge>
|
||||
</CommonWrapper>
|
||||
</template>
|
||||
<span>消息通知</span>
|
||||
<span>{{ $t('app.notificationsTips') }}</span>
|
||||
</n-tooltip>
|
||||
</template>
|
||||
<n-tabs v-model:value="currentTab" type="line" animated justify-content="space-evenly" class="w-390px">
|
||||
<n-tab-pane :name="0">
|
||||
<template #tab>
|
||||
<n-space class="w-130px" justify="center">
|
||||
通知
|
||||
{{ $t('app.notifications') }}
|
||||
<n-badge type="info" :value="groupMessage[0]?.filter(i => !i.isRead).length" :max="99" />
|
||||
</n-space>
|
||||
</template>
|
||||
@ -122,7 +121,7 @@ const groupMessage = computed(() => {
|
||||
<n-tab-pane :name="1">
|
||||
<template #tab>
|
||||
<n-space class="w-130px" justify="center">
|
||||
消息
|
||||
{{ $t('app.messages') }}
|
||||
<n-badge type="warning" :value="groupMessage[1]?.filter(i => !i.isRead).length" :max="99" />
|
||||
</n-space>
|
||||
</template>
|
||||
@ -131,7 +130,7 @@ const groupMessage = computed(() => {
|
||||
<n-tab-pane :name="2">
|
||||
<template #tab>
|
||||
<n-space class="w-130px" justify="center">
|
||||
待办
|
||||
{{ $t('app.todos') }}
|
||||
<n-badge type="error" :value="groupMessage[2]?.filter(i => !i.isRead).length" :max="99" />
|
||||
</n-space>
|
||||
</template>
|
||||
|
@ -6,16 +6,18 @@ import { renderIcon } from '@/utils'
|
||||
const routeStore = useRouteStore()
|
||||
const searchValue = ref('')
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const options = computed(() => {
|
||||
return routeStore.rowRoutes.filter((item) => {
|
||||
const conditions = [
|
||||
item['meta.title']?.includes(searchValue.value),
|
||||
t(`route.${String(item.name)}`, item['meta.title'] || item.name)?.includes(searchValue.value),
|
||||
item.path?.includes(searchValue.value),
|
||||
]
|
||||
return conditions.some(condition => condition)
|
||||
}).map((item) => {
|
||||
return {
|
||||
label: item['meta.title'],
|
||||
label: t(`route.${String(item.name)}`, item['meta.title'] || item.name),
|
||||
value: item.path,
|
||||
icon: item['meta.icon'],
|
||||
}
|
||||
@ -44,11 +46,11 @@ function handleSelect(value: string) {
|
||||
<n-auto-complete
|
||||
v-model:value="searchValue" class="w-20em m-r-1em" :input-props="{
|
||||
autocomplete: 'disabled',
|
||||
}" :options="options" :render-label="renderLabel" placeholder="搜索页面/路径" clearable @select="handleSelect"
|
||||
}" :options="options" :render-label="renderLabel" :placeholder="$t('app.searchPlaceholder')" clearable @select="handleSelect"
|
||||
>
|
||||
<template #prefix>
|
||||
<n-icon>
|
||||
<i-icon-park-outline-search />
|
||||
<icon-park-outline-search />
|
||||
</n-icon>
|
||||
</template>
|
||||
</n-auto-complete>
|
||||
|
@ -1,42 +1,48 @@
|
||||
<script setup lang="ts">
|
||||
import LayoutSelector from '../common/LayoutSelector.vue'
|
||||
import { useAppStore } from '@/store'
|
||||
|
||||
const appStore = useAppStore()
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const drawerActive = ref(false)
|
||||
function openSetting() {
|
||||
drawerActive.value = !drawerActive.value
|
||||
}
|
||||
const transitionSelectorOptions = [
|
||||
{
|
||||
label: '无',
|
||||
value: '',
|
||||
},
|
||||
{
|
||||
label: '侧滑',
|
||||
value: 'fade-slide',
|
||||
},
|
||||
{
|
||||
label: '下滑',
|
||||
value: 'fade-bottom',
|
||||
},
|
||||
{
|
||||
label: '收缩',
|
||||
value: 'fade-scale',
|
||||
},
|
||||
{
|
||||
label: '扩张',
|
||||
value: 'zoom-fade',
|
||||
},
|
||||
{
|
||||
label: '坍缩',
|
||||
value: 'zoom-out',
|
||||
},
|
||||
{
|
||||
label: '柔和',
|
||||
value: 'fade',
|
||||
},
|
||||
]
|
||||
|
||||
const transitionSelectorOptions = computed(() => {
|
||||
return [
|
||||
{
|
||||
label: t('app.transitionNull'),
|
||||
value: '',
|
||||
},
|
||||
{
|
||||
label: t('app.transitionFadeSlide'),
|
||||
value: 'fade-slide',
|
||||
},
|
||||
{
|
||||
label: t('app.transitionFadeBottom'),
|
||||
value: 'fade-bottom',
|
||||
},
|
||||
{
|
||||
label: t('app.transitionFadeScale'),
|
||||
value: 'fade-scale',
|
||||
},
|
||||
{
|
||||
label: t('app.transitionZoomFade'),
|
||||
value: 'zoom-fade',
|
||||
},
|
||||
{
|
||||
label: t('app.transitionZoomOut'),
|
||||
value: 'zoom-out',
|
||||
},
|
||||
{
|
||||
label: t('app.transitionSoft'),
|
||||
value: 'fade',
|
||||
},
|
||||
]
|
||||
})
|
||||
|
||||
const palette = [
|
||||
'#ffb8b8',
|
||||
@ -59,13 +65,13 @@ const palette = [
|
||||
|
||||
function resetSetting() {
|
||||
window.$dialog.warning({
|
||||
title: '重置所有设置',
|
||||
content: '你确定重置所有设置?',
|
||||
positiveText: '确定',
|
||||
negativeText: '取消',
|
||||
title: t('app.resetSettingTitle'),
|
||||
content: t('app.resetSettingContent'),
|
||||
positiveText: t('common.confirm'),
|
||||
negativeText: t('common.cancel'),
|
||||
onPositiveClick: () => {
|
||||
appStore.resetAlltheme()
|
||||
window.$message.success('重置成功')
|
||||
window.$message.success(t('app.resetSettingMeaasge'))
|
||||
},
|
||||
})
|
||||
}
|
||||
@ -76,66 +82,70 @@ function resetSetting() {
|
||||
<template #trigger>
|
||||
<CommonWrapper @click="openSetting">
|
||||
<div>
|
||||
<i-icon-park-outline-setting-two />
|
||||
<n-drawer v-model:show="drawerActive" :width="300">
|
||||
<n-drawer-content title="系统设置" closable>
|
||||
<icon-park-outline-setting-two />
|
||||
<n-drawer v-model:show="drawerActive" :width="360">
|
||||
<n-drawer-content :title="t('app.systemSetting')" closable>
|
||||
<n-space vertical>
|
||||
<n-divider>主题设置</n-divider>
|
||||
<n-divider>{{ $t('app.layoutSetting') }}</n-divider>
|
||||
<LayoutSelector v-model:value="appStore.layoutMode" />
|
||||
<n-divider>{{ $t('app.themeSetting') }}</n-divider>
|
||||
<n-space justify="space-between">
|
||||
色弱模式
|
||||
{{ $t('app.colorWeak') }}
|
||||
<n-switch :value="appStore.colorWeak" @update:value="appStore.toggleColorWeak" />
|
||||
</n-space>
|
||||
<n-space justify="space-between">
|
||||
灰色模式
|
||||
{{ $t('app.blackAndWhite') }}
|
||||
<n-switch :value="appStore.grayMode" @update:value="appStore.toggleGrayMode" />
|
||||
</n-space>
|
||||
<n-space align="center" justify="space-between">
|
||||
主题色
|
||||
{{ $t('app.themeColor') }}
|
||||
<n-color-picker
|
||||
v-model:value="appStore.primaryColor"
|
||||
class="w-7em" :swatches="palette" :show-alpha="false"
|
||||
v-model:value="appStore.primaryColor" class="w-10em" :swatches="palette"
|
||||
@update:value="appStore.setPrimaryColor"
|
||||
/>
|
||||
</n-space>
|
||||
<n-space align="center" justify="space-between">
|
||||
切换动效
|
||||
<n-select v-model:value="appStore.transitionAnimation" class="w-7em" :options="transitionSelectorOptions" @update:value="appStore.reloadPage" />
|
||||
{{ $t('app.pageTransition') }}
|
||||
<n-select
|
||||
v-model:value="appStore.transitionAnimation" class="w-10em"
|
||||
:options="transitionSelectorOptions" @update:value="appStore.reloadPage"
|
||||
/>
|
||||
</n-space>
|
||||
|
||||
<n-divider>界面显示</n-divider>
|
||||
<n-divider>{{ $t('app.interfaceDisplay') }}</n-divider>
|
||||
<n-space justify="space-between">
|
||||
LOGO显示
|
||||
{{ $t('app.logoDisplay') }}
|
||||
<n-switch v-model:value="appStore.showLogo" />
|
||||
</n-space>
|
||||
<n-space justify="space-between">
|
||||
顶部进度
|
||||
{{ $t('app.topProgress') }}
|
||||
<n-switch v-model:value="appStore.showProgress" />
|
||||
</n-space>
|
||||
<n-space justify="space-between">
|
||||
多页签显示
|
||||
{{ $t('app.multitab') }}
|
||||
<n-switch v-model:value="appStore.showTabs" />
|
||||
</n-space>
|
||||
<n-space justify="space-between">
|
||||
底部标签显示
|
||||
{{ $t('app.bottomCopyright') }}
|
||||
<n-switch v-model:value="appStore.showFooter" />
|
||||
</n-space>
|
||||
<n-space justify="space-between">
|
||||
面包屑
|
||||
{{ $t('app.breadcrumb') }}
|
||||
<n-switch v-model:value="appStore.showBreadcrumb" />
|
||||
</n-space>
|
||||
<n-space justify="space-between">
|
||||
面包屑图标
|
||||
{{ $t('app.BreadcrumbIcon') }}
|
||||
<n-switch v-model:value="appStore.showBreadcrumbIcon" />
|
||||
</n-space>
|
||||
<n-space justify="space-between">
|
||||
水印
|
||||
{{ $t('app.watermake') }}
|
||||
<n-switch v-model:value="appStore.showWatermark" />
|
||||
</n-space>
|
||||
</n-space>
|
||||
|
||||
<template #footer>
|
||||
<n-button type="error" @click="resetSetting">
|
||||
重置设置
|
||||
{{ $t('app.reset') }}
|
||||
</n-button>
|
||||
</template>
|
||||
</n-drawer-content>
|
||||
@ -143,8 +153,6 @@ function resetSetting() {
|
||||
</div>
|
||||
</CommonWrapper>
|
||||
</template>
|
||||
<span>设置</span>
|
||||
<span>{{ $t('app.setting') }}</span>
|
||||
</n-tooltip>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
|
@ -1,47 +1,60 @@
|
||||
<script setup lang="ts">
|
||||
import { renderIcon } from '@/utils/icon'
|
||||
import { useAuthStore } from '@/store'
|
||||
import IconGithub from '~icons/icon-park-outline/github'
|
||||
import IconUser from '~icons/icon-park-outline/user'
|
||||
import IconLogout from '~icons/icon-park-outline/logout'
|
||||
import IconBookOpen from '~icons/icon-park-outline/book-open'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const { userInfo, resetAuthStore } = useAuthStore()
|
||||
const router = useRouter()
|
||||
|
||||
const options = [
|
||||
{
|
||||
label: '个人中心',
|
||||
key: 'userCenter',
|
||||
icon: renderIcon('carbon:user-avatar-filled-alt'),
|
||||
},
|
||||
{
|
||||
type: 'divider',
|
||||
key: 'd1',
|
||||
},
|
||||
{
|
||||
label: 'Github',
|
||||
key: 'guthub',
|
||||
icon: renderIcon('icon-park-outline:github'),
|
||||
},
|
||||
{
|
||||
label: 'gitee',
|
||||
key: 'gitee',
|
||||
icon: renderIcon('simple-icons:gitee'),
|
||||
},
|
||||
{
|
||||
type: 'divider',
|
||||
key: 'd1',
|
||||
},
|
||||
{
|
||||
label: '退出登录',
|
||||
key: 'loginOut',
|
||||
icon: renderIcon('icon-park-outline:logout'),
|
||||
},
|
||||
]
|
||||
const options = computed(() => {
|
||||
return [
|
||||
{
|
||||
label: t('app.userCenter'),
|
||||
key: 'userCenter',
|
||||
icon: () => h(IconUser),
|
||||
},
|
||||
{
|
||||
type: 'divider',
|
||||
key: 'd1',
|
||||
},
|
||||
{
|
||||
label: 'Github',
|
||||
key: 'guthub',
|
||||
icon: () => h(IconGithub),
|
||||
},
|
||||
{
|
||||
label: 'Gitee',
|
||||
key: 'gitee',
|
||||
icon: renderIcon('simple-icons:gitee'),
|
||||
},
|
||||
{
|
||||
label: 'Docs',
|
||||
key: 'docs',
|
||||
icon: () => h(IconBookOpen),
|
||||
},
|
||||
{
|
||||
type: 'divider',
|
||||
key: 'd1',
|
||||
},
|
||||
{
|
||||
label: t('app.loginOut'),
|
||||
key: 'loginOut',
|
||||
icon: () => h(IconLogout),
|
||||
},
|
||||
]
|
||||
})
|
||||
function handleSelect(key: string | number) {
|
||||
if (key === 'loginOut') {
|
||||
window.$dialog?.info({
|
||||
title: '退出登录',
|
||||
content: '确认退出当前账号?',
|
||||
positiveText: '确定',
|
||||
negativeText: '取消',
|
||||
title: t('app.loginOutTitle'),
|
||||
content: t('app.loginOutContent'),
|
||||
positiveText: t('common.confirm'),
|
||||
negativeText: t('common.cancel'),
|
||||
onPositiveClick: () => {
|
||||
resetAuthStore()
|
||||
},
|
||||
@ -55,6 +68,9 @@ function handleSelect(key: string | number) {
|
||||
|
||||
if (key === 'gitee')
|
||||
window.open('https://gitee.com/chansee97/nova-admin')
|
||||
|
||||
if (key === 'docs')
|
||||
window.open('https://nova-admin-docs.netlify.app/')
|
||||
}
|
||||
</script>
|
||||
|
||||
@ -64,14 +80,17 @@ function handleSelect(key: string | number) {
|
||||
:options="options"
|
||||
@select="handleSelect"
|
||||
>
|
||||
<CommonWrapper>
|
||||
<n-avatar
|
||||
round
|
||||
size="large"
|
||||
:src="userInfo?.avatar"
|
||||
/>
|
||||
<span class="overflow-hidden text-ellipsis whitespace-nowrap">{{ userInfo?.nickname }}</span>
|
||||
</CommonWrapper>
|
||||
<n-avatar
|
||||
round
|
||||
|
||||
:src="userInfo?.avatar"
|
||||
>
|
||||
<template #fallback>
|
||||
<div class="wh-full flex-center">
|
||||
<icon-park-outline-user />
|
||||
</div>
|
||||
</template>
|
||||
</n-avatar>
|
||||
</n-dropdown>
|
||||
</template>
|
||||
|
||||
|
@ -9,16 +9,13 @@ const name = import.meta.env.VITE_APP_NAME
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="h-60px text-xl flex-center cursor-pointer"
|
||||
class="h-60px text-xl flex-center cursor-pointer gap-2 p-x-2"
|
||||
@click="router.push('/')"
|
||||
>
|
||||
<SvgIcon
|
||||
name="logo"
|
||||
:size="32"
|
||||
/>
|
||||
<svg-icons-logo class="text-1.5em" />
|
||||
<span
|
||||
v-show="!appStore.collapsed"
|
||||
class="mx-3 text-ellipsis overflow-hidden whitespace-nowrap"
|
||||
class="text-ellipsis overflow-hidden whitespace-nowrap"
|
||||
>{{ name }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
39
src/layouts/components/tab/DropTabs.vue
Normal file
39
src/layouts/components/tab/DropTabs.vue
Normal file
@ -0,0 +1,39 @@
|
||||
<script setup lang="ts">
|
||||
import { useTabStore } from '@/store'
|
||||
import { renderIcon } from '@/utils'
|
||||
|
||||
const tabStore = useTabStore()
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
function renderDropTabsLabel(option: any) {
|
||||
return t(`route.${String(option.name)}`, option.meta.title)
|
||||
}
|
||||
function renderDropTabsIcon(option: any) {
|
||||
return renderIcon(option.meta.icon)!()
|
||||
}
|
||||
|
||||
const router = useRouter()
|
||||
function handleDropTabs(key: string, option: any) {
|
||||
router.push(option.path)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-dropdown
|
||||
:options="tabStore.allTabs"
|
||||
:render-label="renderDropTabsLabel"
|
||||
:render-icon="renderDropTabsIcon"
|
||||
trigger="click"
|
||||
size="small"
|
||||
@select="handleDropTabs"
|
||||
>
|
||||
<CommonWrapper>
|
||||
<icon-park-outline-application-menu />
|
||||
</CommonWrapper>
|
||||
</n-dropdown>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
@ -18,10 +18,10 @@ function handleReload() {
|
||||
<n-tooltip placement="bottom" trigger="hover">
|
||||
<template #trigger>
|
||||
<CommonWrapper @click="handleReload">
|
||||
<i-icon-park-outline-refresh :class="{ 'animate-spin': loading }" />
|
||||
<icon-park-outline-refresh :class="{ 'animate-spin': loading }" />
|
||||
</CommonWrapper>
|
||||
</template>
|
||||
<span>刷新页面</span>
|
||||
<span>{{ $t('common.reload') }}</span>
|
||||
</n-tooltip>
|
||||
</template>
|
||||
|
||||
|
@ -1,8 +1,14 @@
|
||||
<script setup lang="ts">
|
||||
import type { RouteLocationNormalized } from 'vue-router'
|
||||
import Reload from './Reload.vue'
|
||||
import { renderIcon } from '@/utils'
|
||||
import DropTabs from './DropTabs.vue'
|
||||
import { useAppStore, useTabStore } from '@/store'
|
||||
import IconRedo from '~icons/icon-park-outline/redo'
|
||||
import IconClose from '~icons/icon-park-outline/close'
|
||||
import IconDelete from '~icons/icon-park-outline/delete-four'
|
||||
import IconLeft from '~icons/icon-park-outline/to-left'
|
||||
import IconRight from '~icons/icon-park-outline/to-right'
|
||||
import IconFullwith from '~icons/icon-park-outline/fullwidth'
|
||||
|
||||
const tabStore = useTabStore()
|
||||
const appStore = useAppStore()
|
||||
@ -14,38 +20,41 @@ function handleTab(route: RouteLocationNormalized) {
|
||||
function handleClose(path: string) {
|
||||
tabStore.closeTab(path)
|
||||
}
|
||||
const options = [
|
||||
{
|
||||
label: '刷新',
|
||||
key: 'reload',
|
||||
icon: renderIcon('icon-park-outline:redo'),
|
||||
},
|
||||
{
|
||||
label: '关闭',
|
||||
key: 'closeCurrent',
|
||||
icon: renderIcon('icon-park-outline:close'),
|
||||
},
|
||||
{
|
||||
label: '关闭其他',
|
||||
key: 'closeOther',
|
||||
icon: renderIcon('icon-park-outline:delete-four'),
|
||||
},
|
||||
{
|
||||
label: '关闭左侧',
|
||||
key: 'closeLeft',
|
||||
icon: renderIcon('icon-park-outline:to-left'),
|
||||
},
|
||||
{
|
||||
label: '关闭右侧',
|
||||
key: 'closeRight',
|
||||
icon: renderIcon('icon-park-outline:to-right'),
|
||||
},
|
||||
{
|
||||
label: '全部关闭',
|
||||
key: 'closeAll',
|
||||
icon: renderIcon('icon-park-outline:fullwidth'),
|
||||
},
|
||||
]
|
||||
const { t } = useI18n()
|
||||
const options = computed(() => {
|
||||
return [
|
||||
{
|
||||
label: t('common.reload'),
|
||||
key: 'reload',
|
||||
icon: () => h(IconRedo),
|
||||
},
|
||||
{
|
||||
label: t('common.close'),
|
||||
key: 'closeCurrent',
|
||||
icon: () => h(IconClose),
|
||||
},
|
||||
{
|
||||
label: t('app.closeOther'),
|
||||
key: 'closeOther',
|
||||
icon: () => h(IconDelete),
|
||||
},
|
||||
{
|
||||
label: t('app.closeLeft'),
|
||||
key: 'closeLeft',
|
||||
icon: () => h(IconLeft),
|
||||
},
|
||||
{
|
||||
label: t('app.closeRight'),
|
||||
key: 'closeRight',
|
||||
icon: () => h(IconRight),
|
||||
},
|
||||
{
|
||||
label: t('app.closeAll'),
|
||||
key: 'closeAll',
|
||||
icon: () => h(IconFullwith),
|
||||
},
|
||||
]
|
||||
})
|
||||
const showDropdown = ref(false)
|
||||
const x = ref(0)
|
||||
const y = ref(0)
|
||||
@ -91,17 +100,6 @@ function handleContextMenu(e: MouseEvent, route: RouteLocationNormalized) {
|
||||
function onClickoutside() {
|
||||
showDropdown.value = false
|
||||
}
|
||||
|
||||
function renderDropTabsLabel(option: any) {
|
||||
return option.meta.title
|
||||
}
|
||||
function renderDropTabsIcon(option: any) {
|
||||
return renderIcon(option.meta.icon)!()
|
||||
}
|
||||
|
||||
function handleDropTabs(key: string, option: any) {
|
||||
router.push(option.path)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@ -119,7 +117,9 @@ function handleDropTabs(key: string, option: any) {
|
||||
:name="item.path"
|
||||
@click="router.push(item.path)"
|
||||
>
|
||||
{{ item.meta.title }}
|
||||
<div class="flex-x-center gap-2">
|
||||
<nova-icon :icon="item.meta.icon" /> {{ $t(`route.${String(item.name)}`, item.meta.title) }}
|
||||
</div>
|
||||
</n-tab>
|
||||
<n-tab
|
||||
v-for="item in tabStore.tabs"
|
||||
@ -130,23 +130,12 @@ function handleDropTabs(key: string, option: any) {
|
||||
@contextmenu="handleContextMenu($event, item)"
|
||||
>
|
||||
<div class="flex-x-center gap-2">
|
||||
<e-icon :icon="item.meta.icon" /> {{ item.meta.title }}
|
||||
<nova-icon :icon="item.meta.icon" /> {{ $t(`route.${String(item.name)}`, item.meta.title) }}
|
||||
</div>
|
||||
</n-tab>
|
||||
<template #suffix>
|
||||
<Reload />
|
||||
<n-dropdown
|
||||
:options="tabStore.allTabs"
|
||||
:render-label="renderDropTabsLabel"
|
||||
:render-icon="renderDropTabsIcon"
|
||||
trigger="click"
|
||||
size="small"
|
||||
@select="handleDropTabs"
|
||||
>
|
||||
<CommonWrapper>
|
||||
<i-icon-park-outline-application-menu />
|
||||
</CommonWrapper>
|
||||
</n-dropdown>
|
||||
<DropTabs />
|
||||
</template>
|
||||
</n-tabs>
|
||||
<n-dropdown
|
||||
@ -162,4 +151,4 @@ function handleDropTabs(key: string, option: any) {
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
<style scoped></style>./DropTabs.vue
|
||||
|
@ -1,3 +0,0 @@
|
||||
const BasicLayout = () => import('./BasicLayout/index.vue')
|
||||
|
||||
export { BasicLayout }
|
15
src/layouts/index.vue
Normal file
15
src/layouts/index.vue
Normal file
@ -0,0 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
import leftMenu from './leftMenu.layout.vue'
|
||||
import topMenu from './topMenu.layout.vue'
|
||||
import { useAppStore } from '@/store/app'
|
||||
|
||||
const appStore = useAppStore()
|
||||
const layoutMap = {
|
||||
leftMenu,
|
||||
topMenu,
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<component :is="layoutMap[appStore.layoutMode]" />
|
||||
</template>
|
@ -11,7 +11,7 @@ import {
|
||||
Setting,
|
||||
TabBar,
|
||||
UserCenter,
|
||||
} from '../components'
|
||||
} from './components'
|
||||
import { useAppStore, useRouteStore } from '@/store'
|
||||
|
||||
const routeStore = useRouteStore()
|
||||
@ -49,11 +49,12 @@ const appStore = useAppStore()
|
||||
<CollapaseButton />
|
||||
<Breadcrumb />
|
||||
</div>
|
||||
<div class="flex-y-center h-full">
|
||||
<div class="flex-y-center gap-1 h-full p-x-xl">
|
||||
<Search />
|
||||
<Notices />
|
||||
<FullScreen />
|
||||
<DarkModeSwitch />
|
||||
<LangsSwitch />
|
||||
<Setting />
|
||||
<UserCenter />
|
||||
</div>
|
59
src/layouts/topMenu.layout.vue
Normal file
59
src/layouts/topMenu.layout.vue
Normal file
@ -0,0 +1,59 @@
|
||||
<script lang="ts" setup>
|
||||
import {
|
||||
BackTop,
|
||||
FullScreen,
|
||||
Logo,
|
||||
Menu,
|
||||
Notices,
|
||||
Search,
|
||||
Setting,
|
||||
TabBar,
|
||||
UserCenter,
|
||||
} from './components'
|
||||
import { useAppStore, useRouteStore } from '@/store'
|
||||
|
||||
const routeStore = useRouteStore()
|
||||
const appStore = useAppStore()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-layout class="wh-full" embedded>
|
||||
<n-layout
|
||||
class="h-full flex flex-col" content-style="display: flex;flex-direction: column;min-height:100%;"
|
||||
embedded :native-scrollbar="false"
|
||||
>
|
||||
<n-layout-header bordered position="absolute" class="z-1">
|
||||
<div class="h-60px flex-y-center justify-between shrink-0">
|
||||
<Logo v-if="appStore.showLogo" />
|
||||
<Menu mode="horizontal" responsive />
|
||||
<div class="flex-y-center gap-1 h-full p-x-xl">
|
||||
<Search />
|
||||
<Notices />
|
||||
<FullScreen />
|
||||
<DarkModeSwitch />
|
||||
<LangsSwitch />
|
||||
<Setting />
|
||||
<UserCenter />
|
||||
</div>
|
||||
</div>
|
||||
<TabBar v-if="appStore.showTabs" class="h-45px" />
|
||||
</n-layout-header>
|
||||
<div class="flex-1 p-16px flex flex-col">
|
||||
<div class="h-60px" />
|
||||
<div v-if="appStore.showTabs" class="h-45px" />
|
||||
<router-view v-slot="{ Component, route }" class="flex-1">
|
||||
<transition :name="appStore.transitionAnimation" mode="out-in">
|
||||
<keep-alive :include="routeStore.cacheRoutes">
|
||||
<component :is="Component" v-if="appStore.loadFlag" :key="route.fullPath" />
|
||||
</keep-alive>
|
||||
</transition>
|
||||
</router-view>
|
||||
<div v-if="appStore.showFooter" class="h-40px" />
|
||||
</div>
|
||||
<n-layout-footer v-if="appStore.showFooter" bordered position="absolute" class="h-40px flex-center">
|
||||
{{ appStore.footerText }}
|
||||
</n-layout-footer>
|
||||
<BackTop />
|
||||
</n-layout>
|
||||
</n-layout>
|
||||
</template>
|
@ -1,6 +1,6 @@
|
||||
import type { App } from 'vue'
|
||||
import AppVue from './App.vue'
|
||||
import AppLoading from './components/common/appLoading.vue'
|
||||
import AppLoading from './components/common/AppLoading.vue'
|
||||
import { installRouter } from '@/router'
|
||||
import { installPinia } from '@/store'
|
||||
|
||||
@ -18,7 +18,7 @@ async function setupApp() {
|
||||
// 注册模块 Vue-router
|
||||
await installRouter(app)
|
||||
|
||||
/* 注册模块 Vue-router/Pinia */
|
||||
/* 注册模块 指令/静态资源 */
|
||||
Object.values(
|
||||
import.meta.glob<{ install: (app: App) => void }>('./modules/*.ts', {
|
||||
eager: true,
|
||||
|
@ -1,6 +1,5 @@
|
||||
import 'uno.css'
|
||||
import '@/styles/index.css'
|
||||
import 'virtual:svg-icons-register'
|
||||
|
||||
// 全局引入的静态资源
|
||||
export function install() {
|
||||
|
19
src/modules/i18n.ts
Normal file
19
src/modules/i18n.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import { createI18n } from 'vue-i18n'
|
||||
import type { App } from 'vue'
|
||||
import enUS from '../../locales/en_US.json'
|
||||
import zhCN from '../../locales/zh_CN.json'
|
||||
import { local } from '@/utils'
|
||||
|
||||
export const i18n = createI18n({
|
||||
legacy: false,
|
||||
locale: local.get('lang') || 'enUS', // 默认显示语言
|
||||
fallbackLocale: 'enUS',
|
||||
messages: {
|
||||
zhCN,
|
||||
enUS,
|
||||
},
|
||||
})
|
||||
|
||||
export function install(app: App) {
|
||||
app.use(i18n)
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
import NProgress from 'nprogress'
|
||||
import { useAppStore } from '@/store'
|
||||
|
||||
export function install() {
|
||||
// 初始载入,初始化body的css变量
|
||||
const appStore = useAppStore()
|
||||
appStore.setPrimaryColor(appStore.primaryColor)
|
||||
|
||||
NProgress.configure({ easing: 'ease', speed: 500 })
|
||||
window.$NProgress = NProgress
|
||||
}
|
@ -15,11 +15,11 @@ export function setupRouterGuard(router: Router) {
|
||||
window.open(to.meta.herf)
|
||||
return false
|
||||
}
|
||||
// 开始 NProgress
|
||||
appStore.showProgress && window.$NProgress?.start()
|
||||
// 开始 loadingBar
|
||||
appStore.showProgress && window.$loadingBar?.start()
|
||||
|
||||
// 判断有无TOKEN,登录鉴权
|
||||
const isLogin = Boolean(local.get('token'))
|
||||
const isLogin = Boolean(local.get('accessToken'))
|
||||
if (!isLogin) {
|
||||
if (to.name === 'login')
|
||||
next()
|
||||
@ -67,7 +67,7 @@ export function setupRouterGuard(router: Router) {
|
||||
router.afterEach((to) => {
|
||||
// 修改网页标题
|
||||
document.title = `${to.meta.title} - ${title}`
|
||||
// 结束 NProgress
|
||||
appStore.showProgress && window.$NProgress?.done()
|
||||
// 结束 loadingBar
|
||||
appStore.showProgress && window.$loadingBar?.finish()
|
||||
})
|
||||
}
|
||||
|
@ -6,7 +6,7 @@ export const routes: RouteRecordRaw[] = [
|
||||
path: '/',
|
||||
name: 'root',
|
||||
redirect: '/appRoot',
|
||||
component: () => import('@/layouts/index'),
|
||||
// component: () => import('@/layouts/index'),
|
||||
children: [
|
||||
],
|
||||
},
|
||||
|
@ -5,27 +5,30 @@ export const staticRoutes: AppRoute.RowRoute[] = [
|
||||
'meta.title': '仪表盘',
|
||||
'meta.requiresAuth': true,
|
||||
'meta.icon': 'icon-park-outline:analysis',
|
||||
'meta.menuType': 'dir',
|
||||
'componentPath': null,
|
||||
'id': 1,
|
||||
'pid': null,
|
||||
},
|
||||
{
|
||||
'name': 'dashboard_workbench',
|
||||
'name': 'workbench',
|
||||
'path': '/dashboard/workbench',
|
||||
'meta.title': '工作台',
|
||||
'meta.requiresAuth': true,
|
||||
'meta.icon': 'icon-park-outline:alarm',
|
||||
'meta.pinTab': true,
|
||||
'meta.menuType': 'page',
|
||||
'componentPath': '/dashboard/workbench/index.vue',
|
||||
'id': 2,
|
||||
'pid': 1,
|
||||
},
|
||||
{
|
||||
'name': 'dashboard_monitor',
|
||||
'name': 'monitor',
|
||||
'path': '/dashboard/monitor',
|
||||
'meta.title': '监控页',
|
||||
'meta.requiresAuth': true,
|
||||
'meta.icon': 'icon-park-outline:anchor',
|
||||
'meta.menuType': 'page',
|
||||
'componentPath': '/dashboard/monitor/index.vue',
|
||||
'id': 3,
|
||||
'pid': 1,
|
||||
@ -36,6 +39,7 @@ export const staticRoutes: AppRoute.RowRoute[] = [
|
||||
'meta.title': '多级菜单演示',
|
||||
'meta.requiresAuth': true,
|
||||
'meta.icon': 'icon-park-outline:list',
|
||||
'meta.menuType': 'dir',
|
||||
'componentPath': null,
|
||||
'id': 4,
|
||||
'pid': null,
|
||||
@ -46,18 +50,20 @@ export const staticRoutes: AppRoute.RowRoute[] = [
|
||||
'meta.title': '多级菜单子页',
|
||||
'meta.requiresAuth': true,
|
||||
'meta.icon': 'icon-park-outline:list',
|
||||
'meta.menuType': 'page',
|
||||
'componentPath': '/test/test2/index.vue',
|
||||
'id': 6,
|
||||
'pid': 4,
|
||||
},
|
||||
{
|
||||
'name': 'test2_detail',
|
||||
'name': 'test2Detail',
|
||||
'path': '/test/test2/detail',
|
||||
'meta.title': '多级菜单的详情页',
|
||||
'meta.requiresAuth': true,
|
||||
'meta.icon': 'icon-park-outline:list',
|
||||
'meta.hide': true,
|
||||
'meta.activeMenu': '/test/test2',
|
||||
'meta.menuType': 'page',
|
||||
'componentPath': '/test/test2/detail/index.vue',
|
||||
'id': 7,
|
||||
'pid': 4,
|
||||
@ -68,6 +74,7 @@ export const staticRoutes: AppRoute.RowRoute[] = [
|
||||
'meta.title': '多级菜单',
|
||||
'meta.requiresAuth': true,
|
||||
'meta.icon': 'icon-park-outline:list',
|
||||
'meta.menuType': 'dir',
|
||||
'componentPath': null,
|
||||
'id': 8,
|
||||
'pid': 4,
|
||||
@ -88,12 +95,13 @@ export const staticRoutes: AppRoute.RowRoute[] = [
|
||||
'meta.title': '列表页',
|
||||
'meta.requiresAuth': true,
|
||||
'meta.icon': 'icon-park-outline:list-two',
|
||||
'meta.menuType': 'dir',
|
||||
'componentPath': null,
|
||||
'id': 10,
|
||||
'pid': null,
|
||||
},
|
||||
{
|
||||
'name': 'list_commonList',
|
||||
'name': 'commonList',
|
||||
'path': '/list/commonList',
|
||||
'meta.title': '常用列表',
|
||||
'meta.requiresAuth': true,
|
||||
@ -103,7 +111,7 @@ export const staticRoutes: AppRoute.RowRoute[] = [
|
||||
'pid': 10,
|
||||
},
|
||||
{
|
||||
'name': 'list_cardList',
|
||||
'name': 'cardList',
|
||||
'path': '/list/cardList',
|
||||
'meta.title': '卡片列表',
|
||||
'meta.requiresAuth': true,
|
||||
@ -113,103 +121,105 @@ export const staticRoutes: AppRoute.RowRoute[] = [
|
||||
'pid': 10,
|
||||
},
|
||||
{
|
||||
'name': 'plugin',
|
||||
'path': '/plugin',
|
||||
'name': 'demo',
|
||||
'path': '/demo',
|
||||
'meta.title': '功能示例',
|
||||
'meta.requiresAuth': true,
|
||||
'meta.icon': 'icon-park-outline:application-one',
|
||||
'meta.menuType': 'dir',
|
||||
'componentPath': null,
|
||||
'id': 13,
|
||||
'pid': null,
|
||||
},
|
||||
{
|
||||
'name': 'fetch',
|
||||
'path': '/plugin/fetch',
|
||||
'meta.title': '接口功能测试',
|
||||
'path': '/demo/fetch',
|
||||
'meta.title': '请求示例',
|
||||
'meta.requiresAuth': true,
|
||||
'meta.icon': 'icon-park-outline:international',
|
||||
'componentPath': '/plugin/fetch/index.vue',
|
||||
'componentPath': '/demo/fetch/index.vue',
|
||||
'id': 5,
|
||||
'pid': 13,
|
||||
},
|
||||
{
|
||||
'name': 'plugin_echarts',
|
||||
'path': '/plugin/echarts',
|
||||
'name': 'echarts',
|
||||
'path': '/demo/echarts',
|
||||
'meta.title': 'ECharts',
|
||||
'meta.requiresAuth': true,
|
||||
'meta.icon': 'icon-park-outline:chart-proportion',
|
||||
'componentPath': '/plugin/echarts/index.vue',
|
||||
'componentPath': '/demo/echarts/index.vue',
|
||||
'id': 15,
|
||||
'pid': 13,
|
||||
},
|
||||
{
|
||||
'name': 'PluginMap',
|
||||
'path': '/plugin/map',
|
||||
'name': 'map',
|
||||
'path': '/demo/map',
|
||||
'meta.title': '地图',
|
||||
'meta.requiresAuth': true,
|
||||
'meta.icon': 'carbon:map',
|
||||
'meta.keepAlive': true,
|
||||
'componentPath': '/plugin/map/index.vue',
|
||||
'componentPath': '/demo/map/index.vue',
|
||||
'id': 17,
|
||||
'pid': 13,
|
||||
},
|
||||
{
|
||||
'name': 'plugin_editor',
|
||||
'path': '/plugin/editor',
|
||||
'name': 'editor',
|
||||
'path': '/demo/editor',
|
||||
'meta.title': '编辑器',
|
||||
'meta.requiresAuth': true,
|
||||
'meta.icon': 'icon-park-outline:editor',
|
||||
'meta.menuType': 'dir',
|
||||
'componentPath': null,
|
||||
'id': 18,
|
||||
'pid': 13,
|
||||
},
|
||||
{
|
||||
'name': 'plugin_md',
|
||||
'path': '/plugin/editor/md',
|
||||
'name': 'editorMd',
|
||||
'path': '/demo/editor/md',
|
||||
'meta.title': 'MarkDown',
|
||||
'meta.requiresAuth': true,
|
||||
'meta.icon': 'ri:markdown-line',
|
||||
'componentPath': '/plugin/editor/md/index.vue',
|
||||
'componentPath': '/demo/editor/md/index.vue',
|
||||
'id': 19,
|
||||
'pid': 18,
|
||||
},
|
||||
{
|
||||
'name': 'plugin_rich',
|
||||
'path': '/plugin/editor/rich',
|
||||
'name': 'editorRich',
|
||||
'path': '/demo/editor/rich',
|
||||
'meta.title': '富文本',
|
||||
'meta.requiresAuth': true,
|
||||
'meta.icon': 'icon-park-outline:edit-one',
|
||||
'componentPath': '/plugin/editor/rich/index.vue',
|
||||
'componentPath': '/demo/editor/rich/index.vue',
|
||||
'id': 20,
|
||||
'pid': 18,
|
||||
},
|
||||
{
|
||||
'name': 'plugin_clipboard',
|
||||
'path': '/plugin/clipboard',
|
||||
'name': 'clipboard',
|
||||
'path': '/demo/clipboard',
|
||||
'meta.title': '剪贴板',
|
||||
'meta.requiresAuth': true,
|
||||
'meta.icon': 'icon-park-outline:clipboard',
|
||||
'componentPath': '/plugin/clipboard/index.vue',
|
||||
'componentPath': '/demo/clipboard/index.vue',
|
||||
'id': 21,
|
||||
'pid': 13,
|
||||
},
|
||||
{
|
||||
'name': 'plugin_icons',
|
||||
'path': '/plugin/icons',
|
||||
'name': 'icons',
|
||||
'path': '/demo/icons',
|
||||
'meta.title': '图标',
|
||||
'meta.requiresAuth': true,
|
||||
'meta.icon': 'icon-park-outline:winking-face-with-open-eyes',
|
||||
'componentPath': '/plugin/icons/index.vue',
|
||||
'componentPath': '/demo/icons/index.vue',
|
||||
'id': 22,
|
||||
'pid': 13,
|
||||
},
|
||||
{
|
||||
'name': 'plugin_QRCode',
|
||||
'path': '/plugin/QRCode',
|
||||
'name': 'QRCode',
|
||||
'path': '/demo/QRCode',
|
||||
'meta.title': '二维码',
|
||||
'meta.requiresAuth': true,
|
||||
'meta.icon': 'icon-park-outline:two-dimensional-code',
|
||||
'componentPath': '/plugin/QRCode/index.vue',
|
||||
'componentPath': '/demo/QRCode/index.vue',
|
||||
'id': 23,
|
||||
'pid': 13,
|
||||
},
|
||||
@ -219,14 +229,15 @@ export const staticRoutes: AppRoute.RowRoute[] = [
|
||||
'meta.title': '外链文档',
|
||||
'meta.requiresAuth': true,
|
||||
'meta.icon': 'icon-park-outline:file-doc',
|
||||
'meta.menuType': 'dir',
|
||||
'componentPath': null,
|
||||
'id': 24,
|
||||
'pid': null,
|
||||
},
|
||||
{
|
||||
'name': 'docments_vue',
|
||||
'name': 'docmentsVue',
|
||||
'path': '/docments/vue',
|
||||
'meta.title': 'vue',
|
||||
'meta.title': 'Vue',
|
||||
'meta.requiresAuth': true,
|
||||
'meta.icon': 'logos:vue',
|
||||
'componentPath': '/docments/vue/index.vue',
|
||||
@ -234,9 +245,9 @@ export const staticRoutes: AppRoute.RowRoute[] = [
|
||||
'pid': 24,
|
||||
},
|
||||
{
|
||||
'name': 'docments_vite',
|
||||
'name': 'docmentsVite',
|
||||
'path': '/docments/vite',
|
||||
'meta.title': 'vite',
|
||||
'meta.title': 'Vite',
|
||||
'meta.requiresAuth': true,
|
||||
'meta.icon': 'logos:vitejs',
|
||||
'componentPath': '/docments/vite/index.vue',
|
||||
@ -244,28 +255,29 @@ export const staticRoutes: AppRoute.RowRoute[] = [
|
||||
'pid': 24,
|
||||
},
|
||||
{
|
||||
'name': 'docments_vueuse',
|
||||
'name': 'docmentsVueuse',
|
||||
'path': '/docments/vueuse',
|
||||
'meta.title': 'VueUse(外链)',
|
||||
'meta.requiresAuth': true,
|
||||
'meta.icon': 'logos:vueuse',
|
||||
'meta.herf': 'https://vueuse.org/guide/',
|
||||
'componentPath': '/docments/vueuse/index.vue',
|
||||
'componentPath': 'null',
|
||||
'id': 27,
|
||||
'pid': 24,
|
||||
},
|
||||
{
|
||||
'name': 'permission',
|
||||
'path': '/permission',
|
||||
'meta.title': '权限示例',
|
||||
'meta.title': '权限',
|
||||
'meta.requiresAuth': true,
|
||||
'meta.icon': 'icon-park-outline:people-safe',
|
||||
'meta.menuType': 'dir',
|
||||
'componentPath': null,
|
||||
'id': 28,
|
||||
'pid': null,
|
||||
},
|
||||
{
|
||||
'name': 'permission_permission',
|
||||
'name': 'permissionDemo',
|
||||
'path': '/permission/permission',
|
||||
'meta.title': '权限示例',
|
||||
'meta.requiresAuth': true,
|
||||
@ -275,9 +287,9 @@ export const staticRoutes: AppRoute.RowRoute[] = [
|
||||
'pid': 28,
|
||||
},
|
||||
{
|
||||
'name': 'permission_justSuper',
|
||||
'name': 'justSuper',
|
||||
'path': '/permission/justSuper',
|
||||
'meta.title': '超管super可见',
|
||||
'meta.title': 'super可见',
|
||||
'meta.requiresAuth': true,
|
||||
'meta.roles': [
|
||||
'super',
|
||||
@ -293,6 +305,7 @@ export const staticRoutes: AppRoute.RowRoute[] = [
|
||||
'meta.title': '异常页',
|
||||
'meta.requiresAuth': true,
|
||||
'meta.icon': 'icon-park-outline:error-computer',
|
||||
'meta.menuType': 'dir',
|
||||
'componentPath': null,
|
||||
'id': 31,
|
||||
'pid': null,
|
||||
@ -300,7 +313,7 @@ export const staticRoutes: AppRoute.RowRoute[] = [
|
||||
{
|
||||
'name': 'demo403',
|
||||
'path': '/error/403',
|
||||
'meta.title': '403页',
|
||||
'meta.title': '403',
|
||||
'meta.requiresAuth': true,
|
||||
'meta.icon': 'carbon:error',
|
||||
'meta.order': 3,
|
||||
@ -311,7 +324,7 @@ export const staticRoutes: AppRoute.RowRoute[] = [
|
||||
{
|
||||
'name': 'demo404',
|
||||
'path': '/error/404',
|
||||
'meta.title': '404页',
|
||||
'meta.title': '404',
|
||||
'meta.requiresAuth': true,
|
||||
'meta.icon': 'icon-park-outline:error',
|
||||
'meta.order': 2,
|
||||
@ -322,7 +335,7 @@ export const staticRoutes: AppRoute.RowRoute[] = [
|
||||
{
|
||||
'name': 'demo500',
|
||||
'path': '/error/500',
|
||||
'meta.title': '500页',
|
||||
'meta.title': '500',
|
||||
'meta.requiresAuth': true,
|
||||
'meta.icon': 'carbon:data-error',
|
||||
'meta.order': 1,
|
||||
@ -336,12 +349,13 @@ export const staticRoutes: AppRoute.RowRoute[] = [
|
||||
'meta.title': '系统设置',
|
||||
'meta.requiresAuth': true,
|
||||
'meta.icon': 'icon-park-outline:setting',
|
||||
'meta.menuType': 'dir',
|
||||
'componentPath': null,
|
||||
'id': 35,
|
||||
'pid': null,
|
||||
},
|
||||
{
|
||||
'name': 'setting_account',
|
||||
'name': 'accountSetting',
|
||||
'path': '/setting/account',
|
||||
'meta.title': '用户设置',
|
||||
'meta.requiresAuth': true,
|
||||
@ -351,7 +365,7 @@ export const staticRoutes: AppRoute.RowRoute[] = [
|
||||
'pid': 35,
|
||||
},
|
||||
{
|
||||
'name': 'setting_dictionary',
|
||||
'name': 'dictionarySetting',
|
||||
'path': '/setting/dictionary',
|
||||
'meta.title': '字典设置',
|
||||
'meta.requiresAuth': true,
|
||||
@ -361,7 +375,7 @@ export const staticRoutes: AppRoute.RowRoute[] = [
|
||||
'pid': 35,
|
||||
},
|
||||
{
|
||||
'name': 'setting_menu',
|
||||
'name': 'menuSetting',
|
||||
'path': '/setting/menu',
|
||||
'meta.title': '菜单设置',
|
||||
'meta.requiresAuth': true,
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { alovaInstance } from '../http'
|
||||
import { request } from '../http'
|
||||
|
||||
export function fetchUserList() {
|
||||
return alovaInstance.Get('/userList')
|
||||
return request.Get('/userList')
|
||||
}
|
||||
|
@ -1,19 +1,19 @@
|
||||
import { alovaInstance } from '../http'
|
||||
import { request } from '../http'
|
||||
|
||||
interface Ilogin {
|
||||
username: string
|
||||
userName: string
|
||||
password: string
|
||||
}
|
||||
|
||||
export function fetchLogin(params: Ilogin) {
|
||||
const methodInstance = alovaInstance.Post<any>('/login', params)
|
||||
const methodInstance = request.Post<Service.ResponseResult<ApiAuth.loginInfo>>('/login', params)
|
||||
methodInstance.meta = {
|
||||
authRole: null,
|
||||
}
|
||||
return methodInstance
|
||||
}
|
||||
export function fetchUpdateToken(params: any) {
|
||||
const method = alovaInstance.Post<ApiAuth.loginInfo>('/updateToken', params)
|
||||
export function fetchUpdateToken(data: any) {
|
||||
const method = request.Post<Service.ResponseResult<ApiAuth.loginInfo>>('/updateToken', data)
|
||||
method.meta = {
|
||||
authRole: 'refreshToken',
|
||||
}
|
||||
@ -21,5 +21,5 @@ export function fetchUpdateToken(params: any) {
|
||||
}
|
||||
|
||||
export function fetchUserRoutes(params: { id: number }) {
|
||||
return alovaInstance.Get<AppRoute.RowRoute[]>('/getUserRoutes', { params })
|
||||
return request.Get<Service.ResponseResult<AppRoute.RowRoute[]> >('/getUserRoutes', { params })
|
||||
}
|
||||
|
5
src/service/api/system.ts
Normal file
5
src/service/api/system.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import { request } from '../http'
|
||||
|
||||
export function fetchAllRoutes() {
|
||||
return request.Get<Service.ResponseResult<AppRoute.RowRoute[]> >('/getUserRoutes')
|
||||
}
|
@ -1,17 +1,17 @@
|
||||
import { alovaInstance, blankInstance } from '../http'
|
||||
import { blankInstance, request } from '../http'
|
||||
|
||||
/* get方法测试 */
|
||||
export function fetachGet(params?: any) {
|
||||
return alovaInstance.Get('/getAPI', { params })
|
||||
return request.Get('/getAPI', { params })
|
||||
}
|
||||
|
||||
/* post方法测试 */
|
||||
export function fetchPost(data: any) {
|
||||
return alovaInstance.Post('/postAPI', data)
|
||||
return request.Post('/postAPI', data)
|
||||
}
|
||||
/* formPost方法测试 */
|
||||
export function fetchFormPost(data: any) {
|
||||
const methodInstance = alovaInstance.Post('/postFormAPI', data)
|
||||
const methodInstance = request.Post('/postFormAPI', data)
|
||||
methodInstance.meta = {
|
||||
isFormPost: true,
|
||||
}
|
||||
@ -19,15 +19,15 @@ export function fetchFormPost(data: any) {
|
||||
}
|
||||
/* delete方法测试 */
|
||||
export function fetchDelete() {
|
||||
return alovaInstance.Delete('/deleteAPI')
|
||||
return request.Delete('/deleteAPI')
|
||||
}
|
||||
/* put方法测试 */
|
||||
export function fetchPut(data: any) {
|
||||
return alovaInstance.Put('/putAPI', data)
|
||||
return request.Put('/putAPI', data)
|
||||
}
|
||||
/* 不携带token的接口 */
|
||||
export function withoutToken() {
|
||||
const methodInstance = alovaInstance.Get('/getAPI')
|
||||
const methodInstance = request.Get('/getAPI')
|
||||
methodInstance.meta = {
|
||||
authRole: null,
|
||||
}
|
||||
@ -35,12 +35,16 @@ export function withoutToken() {
|
||||
}
|
||||
/* 接口数据转换 */
|
||||
export function dictData() {
|
||||
return alovaInstance.Get('/getDictData', {
|
||||
return request.Get('/getDictData', {
|
||||
transformData(rawData, _headers) {
|
||||
const { data } = rawData as any
|
||||
const response = rawData as any
|
||||
return {
|
||||
gender: data.gender === 0 ? '男' : '女',
|
||||
status: `状态是${data.status}`,
|
||||
...response,
|
||||
data: {
|
||||
...response.data,
|
||||
gender: response.data.gender === 0 ? '男' : '女',
|
||||
status: `状态是${response.data.status}`,
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
@ -57,29 +61,34 @@ export function getBlob(url: string) {
|
||||
|
||||
/* 带进度的下载文件 */
|
||||
export function downloadFile(url: string) {
|
||||
return blankInstance.Get(url, {
|
||||
const methodInstance = blankInstance.Get<Blob>(url, {
|
||||
// 开启下载进度
|
||||
enableDownload: true,
|
||||
})
|
||||
methodInstance.meta = {
|
||||
// 标识为bolb数据
|
||||
isBlob: true,
|
||||
}
|
||||
return methodInstance
|
||||
}
|
||||
/* 测试状态码500失败 */
|
||||
export function FailedRequest() {
|
||||
return alovaInstance.Get('/serverError')
|
||||
return request.Get('/serverError')
|
||||
}
|
||||
|
||||
/* 测试业务码500失败 */
|
||||
export function FailedResponse() {
|
||||
return alovaInstance.Post('/businessError')
|
||||
return request.Post('/businessError')
|
||||
}
|
||||
/* 测试业务码10000失败,无提示 */
|
||||
export function FailedResponseWithoutTip() {
|
||||
return alovaInstance.Post('/businessErrorWithoutTip')
|
||||
return request.Post('/businessErrorWithoutTip')
|
||||
}
|
||||
/* token失效的接口 */
|
||||
export function expiredTokenRequest() {
|
||||
return alovaInstance.Get('/expiredToken')
|
||||
return request.Get('/expiredToken')
|
||||
}
|
||||
/* 测试token刷新接口 */
|
||||
export function refreshToken() {
|
||||
return alovaInstance.Get('/updataToken')
|
||||
return request.Get('/updataToken')
|
||||
}
|
||||
|
@ -30,7 +30,7 @@ const { onAuthRequired, onResponseRefreshToken } = createServerTokenAuthenticati
|
||||
},
|
||||
// 添加token到请求头
|
||||
assignToken: (method) => {
|
||||
method.config.headers.Authorization = `Bearer ${local.get('token')}`
|
||||
method.config.headers.Authorization = `Bearer ${local.get('accessToken')}`
|
||||
},
|
||||
})
|
||||
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { $t } from '@/utils'
|
||||
/** 默认实例的Aixos配置 */
|
||||
export const DEFAULT_ALOVA_OPTIONS = {
|
||||
// 请求超时时间,默认15秒
|
||||
@ -8,25 +9,25 @@ export const DEFAULT_ALOVA_OPTIONS = {
|
||||
export const DEFAULT_BACKEND_OPTIONS = {
|
||||
codeKey: 'code',
|
||||
dataKey: 'data',
|
||||
msgKey: 'msg',
|
||||
msgKey: 'message',
|
||||
successCode: 200,
|
||||
}
|
||||
|
||||
/** 请求不成功各种状态的错误 */
|
||||
export const ERROR_STATUS = {
|
||||
default: '请求错误~',
|
||||
400: '400: 请求出现语法错误~',
|
||||
401: '401: 用户未授权~',
|
||||
403: '403: 服务器拒绝访问~',
|
||||
404: '404: 请求的资源不存在~',
|
||||
405: '405: 请求方法未允许~',
|
||||
408: '408: 网络请求超时~',
|
||||
500: '500: 服务器内部错误~',
|
||||
501: '501: 服务器未实现请求功能~',
|
||||
502: '502: 错误网关~',
|
||||
503: '503: 服务不可用~',
|
||||
504: '504: 网关超时~',
|
||||
505: '505: http版本不支持该请求~',
|
||||
default: $t('http.defaultTip'),
|
||||
400: $t('http.400'),
|
||||
401: $t('http.401'),
|
||||
403: $t('http.403'),
|
||||
404: $t('http.404'),
|
||||
405: $t('http.405'),
|
||||
408: $t('http.408'),
|
||||
500: $t('http.500'),
|
||||
501: $t('http.501'),
|
||||
502: $t('http.502'),
|
||||
503: $t('http.503'),
|
||||
504: $t('http.504'),
|
||||
505: $t('http.505'),
|
||||
}
|
||||
|
||||
/** 没有错误提示的code */
|
||||
|
@ -17,12 +17,12 @@ export function handleResponseError(response: Response) {
|
||||
const error: Service.RequestError = {
|
||||
errorType: 'Response Error',
|
||||
code: 0,
|
||||
msg: ERROR_STATUS.default,
|
||||
message: ERROR_STATUS.default,
|
||||
data: null,
|
||||
}
|
||||
const errorCode: ErrorStatus = response.status as ErrorStatus
|
||||
const msg = ERROR_STATUS[errorCode] || ERROR_STATUS.default
|
||||
Object.assign(error, { code: errorCode, msg })
|
||||
const message = ERROR_STATUS[errorCode] || ERROR_STATUS.default
|
||||
Object.assign(error, { code: errorCode, message })
|
||||
|
||||
showError(error)
|
||||
|
||||
@ -40,7 +40,7 @@ export function handleBusinessError(data: Record<string, any>, config: Required<
|
||||
const error: Service.RequestError = {
|
||||
errorType: 'Business Error',
|
||||
code: data[codeKey],
|
||||
msg: data[msgKey],
|
||||
message: data[msgKey],
|
||||
data: data.data,
|
||||
}
|
||||
|
||||
@ -56,11 +56,12 @@ export function handleBusinessError(data: Record<string, any>, config: Required<
|
||||
* @return {*} result
|
||||
*/
|
||||
export function handleServiceResult(data: any, isSuccess: boolean = true) {
|
||||
return {
|
||||
const result = {
|
||||
isSuccess,
|
||||
errorType: null,
|
||||
...data,
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
@ -69,9 +70,9 @@ export function handleServiceResult(data: any, isSuccess: boolean = true) {
|
||||
*/
|
||||
export async function handleRefreshToken() {
|
||||
const authStore = useAuthStore()
|
||||
const data = await fetchUpdateToken({ refreshToken: local.get('refreshToken') })
|
||||
const { data } = await fetchUpdateToken({ refreshToken: local.get('refreshToken') })
|
||||
if (data) {
|
||||
local.set('token', data.accessToken)
|
||||
local.set('accessToken', data.accessToken)
|
||||
local.set('refreshToken', data.refreshToken)
|
||||
}
|
||||
else {
|
||||
@ -86,5 +87,5 @@ export function showError(error: Service.RequestError) {
|
||||
if (ERROR_NO_TIP_STATUS.includes(code))
|
||||
return
|
||||
|
||||
window.$message.error(error.msg)
|
||||
window.$message.error(error.message)
|
||||
}
|
||||
|
@ -2,11 +2,11 @@ import { createAlovaInstance } from './alova'
|
||||
import { serviceConfig } from '@/../service.config'
|
||||
import { generateProxyPattern } from '@/../build/proxy'
|
||||
|
||||
const isHttpProxy = import.meta.env.VITE_HTTP_PROXY === 'Y' || false
|
||||
const isHttpProxy = import.meta.env.DEV && import.meta.env.VITE_HTTP_PROXY === 'Y'
|
||||
|
||||
const { url } = generateProxyPattern(serviceConfig[import.meta.env.MODE])
|
||||
|
||||
export const alovaInstance = createAlovaInstance({
|
||||
export const request = createAlovaInstance({
|
||||
baseURL: isHttpProxy ? url.proxy : url.value,
|
||||
})
|
||||
|
||||
|
@ -1,3 +1,4 @@
|
||||
export * from './api/list'
|
||||
export * from './api/system'
|
||||
export * from './api/login'
|
||||
export * from './api/list'
|
||||
export * from './api/test'
|
||||
|
@ -1,9 +1,11 @@
|
||||
import type { GlobalThemeOverrides } from 'naive-ui'
|
||||
import chroma from 'chroma-js'
|
||||
import { colord } from 'colord'
|
||||
import { set } from 'radash'
|
||||
import themeConfig from './theme.json'
|
||||
import { local, setLocale } from '@/utils'
|
||||
|
||||
type TransitionAnimation = '' | 'fade-slide' | 'fade-bottom' | 'fade-scale' | 'zoom-fade' | 'zoom-out'
|
||||
export type LayoutMode = 'leftMenu' | 'topMenu'
|
||||
|
||||
const docEle = ref(document.documentElement)
|
||||
|
||||
@ -17,6 +19,7 @@ export const useAppStore = defineStore('app-store', {
|
||||
state: () => {
|
||||
return {
|
||||
footerText: 'Copyright © 2024 chansee97',
|
||||
lang: 'enUS' as App.lang,
|
||||
theme: themeConfig as GlobalThemeOverrides,
|
||||
primaryColor: themeConfig.common.primaryColor,
|
||||
collapsed: false,
|
||||
@ -31,6 +34,7 @@ export const useAppStore = defineStore('app-store', {
|
||||
showBreadcrumbIcon: true,
|
||||
showWatermark: false,
|
||||
transitionAnimation: 'fade-slide' as TransitionAnimation,
|
||||
layoutMode: 'leftMenu' as LayoutMode,
|
||||
}
|
||||
},
|
||||
getters: {
|
||||
@ -61,15 +65,20 @@ export const useAppStore = defineStore('app-store', {
|
||||
this.showBreadcrumbIcon = true
|
||||
this.showWatermark = false
|
||||
this.transitionAnimation = 'fade-slide'
|
||||
this.layoutMode = 'leftMenu'
|
||||
|
||||
// 重置所有配色
|
||||
this.setPrimaryColor(this.primaryColor)
|
||||
},
|
||||
setAppLang(lang: App.lang) {
|
||||
setLocale(lang)
|
||||
local.set('lang', lang)
|
||||
this.lang = lang
|
||||
},
|
||||
/* 设置主题色 */
|
||||
setPrimaryColor(color: string) {
|
||||
docEle.value.style.setProperty('--primary-color', color)
|
||||
const brightenColor = chroma(color).brighten(1).hex()
|
||||
const darkenColor = chroma(color).darken(1).hex()
|
||||
const brightenColor = colord(color).lighten(0.1).toHex()
|
||||
const darkenColor = colord(color).darken(0.05).toHex()
|
||||
set(this.theme, 'common.primaryColor', color)
|
||||
set(this.theme, 'common.primaryColorHover', brightenColor)
|
||||
set(this.theme, 'common.primaryColorPressed', darkenColor)
|
||||
|
@ -12,7 +12,7 @@ export const useAuthStore = defineStore('auth-store', {
|
||||
state: (): AuthStatus => {
|
||||
return {
|
||||
userInfo: local.get('userInfo'),
|
||||
token: local.get('token') || '',
|
||||
token: local.get('accessToken') || '',
|
||||
}
|
||||
},
|
||||
getters: {
|
||||
@ -46,15 +46,15 @@ export const useAuthStore = defineStore('auth-store', {
|
||||
}
|
||||
},
|
||||
clearAuthStorage() {
|
||||
local.remove('token')
|
||||
local.remove('accessToken')
|
||||
local.remove('refreshToken')
|
||||
local.remove('userInfo')
|
||||
},
|
||||
|
||||
/* 用户登录 */
|
||||
async login(username: string, password: string) {
|
||||
const { error, data } = await fetchLogin({ username, password })
|
||||
if (error)
|
||||
async login(userName: string, password: string) {
|
||||
const { isSuccess, data } = await fetchLogin({ userName, password })
|
||||
if (!isSuccess)
|
||||
return
|
||||
|
||||
// 处理登录信息
|
||||
@ -65,7 +65,7 @@ export const useAuthStore = defineStore('auth-store', {
|
||||
async handleAfterLogin(data: ApiAuth.loginInfo) {
|
||||
// 将token和userInfo保存下来
|
||||
local.set('userInfo', data)
|
||||
local.set('token', data.accessToken)
|
||||
local.set('accessToken', data.accessToken)
|
||||
local.set('refreshToken', data.refreshToken)
|
||||
this.token = data.accessToken
|
||||
this.userInfo = data
|
||||
|
@ -3,12 +3,12 @@ import { RouterLink } from 'vue-router'
|
||||
import { h } from 'vue'
|
||||
import { clone, construct, min } from 'radash'
|
||||
import type { RouteRecordRaw } from 'vue-router'
|
||||
import { arrayToTree, local, renderIcon } from '@/utils'
|
||||
import { $t, arrayToTree, local, renderIcon } from '@/utils'
|
||||
import { router } from '@/router'
|
||||
import { fetchUserRoutes } from '@/service'
|
||||
import { staticRoutes } from '@/router/routes.static'
|
||||
import { usePermission } from '@/hooks'
|
||||
import { BasicLayout } from '@/layouts/index'
|
||||
import Layout from '@/layouts/index.vue'
|
||||
import { useAuthStore } from '@/store/auth'
|
||||
|
||||
interface RoutesStatus {
|
||||
@ -34,29 +34,29 @@ export const useRouteStore = defineStore('route-store', {
|
||||
this.$reset()
|
||||
},
|
||||
resetRoutes() {
|
||||
/* 删除后面添加的路由 */
|
||||
router.removeRoute('appRoot')
|
||||
},
|
||||
/* 设置当前高亮的菜单key */
|
||||
// set the currently highlighted menu key
|
||||
setActiveMenu(key: string) {
|
||||
this.activeMenu = key
|
||||
},
|
||||
/* 生成侧边菜单的数据 */
|
||||
createMenus(userRoutes: AppRoute.RowRoute[]) {
|
||||
const resultMenus = clone(userRoutes).map(i => construct(i)) as AppRoute.Route[]
|
||||
// arrayToTree2()
|
||||
/** 过滤不需要显示的菜单 */
|
||||
|
||||
// filter menus that do not need to be displayed
|
||||
const visibleMenus = resultMenus.filter(route => !route.meta.hide)
|
||||
// 生成侧边菜单
|
||||
|
||||
// generate side menu
|
||||
this.menus = arrayToTree(this.transformAuthRoutesToMenus(visibleMenus))
|
||||
},
|
||||
|
||||
//* 将返回的路由表渲染成侧边栏 */
|
||||
// render the returned routing table as a sidebar
|
||||
transformAuthRoutesToMenus(userRoutes: AppRoute.Route[]): MenuOption[] {
|
||||
const { hasPermission } = usePermission()
|
||||
/** 过滤没有权限的侧边菜单 */
|
||||
// Filter out side menus without permission
|
||||
return userRoutes.filter(i => hasPermission(i.meta.roles))
|
||||
/** 根据order大小菜单排序 */
|
||||
// Sort the menu according to the order size
|
||||
.sort((a, b) => {
|
||||
if (a.meta && a.meta.order && b.meta && b.meta.order)
|
||||
return a.meta.order - b.meta.order
|
||||
@ -66,13 +66,14 @@ export const useRouteStore = defineStore('route-store', {
|
||||
return 1
|
||||
else return 0
|
||||
})
|
||||
/** 转换为侧边菜单数据结构 */
|
||||
|
||||
// Convert to side menu data structure
|
||||
.map((item) => {
|
||||
const target: MenuOption = {
|
||||
id: item.id,
|
||||
pid: item.pid,
|
||||
label:
|
||||
(!item.children || item.children.length === 0)
|
||||
(!item.meta.menuType || item.meta.menuType === 'page')
|
||||
? () =>
|
||||
h(
|
||||
RouterLink,
|
||||
@ -81,30 +82,75 @@ export const useRouteStore = defineStore('route-store', {
|
||||
path: item.path,
|
||||
},
|
||||
},
|
||||
{ default: () => item.meta.title },
|
||||
{ default: () => $t(`route.${String(item.name)}`, item.meta.title) },
|
||||
)
|
||||
: item.meta.title,
|
||||
: () => $t(`route.${String(item.name)}`, item.meta.title),
|
||||
key: item.path,
|
||||
icon: item.meta.icon ? renderIcon(item.meta.icon) : undefined,
|
||||
}
|
||||
return target
|
||||
})
|
||||
},
|
||||
createRoutes(routes: AppRoute.RowRoute[]) {
|
||||
const { hasPermission } = usePermission()
|
||||
|
||||
// Structure the meta field
|
||||
let resultRouter = clone(routes).map(i => construct(i)) as AppRoute.Route[]
|
||||
|
||||
// Route permission filtering
|
||||
resultRouter = resultRouter.filter(i => hasPermission(i.meta.roles))
|
||||
|
||||
// Generate an array of route names that need to be kept alive
|
||||
this.cacheRoutes = resultRouter.filter((i) => {
|
||||
return i.meta.keepAlive
|
||||
})
|
||||
.map(i => i.name)
|
||||
|
||||
// Generate routes, no need to import files for those with redirect
|
||||
const modules = import.meta.glob('@/views/**/*.vue')
|
||||
resultRouter = resultRouter.map((item: AppRoute.Route) => {
|
||||
if (item.componentPath && !item.redirect)
|
||||
item.component = modules[`/src/views${item.componentPath}`]
|
||||
return item
|
||||
})
|
||||
|
||||
// Generate route tree
|
||||
resultRouter = arrayToTree(resultRouter) as AppRoute.Route[]
|
||||
|
||||
const appRootRoute: RouteRecordRaw = {
|
||||
path: '/appRoot',
|
||||
name: 'appRoot',
|
||||
redirect: import.meta.env.VITE_HOME_PATH,
|
||||
component: Layout,
|
||||
meta: {
|
||||
title: '',
|
||||
icon: 'icon-park-outline:home',
|
||||
},
|
||||
children: [],
|
||||
}
|
||||
|
||||
// Set the correct redirect path for the route
|
||||
this.setRedirect(resultRouter)
|
||||
|
||||
// Insert the processed route into the root route
|
||||
appRootRoute.children = resultRouter as unknown as RouteRecordRaw[]
|
||||
router.addRoute(appRootRoute)
|
||||
},
|
||||
setRedirect(routes: AppRoute.Route[]) {
|
||||
routes.forEach((route) => {
|
||||
if (route.children) {
|
||||
if (!route.redirect) {
|
||||
// 过滤出没有隐藏的子元素集
|
||||
// Filter out a collection of child elements that are not hidden
|
||||
const visibleChilds = route.children.filter(child => !child.meta.hide)
|
||||
|
||||
// 过滤出含有order属性的页面
|
||||
// Redirect page to the path of the first child element by default
|
||||
let target = visibleChilds[0]
|
||||
|
||||
// Filter out pages with the order attribute
|
||||
const orderChilds = visibleChilds.filter(child => child.meta.order)
|
||||
|
||||
// 重定向页默认第一个子元素的路径
|
||||
let target = route.children[0]
|
||||
if (orderChilds.length > 0)
|
||||
// 有order则取最小者重定向
|
||||
target = min(orderChilds, i => i.meta.order as number) as AppRoute.Route
|
||||
target = min(orderChilds, i => i.meta.order!) as AppRoute.Route
|
||||
|
||||
route.redirect = target.path
|
||||
}
|
||||
@ -113,48 +159,8 @@ export const useRouteStore = defineStore('route-store', {
|
||||
}
|
||||
})
|
||||
},
|
||||
createRoutes(routes: AppRoute.RowRoute[]) {
|
||||
const { hasPermission } = usePermission()
|
||||
// 结构化meta字段
|
||||
let resultRouter = clone(routes).map(i => construct(i)) as AppRoute.Route[]
|
||||
// 路由权限过滤
|
||||
resultRouter = resultRouter.filter(i => hasPermission(i.meta.roles))
|
||||
|
||||
// 生成需要keepAlive的路由name数组
|
||||
this.cacheRoutes = resultRouter.filter((i) => {
|
||||
return i.meta.keepAlive
|
||||
})
|
||||
.map(i => i.name)
|
||||
|
||||
// 生成路由,有redirect的不需要引入文件
|
||||
const modules = import.meta.glob('@/views/**/*.vue')
|
||||
resultRouter = resultRouter.map((item: any) => {
|
||||
if (item.componentPath && !item.redirect)
|
||||
item.component = modules[`/src/views${item.componentPath}`]
|
||||
return item
|
||||
})
|
||||
|
||||
resultRouter = arrayToTree(resultRouter) as AppRoute.Route[]
|
||||
this.setRedirect(resultRouter)
|
||||
const appRootRoute: RouteRecordRaw = {
|
||||
path: '/appRoot',
|
||||
name: 'appRoot',
|
||||
redirect: import.meta.env.VITE_HOME_PATH,
|
||||
component: BasicLayout,
|
||||
meta: {
|
||||
title: '首页',
|
||||
icon: 'icon-park-outline:home',
|
||||
},
|
||||
children: [],
|
||||
}
|
||||
// 根据角色过滤后的插入根路由中
|
||||
appRootRoute.children = resultRouter as unknown as RouteRecordRaw[]
|
||||
// 插入路由表
|
||||
router.addRoute(appRootRoute)
|
||||
},
|
||||
async initRouteInfo() {
|
||||
if (import.meta.env.VITE_AUTH_ROUTE_MODE === 'dynamic') {
|
||||
// 根据用户id来获取用户的路由
|
||||
const userInfo = local.get('userInfo')
|
||||
|
||||
if (!userInfo || !userInfo.id) {
|
||||
@ -163,6 +169,7 @@ export const useRouteStore = defineStore('route-store', {
|
||||
return
|
||||
}
|
||||
|
||||
// Get user's route
|
||||
const { data } = await fetchUserRoutes({
|
||||
id: userInfo.id,
|
||||
})
|
||||
@ -179,12 +186,19 @@ export const useRouteStore = defineStore('route-store', {
|
||||
},
|
||||
async initAuthRoute() {
|
||||
this.isInitAuthRoute = false
|
||||
// 初始化路由信息
|
||||
|
||||
// Initialize route information
|
||||
const rowRoutes = await this.initRouteInfo()
|
||||
if (!rowRoutes) {
|
||||
window.$message.error($t(`app.getRouteError`))
|
||||
return
|
||||
}
|
||||
this.rowRoutes = rowRoutes
|
||||
// 生成真实路由并插入
|
||||
|
||||
// Generate actual route and insert
|
||||
this.createRoutes(rowRoutes)
|
||||
// 生成侧边菜单
|
||||
|
||||
// Generate side menu
|
||||
this.createMenus(rowRoutes)
|
||||
|
||||
this.isInitAuthRoute = true
|
||||
|
@ -100,7 +100,7 @@ export const useTabStore = defineStore('tab-store', {
|
||||
enabled: true,
|
||||
strategies: [
|
||||
{
|
||||
storage: localStorage,
|
||||
storage: sessionStorage,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
@ -1,6 +1,5 @@
|
||||
@import './reset.css';
|
||||
@import './transition.css';
|
||||
@import './nprogress.css';
|
||||
|
||||
html,
|
||||
body,
|
||||
|
@ -1,84 +0,0 @@
|
||||
/* Make clicks pass-through */
|
||||
#nprogress {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
#nprogress .bar {
|
||||
background: var(--primary-color);
|
||||
|
||||
position: fixed;
|
||||
z-index: 1031;
|
||||
top: 0;
|
||||
left: 0;
|
||||
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
}
|
||||
|
||||
/* Fancy blur effect */
|
||||
#nprogress .peg {
|
||||
display: block;
|
||||
position: absolute;
|
||||
right: 0px;
|
||||
width: 100px;
|
||||
height: 100%;
|
||||
box-shadow: 0 0 10px var(--primary-color), 0 0 5px var(--primary-color);
|
||||
opacity: 1.0;
|
||||
|
||||
-webkit-transform: rotate(3deg) translate(0px, -4px);
|
||||
-ms-transform: rotate(3deg) translate(0px, -4px);
|
||||
transform: rotate(3deg) translate(0px, -4px);
|
||||
}
|
||||
|
||||
/* Remove these to get rid of the spinner */
|
||||
#nprogress .spinner {
|
||||
display: block;
|
||||
position: fixed;
|
||||
z-index: 1031;
|
||||
top: 15px;
|
||||
right: 15px;
|
||||
}
|
||||
|
||||
#nprogress .spinner-icon {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
box-sizing: border-box;
|
||||
|
||||
border: solid 2px transparent;
|
||||
border-top-color: var(--primary-color);
|
||||
border-left-color: var(--primary-color);
|
||||
border-radius: 50%;
|
||||
|
||||
-webkit-animation: nprogress-spinner 400ms linear infinite;
|
||||
animation: nprogress-spinner 400ms linear infinite;
|
||||
}
|
||||
|
||||
.nprogress-custom-parent {
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.nprogress-custom-parent #nprogress .spinner,
|
||||
.nprogress-custom-parent #nprogress .bar {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
@-webkit-keyframes nprogress-spinner {
|
||||
0% {
|
||||
-webkit-transform: rotate(0deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
-webkit-transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes nprogress-spinner {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
6
src/typings/api.d.ts
vendored
6
src/typings/api.d.ts
vendored
@ -7,18 +7,20 @@ declare namespace ApiAuth {
|
||||
/** 用户id */
|
||||
id: number
|
||||
/** 用户名 */
|
||||
username: string
|
||||
userName: string
|
||||
/* 用户头像 */
|
||||
avatar?: string
|
||||
/* 用户邮箱 */
|
||||
email?: string
|
||||
/* 用户昵称 */
|
||||
nickname?: string
|
||||
notes?: string
|
||||
/* 用户电话 */
|
||||
tel?: string
|
||||
/** 用户角色类型 */
|
||||
role: Auth.RoleType
|
||||
/** 访问toekn */
|
||||
accessToken: string
|
||||
/** 刷新toekn */
|
||||
refreshToken: string
|
||||
}
|
||||
}
|
||||
|
2
src/typings/env.d.ts
vendored
2
src/typings/env.d.ts
vendored
@ -14,7 +14,7 @@ interface ImportMetaEnv {
|
||||
/** 开启请求代理 */
|
||||
readonly VITE_HTTP_PROXY?: 'Y' | 'N'
|
||||
/** 是否开启打包压缩 */
|
||||
readonly VITE_COMPRESS_OPEN?: 'Y' | 'N'
|
||||
readonly VITE_BUILD_COMPRESS?: 'Y' | 'N'
|
||||
/** 压缩算法类型 */
|
||||
readonly VITE_COMPRESS_TYPE?:
|
||||
| 'gzip'
|
||||
|
16
src/typings/global.d.ts
vendored
16
src/typings/global.d.ts
vendored
@ -3,7 +3,6 @@ interface Window {
|
||||
$dialog: import('naive-ui').DialogApi
|
||||
$message: import('naive-ui').MessageApi
|
||||
$notification: import('naive-ui').NotificationApi
|
||||
$NProgress: import('NProgress').NProgress
|
||||
}
|
||||
|
||||
declare const AMap: any
|
||||
@ -26,10 +25,19 @@ declare namespace Storage {
|
||||
}
|
||||
|
||||
interface Local {
|
||||
/* 存储用户信息 */
|
||||
userInfo: ApiAuth.loginInfo
|
||||
token: string
|
||||
/* 存储访问token */
|
||||
accessToken: string
|
||||
/* 存储刷新token */
|
||||
refreshToken: string
|
||||
tabsRoutes: string
|
||||
login_account: any
|
||||
/* 存储登录账号 */
|
||||
loginAccount: any
|
||||
/* 存储当前语言 */
|
||||
lang: App.lang
|
||||
}
|
||||
}
|
||||
|
||||
declare namespace App {
|
||||
type lang = 'zhCN' | 'enUS'
|
||||
}
|
||||
|
6
src/typings/package.d.ts
vendored
6
src/typings/package.d.ts
vendored
@ -1,6 +0,0 @@
|
||||
declare module '~icons/*' {
|
||||
import type { FunctionalComponent, SVGAttributes } from 'vue'
|
||||
|
||||
const component: FunctionalComponent<SVGAttributes>
|
||||
export default component
|
||||
}
|
20
src/typings/route.d.ts
vendored
20
src/typings/route.d.ts
vendored
@ -1,7 +1,9 @@
|
||||
declare namespace AppRoute {
|
||||
/** 路由描述 */
|
||||
|
||||
type MenuType = 'dir' | 'page'
|
||||
/** 单个路由所携带的meta标识 */
|
||||
interface RouteMeta {
|
||||
/* 页面标题,通常必选。 */
|
||||
/* 页面标题,通常必选。 */
|
||||
title: string
|
||||
/* 图标,一般配合菜单使用 */
|
||||
icon?: string
|
||||
@ -17,14 +19,16 @@ declare namespace AppRoute {
|
||||
order?: number
|
||||
/* 嵌套外链 */
|
||||
herf?: string
|
||||
/** 当前路由需要选中的菜单项(用于跳转至不在左侧菜单显示的路由且需要高亮某个菜单的情况) */
|
||||
/** 当前路由不在左侧菜单显示,但需要高亮某个菜单的情况 */
|
||||
activeMenu?: string
|
||||
/** 当前路由是否会被添加到Tab中 */
|
||||
withoutTab?: boolean
|
||||
/** 当前路由是否会被固定在Tab中,用于一些常驻页面 */
|
||||
pinTab?: boolean
|
||||
/** 当前路由在左侧菜单是目录还是页面,不设置默认为page */
|
||||
menuType?: MenuType
|
||||
}
|
||||
/** 单个路由的类型结构(动态路由模式:后端返回此类型结构的路由) */
|
||||
|
||||
interface baseRoute {
|
||||
/** 路由名称(路由唯一标识) */
|
||||
name: string
|
||||
@ -34,16 +38,20 @@ declare namespace AppRoute {
|
||||
redirect?: string
|
||||
/* 页面组件地址 */
|
||||
componentPath?: string | null
|
||||
// 路由id
|
||||
/* 路由id */
|
||||
id: numnber
|
||||
// 父级路由id,顶级页面为null
|
||||
/* 父级路由id,顶级页面为null */
|
||||
pid: number | null
|
||||
}
|
||||
|
||||
/** 单个路由的类型结构(动态路由模式:后端返回此类型结构的路由) */
|
||||
type RowRoute = {
|
||||
[K in keyof RouteMeta as `meta.${K}`]?: RouteMeta[K]
|
||||
} & baseRoute
|
||||
|
||||
/**
|
||||
* 挂载到项目上的真实路由结构
|
||||
*/
|
||||
interface Route extends baseRoute {
|
||||
/** 子路由 */
|
||||
children?: Route[]
|
||||
|
17
src/typings/service.d.ts
vendored
17
src/typings/service.d.ts
vendored
@ -20,7 +20,7 @@ declare namespace Service {
|
||||
successCode?: number | string
|
||||
}
|
||||
|
||||
type RequestErrorType = 'Response Error' | 'Business Error'
|
||||
type RequestErrorType = 'Response Error' | 'Business Error' | null
|
||||
type RequestCode = string | number
|
||||
|
||||
interface RequestError {
|
||||
@ -29,8 +29,21 @@ declare namespace Service {
|
||||
/** 错误码 */
|
||||
code: RequestCode
|
||||
/** 错误信息 */
|
||||
msg: string
|
||||
message: string
|
||||
/** 返回的数据 */
|
||||
data?: any
|
||||
}
|
||||
|
||||
interface ResponseResult<T> extends RequestError {
|
||||
/** 请求服务是否成功 */
|
||||
isSuccess: boolean
|
||||
/** 请求服务的错误类型 */
|
||||
errorType: RequestErrorType
|
||||
/** 错误码 */
|
||||
code: RequestCode
|
||||
/** 错误信息 */
|
||||
message: string
|
||||
/** 返回的数据 */
|
||||
data: T
|
||||
}
|
||||
}
|
||||
|
20
src/utils/i18n.ts
Normal file
20
src/utils/i18n.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import type { NDateLocale, NLocale } from 'naive-ui'
|
||||
import { dateZhCN, zhCN } from 'naive-ui'
|
||||
import { i18n } from '@/modules/i18n'
|
||||
|
||||
export function setLocale(locale: App.lang) {
|
||||
i18n.global.locale.value = locale
|
||||
}
|
||||
|
||||
export const $t = i18n.global.t
|
||||
|
||||
export const naiveI18nOptions: Record<App.lang, { locale: NLocale | null, dateLocale: NDateLocale | null }> = {
|
||||
zhCN: {
|
||||
locale: zhCN,
|
||||
dateLocale: dateZhCN,
|
||||
},
|
||||
enUS: {
|
||||
locale: null,
|
||||
dateLocale: null,
|
||||
},
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
import { Icon } from '@iconify/vue'
|
||||
import { NIcon } from 'naive-ui'
|
||||
|
||||
export function renderIcon(icon: string) {
|
||||
return () => h(NIcon, null, { default: () => h(Icon, { icon }) })
|
||||
export function renderIcon(icon: string, props?: import('naive-ui').IconProps) {
|
||||
return () => h(NIcon, props, { default: () => h(Icon, { icon }) })
|
||||
}
|
||||
|
@ -1,3 +1,4 @@
|
||||
export * from './icon'
|
||||
export * from './storage'
|
||||
export * from './array'
|
||||
export * from './i18n'
|
||||
|
@ -1,4 +1,4 @@
|
||||
const STORAGE_PREFIX = 'nova_'
|
||||
const STORAGE_PREFIX = import.meta.env.VITE_STORAGE_PREFIX
|
||||
|
||||
interface StorageData<T> {
|
||||
value: T
|
||||
|
128
src/views/dashboard/monitor/components/chart.vue
Normal file
128
src/views/dashboard/monitor/components/chart.vue
Normal file
@ -0,0 +1,128 @@
|
||||
<script setup lang="ts">
|
||||
import { graphic } from 'echarts'
|
||||
import { type ECOption, useEcharts } from '@/hooks'
|
||||
|
||||
const lineOptions = ref<ECOption>({
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
},
|
||||
grid: {
|
||||
left: '2%',
|
||||
right: '2%',
|
||||
bottom: '0%',
|
||||
top: '0%',
|
||||
containLabel: true,
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
boundaryGap: false,
|
||||
data: ['10:00', '10:10', '10:10', '10:30', '10:40', '10:50'],
|
||||
axisTick: {
|
||||
show: false,
|
||||
},
|
||||
axisLine: {
|
||||
show: false,
|
||||
},
|
||||
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
splitLine: {
|
||||
show: false,
|
||||
},
|
||||
axisTick: {
|
||||
show: false,
|
||||
},
|
||||
axisLabel: {
|
||||
show: false,
|
||||
},
|
||||
},
|
||||
series: [{
|
||||
name: '2',
|
||||
type: 'line',
|
||||
z: 3,
|
||||
showSymbol: false,
|
||||
smoothMonotone: 'x',
|
||||
lineStyle: {
|
||||
width: 3,
|
||||
color: {
|
||||
type: 'linear',
|
||||
x: 0,
|
||||
y: 0,
|
||||
x2: 0,
|
||||
y2: 1,
|
||||
colorStops: [{
|
||||
offset: 0,
|
||||
color: 'rgba(59,102,246)', // 0% 处的颜色
|
||||
}, {
|
||||
offset: 1,
|
||||
color: 'rgba(118,237,252)', // 100% 处的颜色
|
||||
}],
|
||||
},
|
||||
shadowBlur: 4,
|
||||
shadowColor: 'rgba(69,126,247,.2)',
|
||||
shadowOffsetY: 4,
|
||||
},
|
||||
areaStyle: {
|
||||
color: {
|
||||
type: 'linear',
|
||||
x: 0,
|
||||
y: 0,
|
||||
x2: 0,
|
||||
y2: 1,
|
||||
colorStops: [{
|
||||
offset: 0,
|
||||
color: 'rgba(227,233,250,.9)', // 0% 处的颜色
|
||||
}, {
|
||||
offset: 1,
|
||||
color: 'rgba(248,251,252,.3)', // 100% 处的颜色
|
||||
}],
|
||||
},
|
||||
},
|
||||
smooth: true,
|
||||
data: [20, 56, 17, 40, 68, 42],
|
||||
}, {
|
||||
name: '1',
|
||||
type: 'line',
|
||||
showSymbol: false,
|
||||
smoothMonotone: 'x',
|
||||
|
||||
lineStyle: {
|
||||
width: 3,
|
||||
color: new graphic.LinearGradient(0, 0, 0, 1, [{
|
||||
offset: 0,
|
||||
color: 'rgba(255,84,108)',
|
||||
}, {
|
||||
offset: 1,
|
||||
color: 'rgba(252,140,118)',
|
||||
}], false),
|
||||
shadowBlur: 4,
|
||||
shadowColor: 'rgba(253,121,128,.2)',
|
||||
shadowOffsetY: 4,
|
||||
},
|
||||
areaStyle: {
|
||||
color: new graphic.LinearGradient(0, 0, 0, 1, [{
|
||||
offset: 0,
|
||||
color: 'rgba(255,84,108,.15)',
|
||||
}, {
|
||||
offset: 1,
|
||||
color: 'rgba(252,140,118,0)',
|
||||
}], false),
|
||||
},
|
||||
smooth: true,
|
||||
data: [20, 71, 8, 50, 57, 32],
|
||||
}],
|
||||
}) as Ref<ECOption>
|
||||
const { domRef: lineRef } = useEcharts(lineOptions)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
ref="lineRef"
|
||||
class="h-400px"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
105
src/views/dashboard/monitor/components/chart2.vue
Normal file
105
src/views/dashboard/monitor/components/chart2.vue
Normal file
@ -0,0 +1,105 @@
|
||||
<script setup lang="ts">
|
||||
import { graphic } from 'echarts'
|
||||
import { type ECOption, useEcharts } from '@/hooks'
|
||||
|
||||
const chartData = [
|
||||
{ name: '1', value: 300 },
|
||||
{ name: '2', value: 400 },
|
||||
{ name: '3', value: 452 },
|
||||
{ name: '4', value: 770 },
|
||||
{ name: '5', value: 650 },
|
||||
{ name: '6', value: 256 },
|
||||
{ name: '7', value: 350 },
|
||||
{ name: '8', value: 422 },
|
||||
{ name: '9', value: 235 },
|
||||
{ name: '10', value: 658 },
|
||||
{ name: '11', value: 600 },
|
||||
{ name: '12', value: 400 },
|
||||
{ name: '13', value: 523 },
|
||||
{ name: '14', value: 482 },
|
||||
{ name: '15', value: 423 },
|
||||
]
|
||||
|
||||
const xData = chartData.map(v => v.name)
|
||||
const sData = chartData.map(v => v.value)
|
||||
|
||||
const option = ref<ECOption>({
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
},
|
||||
grid: {
|
||||
left: '2%',
|
||||
right: '2%',
|
||||
bottom: '0%',
|
||||
top: '0%',
|
||||
containLabel: true,
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
boundaryGap: false,
|
||||
data: xData,
|
||||
axisTick: {
|
||||
show: false,
|
||||
},
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
color: 'rgba(151,151,151,0.5)',
|
||||
type: 'dashed',
|
||||
},
|
||||
},
|
||||
axisLabel: {
|
||||
margin: 10,
|
||||
color: '#666',
|
||||
fontSize: 14,
|
||||
},
|
||||
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
splitLine: {
|
||||
lineStyle: {
|
||||
color: 'rgba(151,151,151,0.5)',
|
||||
type: 'dashed',
|
||||
},
|
||||
},
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
color: 'rgba(151,151,151,0.5)',
|
||||
type: 'dashed',
|
||||
},
|
||||
},
|
||||
axisTick: {
|
||||
show: false,
|
||||
},
|
||||
axisLabel: {
|
||||
show: false,
|
||||
},
|
||||
},
|
||||
series: [{
|
||||
type: 'bar',
|
||||
barWidth: '20px',
|
||||
data: sData,
|
||||
itemStyle: {
|
||||
color: new graphic.LinearGradient(0, 0, 0, 1, [{
|
||||
offset: 0,
|
||||
color: '#00BD89', // 0% 处的颜色
|
||||
}, {
|
||||
offset: 1,
|
||||
color: '#C9F9E1', // 100% 处的颜色
|
||||
}], false),
|
||||
},
|
||||
}],
|
||||
}) as Ref<ECOption>
|
||||
const { domRef: lineRef } = useEcharts(option)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
ref="lineRef"
|
||||
class="h-400px"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
60
src/views/dashboard/monitor/components/chart3.vue
Normal file
60
src/views/dashboard/monitor/components/chart3.vue
Normal file
@ -0,0 +1,60 @@
|
||||
<script setup lang="ts">
|
||||
import { type ECOption, useEcharts } from '@/hooks'
|
||||
|
||||
const option = ref<ECOption>({
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
formatter: '{b} : {d}%',
|
||||
},
|
||||
legend: {
|
||||
orient: 'horizontal',
|
||||
top: 30,
|
||||
padding: 5,
|
||||
itemWidth: 20,
|
||||
itemHeight: 12,
|
||||
textStyle: {
|
||||
color: '#777',
|
||||
},
|
||||
},
|
||||
series: [{
|
||||
type: 'pie',
|
||||
radius: ['45%', '60%'],
|
||||
center: ['50%', '50%'],
|
||||
label: {
|
||||
show: true,
|
||||
formatter: '{b} : {d}%',
|
||||
color: '#777',
|
||||
},
|
||||
labelLine: {
|
||||
show: true,
|
||||
length2: 10,
|
||||
},
|
||||
data: [
|
||||
{
|
||||
value: 335,
|
||||
name: '直接访问',
|
||||
},
|
||||
{
|
||||
value: 77,
|
||||
name: 'Bilibili',
|
||||
},
|
||||
{
|
||||
value: 82,
|
||||
name: '知乎',
|
||||
},
|
||||
{
|
||||
value: 421,
|
||||
name: '小红书',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}) as Ref<ECOption>
|
||||
const { domRef: lineRef } = useEcharts(option)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div ref="lineRef" class="h-400px" />
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
@ -1,4 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import Chart from './components/chart.vue'
|
||||
import Chart2 from './components/chart2.vue'
|
||||
import Chart3 from './components/chart3.vue'
|
||||
|
||||
const tableData = [
|
||||
{
|
||||
id: 0,
|
||||
@ -50,7 +54,7 @@ const tableData = [
|
||||
color="#de4307"
|
||||
size="42"
|
||||
>
|
||||
<i-icon-park-outline-chart-histogram />
|
||||
<icon-park-outline-chart-histogram />
|
||||
</n-icon>
|
||||
</n-space>
|
||||
<template #footer>
|
||||
@ -82,7 +86,7 @@ const tableData = [
|
||||
color="#ffb549"
|
||||
size="42"
|
||||
>
|
||||
<i-icon-park-outline-chart-graph />
|
||||
<icon-park-outline-chart-graph />
|
||||
</n-icon>
|
||||
</n-space>
|
||||
<template #footer>
|
||||
@ -114,7 +118,7 @@ const tableData = [
|
||||
color="#1687a7"
|
||||
size="42"
|
||||
>
|
||||
<i-icon-park-outline-average />
|
||||
<icon-park-outline-average />
|
||||
</n-icon>
|
||||
</n-space>
|
||||
<template #footer>
|
||||
@ -146,7 +150,7 @@ const tableData = [
|
||||
color="#42218E"
|
||||
size="42"
|
||||
>
|
||||
<i-icon-park-outline-chart-pie />
|
||||
<icon-park-outline-chart-pie />
|
||||
</n-icon>
|
||||
</n-space>
|
||||
<template #footer>
|
||||
@ -170,10 +174,10 @@ const tableData = [
|
||||
pane-style="padding: 20px;"
|
||||
>
|
||||
<n-tab-pane name="流量趋势">
|
||||
流量趋势
|
||||
<Chart />
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="访问量趋势">
|
||||
访问量趋势
|
||||
<Chart2 />
|
||||
</n-tab-pane>
|
||||
</n-tabs>
|
||||
</n-card>
|
||||
@ -185,7 +189,7 @@ const tableData = [
|
||||
content: true,
|
||||
}"
|
||||
>
|
||||
1
|
||||
<Chart3 />
|
||||
</n-card>
|
||||
</n-gi>
|
||||
<n-gi :span="16">
|
||||
|
@ -29,7 +29,7 @@ const { userInfo } = useAuthStore()
|
||||
<template #avatar>
|
||||
<n-el>
|
||||
<n-icon-wrapper :size="46" color="var(--success-color)" :border-radius="999">
|
||||
<e-icon :size="26" icon="icon-park-outline:user" />
|
||||
<nova-icon :size="26" icon="icon-park-outline:user" />
|
||||
</n-icon-wrapper>
|
||||
</n-el>
|
||||
</template>
|
||||
@ -47,7 +47,7 @@ const { userInfo } = useAuthStore()
|
||||
<template #avatar>
|
||||
<n-el>
|
||||
<n-icon-wrapper :size="46" color="var(--success-color)" :border-radius="999">
|
||||
<e-icon :size="26" icon="icon-park-outline:every-user" />
|
||||
<nova-icon :size="26" icon="icon-park-outline:every-user" />
|
||||
</n-icon-wrapper>
|
||||
</n-el>
|
||||
</template>
|
||||
@ -65,7 +65,7 @@ const { userInfo } = useAuthStore()
|
||||
<template #avatar>
|
||||
<n-el>
|
||||
<n-icon-wrapper :size="46" color="var(--success-color)" :border-radius="999">
|
||||
<e-icon :size="26" icon="icon-park-outline:preview-open" />
|
||||
<nova-icon :size="26" icon="icon-park-outline:preview-open" />
|
||||
</n-icon-wrapper>
|
||||
</n-el>
|
||||
</template>
|
||||
@ -83,7 +83,7 @@ const { userInfo } = useAuthStore()
|
||||
<template #avatar>
|
||||
<n-el>
|
||||
<n-icon-wrapper :size="46" color="var(--success-color)" :border-radius="999">
|
||||
<e-icon :size="26" icon="icon-park-outline:star" />
|
||||
<nova-icon :size="26" icon="icon-park-outline:star" />
|
||||
</n-icon-wrapper>
|
||||
</n-el>
|
||||
</template>
|
||||
@ -237,7 +237,7 @@ const { userInfo } = useAuthStore()
|
||||
订单数
|
||||
</n-text>
|
||||
<n-icon-wrapper :size="46" :border-radius="999">
|
||||
<e-icon :size="26" icon="icon-park-outline:all-application" />
|
||||
<nova-icon :size="26" icon="icon-park-outline:all-application" />
|
||||
</n-icon-wrapper>
|
||||
<n-text strong class="text-2xl">
|
||||
1,234,123
|
||||
@ -253,7 +253,7 @@ const { userInfo } = useAuthStore()
|
||||
</n-text>
|
||||
<n-el>
|
||||
<n-icon-wrapper :size="46" color="var(--warning-color)" :border-radius="999">
|
||||
<e-icon :size="26" icon="icon-park-outline:list-bottom" />
|
||||
<nova-icon :size="26" icon="icon-park-outline:list-bottom" />
|
||||
</n-icon-wrapper>
|
||||
</n-el>
|
||||
<n-text strong class="text-2xl">
|
||||
|
26
src/views/demo/fetch/components/Delete.vue
Normal file
26
src/views/demo/fetch/components/Delete.vue
Normal file
@ -0,0 +1,26 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
fetchDelete,
|
||||
} from '@/service'
|
||||
|
||||
const emit = defineEmits<{
|
||||
update: [data: any] // 具名元组语法
|
||||
}>()
|
||||
|
||||
async function handleDelete() {
|
||||
const res = await fetchDelete()
|
||||
emit('update', res)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-card title="Delete" size="small">
|
||||
<n-button @click="handleDelete">
|
||||
click
|
||||
</n-button>
|
||||
</n-card>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
40
src/views/demo/fetch/components/DownLoad.vue
Normal file
40
src/views/demo/fetch/components/DownLoad.vue
Normal file
@ -0,0 +1,40 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
getBlob,
|
||||
} from '@/service'
|
||||
|
||||
const emit = defineEmits<{
|
||||
update: [data: any]
|
||||
}>()
|
||||
|
||||
const filePath = ref('https://images.unsplash.com/photo-1663529628961-80aa6ebcd157?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=764&q=80')
|
||||
|
||||
async function getBlobFile() {
|
||||
const res = await getBlob(filePath.value)
|
||||
emit('update', 'fileOk')
|
||||
downloadLink(res, 'fileOk')
|
||||
}
|
||||
|
||||
function downloadLink(data: Blob, name: string) {
|
||||
const link = URL.createObjectURL(data)
|
||||
const eleLink = document.createElement('a')
|
||||
eleLink.download = name
|
||||
eleLink.style.display = 'none'
|
||||
eleLink.href = link
|
||||
document.body.appendChild(eleLink)
|
||||
eleLink.click()
|
||||
document.body.removeChild(eleLink)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-card title="下载文件" size="small">
|
||||
<n-button @click="getBlobFile">
|
||||
click
|
||||
</n-button>
|
||||
</n-card>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
63
src/views/demo/fetch/components/DownLoadWithProgress.vue
Normal file
63
src/views/demo/fetch/components/DownLoadWithProgress.vue
Normal file
@ -0,0 +1,63 @@
|
||||
<script setup lang="ts">
|
||||
import { useRequest } from 'alova'
|
||||
import {
|
||||
downloadFile,
|
||||
} from '@/service'
|
||||
|
||||
const emit = defineEmits<{
|
||||
update: [data: any]
|
||||
}>()
|
||||
|
||||
const filePath = ref('https://images.unsplash.com/photo-1663529628961-80aa6ebcd157?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=764&q=80')
|
||||
|
||||
const { downloading, abort, send, data } = useRequest(downloadFile(filePath.value), {
|
||||
// 当immediate为false时,默认不发出
|
||||
immediate: false,
|
||||
})
|
||||
|
||||
const downloadProcess = computed(() => {
|
||||
if (!downloading.value.loaded)
|
||||
return 0
|
||||
return Math.floor(downloading.value.loaded / downloading.value.total * 100)
|
||||
})
|
||||
|
||||
async function handleDownloadFile() {
|
||||
await send()
|
||||
emit('update', 'fileOk')
|
||||
downloadLink(data.value, 'fileOk')
|
||||
}
|
||||
|
||||
function downloadLink(data: Blob, name: string) {
|
||||
const link = URL.createObjectURL(data)
|
||||
const eleLink = document.createElement('a')
|
||||
eleLink.download = name
|
||||
eleLink.style.display = 'none'
|
||||
eleLink.href = link
|
||||
document.body.appendChild(eleLink)
|
||||
eleLink.click()
|
||||
document.body.removeChild(eleLink)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-card title="带进度的下载文件" size="small">
|
||||
<n-space vertical>
|
||||
<n-input v-model:value="filePath" />
|
||||
<div>文件大小:{{ downloading.total }}B</div>
|
||||
<div>已下载:{{ downloading.loaded }}B</div>
|
||||
<n-progress type="line" indicator-placement="inside" :percentage="downloadProcess" />
|
||||
<n-space>
|
||||
<n-button strong secondary @click="handleDownloadFile">
|
||||
开始下载
|
||||
</n-button>
|
||||
<n-button strong secondary type="warning" @click="abort">
|
||||
中断下载
|
||||
</n-button>
|
||||
</n-space>
|
||||
</n-space>
|
||||
</n-card>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user