Merge branch 'updata-fetch-layout' of https://github.com/JuneOY/nova-admin into updata-fetch-layout

This commit is contained in:
Xou 2024-04-26 11:04:50 +08:00
commit d5b1a47cbb
131 changed files with 2834 additions and 840 deletions

3
.env
View File

@ -9,3 +9,6 @@ VITE_AUTH_ROUTE_MODE=static
# 设置登陆后跳转地址
VITE_HOME_PATH = /dashboard/workbench
# 本地存储前缀
VITE_STORAGE_PREFIX=

View File

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

@ -0,0 +1,6 @@
# 是否开启压缩资源
VITE_BUILD_COMPRESS=N
# 压缩算法 gzip | brotliCompress | deflate | deflateRaw
VITE_COMPRESS_TYPE=gzip

1
.github/FUNDING.yml vendored Normal file
View 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
View 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
View File

@ -0,0 +1 @@
blank_issues_enabled: false

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

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

View File

@ -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
![Contributors](https://contrib.rocks/image?repo=chansee97/nova-admin)
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

View File

@ -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)
## 贡献者
![Contributors](https://contrib.rocks/image?repo=chansee97/nova-admin)
感谢他们的所做的一切贡献!
<a href="https://github.com/chansee97/nova-admin/graphs/contributors">
<img src="https://contrib.rocks/image?repo=chansee97/nova-admin" alt="contributors" />
</a>
## Star 历史

View File

@ -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
View 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
View 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": "关于"
}
}

View File

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

View File

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

View File

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

Before

Width:  |  Height:  |  Size: 902 B

After

Width:  |  Height:  |  Size: 902 B

View File

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

View File

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

View File

@ -30,7 +30,7 @@ const router = useRouter()
type="primary"
@click="router.push('/')"
>
回到首页
{{ $t('app.backHome') }}
</n-button>
</div>
</template>

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

@ -0,0 +1,9 @@
// export const genderLabels: Record<NonNullable<CommonList.GenderType>, string> = {
// 0: '女',
// 1: '男',
// }
/** Gender */
export enum Gender {
male,
female,
}

View File

@ -1,5 +0,0 @@
/** 用户性别 */
export const genderLabels: Record<NonNullable<CommonList.GenderType>, string> = {
0: '女',
1: '男',
}

View File

@ -1 +1,2 @@
export * from './business'
export * from './Regex'
export * from './User'

View File

@ -1,4 +1,4 @@
export * from './useBoolean'
export * from './useLoading'
export * from './useEcharts'
export * from './useSystem'
export * from './usePermission'

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

15
src/layouts/index.vue Normal file
View 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>

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

@ -6,7 +6,7 @@ export const routes: RouteRecordRaw[] = [
path: '/',
name: 'root',
redirect: '/appRoot',
component: () => import('@/layouts/index'),
// component: () => import('@/layouts/index'),
children: [
],
},

View File

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

View File

@ -1,5 +1,5 @@
import { alovaInstance } from '../http'
import { request } from '../http'
export function fetchUserList() {
return alovaInstance.Get('/userList')
return request.Get('/userList')
}

View File

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

View File

@ -0,0 +1,5 @@
import { request } from '../http'
export function fetchAllRoutes() {
return request.Get<Service.ResponseResult<AppRoute.RowRoute[]> >('/getUserRoutes')
}

View File

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

View File

@ -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')}`
},
})

View File

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

View File

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

View File

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

View File

@ -1,3 +1,4 @@
export * from './api/list'
export * from './api/system'
export * from './api/login'
export * from './api/list'
export * from './api/test'

View File

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

View File

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

View File

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

View File

@ -100,7 +100,7 @@ export const useTabStore = defineStore('tab-store', {
enabled: true,
strategies: [
{
storage: localStorage,
storage: sessionStorage,
},
],
},

View File

@ -1,6 +1,5 @@
@import './reset.css';
@import './transition.css';
@import './nprogress.css';
html,
body,

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +0,0 @@
declare module '~icons/*' {
import type { FunctionalComponent, SVGAttributes } from 'vue'
const component: FunctionalComponent<SVGAttributes>
export default component
}

View File

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

View File

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

View File

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

View File

@ -1,3 +1,4 @@
export * from './icon'
export * from './storage'
export * from './array'
export * from './i18n'

View File

@ -1,4 +1,4 @@
const STORAGE_PREFIX = 'nova_'
const STORAGE_PREFIX = import.meta.env.VITE_STORAGE_PREFIX
interface StorageData<T> {
value: T

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

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

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

View File

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

View File

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

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

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

View 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), {
// immediatefalse
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