1
0
mirror of https://github.com/zclzone/vue-naive-admin.git synced 2026-01-22 23:50:22 +08:00

2 Commits

Author SHA1 Message Date
qiaofugui
693c4542ef Merge 6f38034c14 into 4f637d76e6 2024-05-06 08:57:06 +08:00
qiaoronggui
6f38034c14 fix: 路由标签较多,屏幕较窄情况下没有左右滚动条 2024-04-30 09:55:23 +08:00
105 changed files with 4349 additions and 7457 deletions

View File

@@ -1,9 +0,0 @@
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
end_of_line = unset
insert_final_newline = true
trim_trailing_whitespace = true

View File

@@ -4,9 +4,6 @@ VITE_USE_HASH = 'true'
# 资源公共路径,需要以 /开头和结尾
VITE_PUBLIC_PATH = '/'
# Axios 基础路径
# VITE_AXIOS_BASE_URL = '/api' # 用于代理
VITE_AXIOS_BASE_URL = 'https://m1.apifoxmock.com/m1/3776410-3408296-default' # apifox云端mock
# 代理配置-target
VITE_PROXY_TARGET = 'http://localhost:8085'
# 代理配置-target 本地服务 | Apifox云端mock
# VITE_PROXY_TARGET = 'http://localhost:8085'
VITE_PROXY_TARGET = 'https://mock.apifox.com/m1/3776410-0-default/'

View File

@@ -4,5 +4,5 @@ VITE_USE_HASH = 'false'
# 资源公共路径,需要以 /开头和结尾
VITE_PUBLIC_PATH = '/'
# VITE_AXIOS_BASE_URL = '/api' # 用于代理
VITE_AXIOS_BASE_URL = 'https://m1.apifoxmock.com/m1/3776410-3408296-default' # apifox云端mock
# 代理配置-target
VITE_PROXY_TARGET = 'http://localhost:8085'

View File

@@ -0,0 +1,63 @@
{
"globals": {
"$loadingBar": true,
"$message": true,
"$dialog": true,
"$notification": true,
"$modal": true,
"defineOptions": true,
"EffectScope": true,
"computed": true,
"createApp": true,
"customRef": true,
"defineAsyncComponent": true,
"defineComponent": true,
"effectScope": true,
"getCurrentInstance": true,
"getCurrentScope": true,
"h": true,
"inject": true,
"isProxy": true,
"isReactive": true,
"isReadonly": true,
"isRef": true,
"markRaw": true,
"nextTick": true,
"onActivated": true,
"onBeforeMount": true,
"onBeforeUnmount": true,
"onBeforeUpdate": true,
"onDeactivated": true,
"onErrorCaptured": true,
"onMounted": true,
"onRenderTracked": true,
"onRenderTriggered": true,
"onScopeDispose": true,
"onServerPrefetch": true,
"onUnmounted": true,
"onUpdated": true,
"provide": true,
"reactive": true,
"readonly": true,
"ref": true,
"resolveComponent": true,
"shallowReactive": true,
"shallowReadonly": true,
"shallowRef": true,
"toRaw": true,
"toRef": true,
"toRefs": true,
"triggerRef": true,
"unref": true,
"useAttrs": true,
"useCssModule": true,
"useCssVars": true,
"useRoute": true,
"useRouter": true,
"useSlots": true,
"watch": true,
"watchEffect": true,
"watchPostEffect": true,
"watchSyncEffect": true
}
}

4
.eslintignore Normal file
View File

@@ -0,0 +1,4 @@
node_modules
dist
public
package.json

3
.npmrc
View File

@@ -1,3 +1,2 @@
registry=https://registry.npmmirror.com
shamefully-hoist=true
strict-peer-dependencies=false
shamefully-hoist=true

3
.prettierignore Normal file
View File

@@ -0,0 +1,3 @@
/node_modules/**
/dist/*
/public/*

7
.prettierrc.json Normal file
View File

@@ -0,0 +1,7 @@
{
"printWidth": 100,
"singleQuote": true,
"semi": false,
"endOfLine": "auto",
"htmlWhitespaceSensitivity": "ignore"
}

View File

@@ -6,7 +6,6 @@
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode",
"sdras.vue-vscode-snippets",
"mikestead.dotenv",
"tu6ge.naive-ui-intelligence"
"mikestead.dotenv"
]
}

40
.vscode/settings.json vendored
View File

@@ -1,23 +1,25 @@
{
"prettier.enable": false,
"editor.formatOnSave": false,
// Auto fix
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit",
"source.organizeImports": "never"
"files.eol": "\n",
"files.associations": {
"*.env.*": "dotenv",
"*.svg": "html",
"*.css": "scss"
},
// Enable eslint for all supported languages
"eslint.validate": [
"javascript",
"javascriptreact",
"typescript",
"typescriptreact",
"vue",
"html",
"markdown",
"json",
"jsonc"
]
"editor.formatOnSave": false,
"[html][css][less][scss][sass][yaml][yml][jsonc][json]": {
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[markdown]": {
"editor.formatOnSave": true,
"editor.defaultFormatter": "yzhang.markdown-all-in-one"
},
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
},
"eslint.validate": ["javascript", "typescript", "javascriptreact", "typescriptreact", "vue"],
"eslint.options": {
"overrideConfigFile": "package.json"
}
}

View File

@@ -5,19 +5,9 @@
</p>
<p align="center">
<a href="./LICENSE"><img alt="MIT License" src="https://badgen.net/github/license/zclzone/vue-naive-admin"/></a>
<a href="https://gitcode.com/zclzone/vue-naive-admin"><img alt="GitCode" src="https://gitcode.com/zclzone/vue-naive-admin/star/badge.svg
"/></a>
</p>
---
<a href="https://hellogithub.com/repository/54f19ba1f9ae4238b3cbd111f3c428b0" target="_blank"><img src="https://abroad.hellogithub.com/v1/widgets/recommend.svg?rid=54f19ba1f9ae4238b3cbd111f3c428b0&claim_uid=jXGayRdJZScqMNr" alt="FeaturedHelloGitHub" style="width: 250px; height: 54px;" width="250" height="54" /></a>
> [!Tip]
> 我们提供WebPC+H5+小程序)的技术支持服务及定制开发,不限前后端,详细需求可联系作者,微信: isme-admin邮箱: zclzone@outlook.com
## 简介
Vue Naive Admin 是一款极简风格的后台管理模板,包含前后端解决方案,前端使用 Vite + Vue3 + Pinia + Unocss后端使用 Nestjs + TypeOrm + MySql简单易用赏心悦目历经十几次重构和细节打磨诚意满满
## 设计理念
@@ -58,26 +48,17 @@ Vue Naive Admin 2022年2月开始开源从 1.0 到现在的 2.0,一直秉
Vue Naive Admin 提供一套后端代码,技术栈使用 Nestjs + TypeOrm + MySql内置 JWT、RABC及模板所需的一些基础接口。
*后续可能会提供 Java 版和 Go 版的,但由于精力有限,欢迎感兴趣的大佬基于前端提供对接好的后端项目,当然,并不局限于 Java 和 Go已对接的后端项目会展示到仓库的 README 和 官方文档中*
- 源码-github: [isme-nest-serve | github](https://github.com/zclzone/isme-nest-serve)
- 源码-gitee: [isme-nest-serve | gitee](https://gitee.com/isme-admin/isme-nest-serve)
- 源码-gitcode: [isme-nest-serve | gitcode](https://gitcode.com/zclzone/isme-nest-serve)
## 文档
- 项目文档: [docs | vue-naive-admin](https://isme.top)
- 项目文档: [docs | vue-naive-admin](https://docs.isme.top/web/#/624306705/188522224)
- 接口文档: [apidoc | isme-nest-serve](https://apifox.com/apidoc/shared-ff4a4d32-c0d1-4caf-b0ee-6abc130f734a)
> 注:有个比较常见的问题,就是如何添加菜单和修改菜单,由于项目是由后端控制菜单资源的,所以需要对接后端后在资源管理功能对菜单进行增删改,然后在角色管理功能给对应角色进行授权。具体如何对接后端,请参考 [项目文档](https://isme.top)。当然,可能有些菜单你不想通过权限控制,那么你可以在 `/src/settings.js` 文件添加 basePermissions只需对齐菜单资源的结构即可结构可以参照 [接口文档](https://apifox.com/apidoc/shared-ff4a4d32-c0d1-4caf-b0ee-6abc130f734a/api-134536978)。
## 使用这个模板开始你的项目
[使用这个模板创建Github仓库](https://github.com/zclzone/vue-naive-admin/generate).
或者使用 `degit` 克隆此仓库,这样将没有任何历史提交记录:
```cmd
npx degit zclzone/vue-naive-admin
```
## 版权说明
@@ -91,8 +72,4 @@ npx degit zclzone/vue-naive-admin
- [isme-java-serve](https://github.com/DHBin/isme-java-serve): 一个轻量级的Java后端服务基于SpringBoot、MybatisPlus、SaToken、MapStruct等实现已对接 Vue Naive Admin 2.0。
- [naive-admin-go](https://github.com/ituserxxx/naive-admin-go): 一个 Go 后端服务,基于 gin、gorm、mysql、jwt和session已对接 Vue Naive Admin 2.0。
- [isme-java](https://github.com/AllenDengMs/isme-java): 一个轻量且完成度高的Java后端服务基于Springboot 3 + JDK21层次结构严谨注释齐全避免过度封装代码可读性度高依赖最简化上手成本低已集成 账号管理、权限管理、API鉴权、消息国际化等功能。
## 联系作者 or 进交流群
[https://www.isme.top/contact.html](https://www.isme.top/contact.html)

View File

@@ -6,9 +6,9 @@
* Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top
**********************************/
import path from 'node:path'
import { globSync } from 'glob'
import dynamicIcons from '../src/assets/icons/dynamic-icons.js'
import path from 'path'
import dynamicIcons from '../src/assets/icons/dynamic-icons'
/**
* @usage 生成icons, 用于 unocss safelist以支持页面动态渲染自定义图标
@@ -35,5 +35,5 @@ export function getIcons() {
*/
export function getPagePathes() {
const files = globSync('src/views/**/*.vue')
return files.map(item => `/${path.normalize(item).replace(/\\/g, '/')}`)
return files.map((item) => '/' + path.normalize(item).replace(/\\/g, '/'))
}

View File

@@ -13,11 +13,10 @@ export function pluginIcons() {
return {
name: 'isme:icons',
resolveId(id) {
if (id === PLUGIN_ICONS_ID)
return `\0${PLUGIN_ICONS_ID}`
if (id === PLUGIN_ICONS_ID) return '\0' + PLUGIN_ICONS_ID
},
load(id) {
if (id === `\0${PLUGIN_ICONS_ID}`) {
if (id === '\0' + PLUGIN_ICONS_ID) {
return `export default ${JSON.stringify(getIcons())}`
}
},

View File

@@ -6,5 +6,5 @@
* Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top
**********************************/
export { pluginIcons } from './icons'
export { pluginPagePathes } from './page-pathes'
export { pluginIcons } from './icons'

View File

@@ -13,11 +13,10 @@ export function pluginPagePathes() {
return {
name: 'isme:page-pathes',
resolveId(id) {
if (id === PLUGIN_PAGE_PATHES_ID)
return `\0${PLUGIN_PAGE_PATHES_ID}`
if (id === PLUGIN_PAGE_PATHES_ID) return '\0' + PLUGIN_PAGE_PATHES_ID
},
load(id) {
if (id === `\0${PLUGIN_PAGE_PATHES_ID}`) {
if (id === '\0' + PLUGIN_PAGE_PATHES_ID) {
return `export default ${JSON.stringify(getPagePathes())}`
}
},

View File

@@ -1,34 +0,0 @@
import antfu from '@antfu/eslint-config'
export default antfu({
unocss: true,
formatters: true,
stylistic: true,
rules: {
'n/prefer-global/process': 'off',
'no-undef': 'error',
'no-fallthrough': 'off',
'vue/block-order': 'off',
'@typescript-eslint/no-this-alias': 'off',
'prefer-promise-reject-errors': 'off',
},
languageOptions: {
globals: {
h: 'readonly',
unref: 'readonly',
provide: 'readonly',
inject: 'readonly',
markRaw: 'readonly',
defineAsyncComponent: 'readonly',
nextTick: 'readonly',
useRoute: 'readonly',
useRouter: 'readonly',
Message: 'readonly',
$loadingBar: 'readonly',
$message: 'readonly',
$dialog: 'readonly',
$notification: 'readonly',
$modal: 'readonly',
},
},
})

View File

@@ -1,93 +1,27 @@
<!doctype html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" href="/favicon.png" />
<title>%VITE_TITLE%</title>
<style>
.loading-container {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
}
.dark .loading-container {
background-color: #232324;
color: rgba(255, 255, 255, 0.9);
}
.loading-container .loading {
--speed-of-animation: 0.9s;
--gap: 12px;
--first-color: #4c86f9;
--second-color: #49a84c;
--third-color: #f6bb02;
--fourth-color: #26a69a;
--fifth-color: #2196f3;
margin: auto;
width: 160px;
height: 100px;
display: flex;
justify-content: center;
align-items: center;
gap: var(--gap);
}
.loading-container .loading span {
width: 6px;
height: 80px;
background: var(--first-color);
animation: scale var(--speed-of-animation) ease-in-out infinite;
}
.loading-container .loading span:nth-child(2) {
background: var(--second-color);
animation-delay: -0.8s;
}
.loading-container .loading span:nth-child(3) {
background: var(--third-color);
animation-delay: -0.7s;
}
.loading-container .loading span:nth-child(4) {
background: var(--fourth-color);
animation-delay: -0.6s;
}
.loading-container .loading span:nth-child(5) {
background: var(--fifth-color);
animation-delay: -0.5s;
}
@keyframes scale {
0%,
40%,
100% {
transform: scaleY(0.25);
}
20% {
transform: scaleY(1);
}
}
</style>
<link rel="stylesheet" href="/resource/loading.css" />
<title><%= title %></title>
</head>
<body class="dark:text-#e9e9e9 auto-bg">
<div id="app">
<!-- 白屏时的loading效果 -->
<div class="loading-container">
<div class="loading">
<span></span>
<span></span>
<span></span>
<span></span>
<span></span>
<div class="loading-spin__container">
<div class="loading-spin">
<div class="left-0 top-0 loading-spin-item"></div>
<div class="left-0 bottom-0 loading-spin-item loading-delay-500"></div>
<div class="right-0 top-0 loading-spin-item loading-delay-1000"></div>
<div class="right-0 bottom-0 loading-spin-item loading-delay-1500"></div>
</div>
</div>
<div class="loading-title"><%= title %></div>
</div>
<script src="/resource/loading.js"></script>
</div>
<script type="module" src="/src/main.js"></script>
</body>

View File

@@ -1,13 +1,13 @@
{
"compilerOptions": {
"target": "ESNext",
"jsx": "preserve",
"baseUrl": "./",
"moduleResolution": "node",
"paths": {
"@/*": ["src/*"],
"~/*": ["./*"]
},
"jsx": "preserve",
"allowJs": true
},
"exclude": ["node_modules", "dist"]

View File

@@ -1,60 +1,58 @@
{
"name": "vue-naive-admin",
"type": "module",
"version": "2.0.0",
"private": true,
"version": "2.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"lint:fix": "eslint --fix",
"postinstall": "npx simple-git-hooks",
"up": "taze major -I"
"lint:fix": "eslint --fix --ext .js,.vue ."
},
"dependencies": {
"@arco-design/color": "^0.4.0",
"@vueuse/core": "^14.1.0",
"axios": "^1.13.2",
"dayjs": "^1.11.19",
"echarts": "^6.0.0",
"lodash-es": "^4.17.22",
"naive-ui": "^2.43.2",
"pinia": "^3.0.4",
"pinia-plugin-persistedstate": "^4.7.1",
"vue": "^3.5.27",
"vue-echarts": "^8.0.1",
"vue-router": "^4.6.4",
"@vueuse/core": "^10.9.0",
"axios": "^1.6.8",
"dayjs": "^1.11.10",
"echarts": "^5.5.0",
"lodash-es": "^4.17.21",
"naive-ui": "^2.38.1",
"pinia": "^2.1.7",
"pinia-plugin-persistedstate": "^3.2.1",
"vue": "^3.4.23",
"vue-echarts": "^6.7.0",
"vue-router": "^4.3.2",
"xlsx": "^0.18.5"
},
"devDependencies": {
"@antfu/eslint-config": "^7.0.1",
"@iconify/json": "^2.2.430",
"@unocss/eslint-config": "^66.6.0",
"@unocss/eslint-plugin": "^66.6.0",
"@unocss/preset-rem-to-px": "^66.6.0",
"@vitejs/plugin-vue": "^6.0.3",
"@vitejs/plugin-vue-jsx": "^5.1.3",
"eslint": "^9.39.2",
"eslint-plugin-format": "^1.3.1",
"esno": "^4.8.0",
"fs-extra": "^11.3.3",
"glob": "^13.0.0",
"lint-staged": "^16.2.7",
"rollup-plugin-visualizer": "^6.0.5",
"simple-git-hooks": "^2.13.1",
"taze": "^19.9.2",
"unocss": "^66.6.0",
"unplugin-auto-import": "^21.0.0",
"unplugin-vue-components": "^31.0.0",
"vite": "^7.3.1",
"vite-plugin-router-warn": "^2.0.0",
"vite-plugin-vue-devtools": "^8.0.5",
"vue3-intro-step": "^1.0.5"
"@iconify/json": "^2.2.203",
"@iconify/utils": "^2.1.23",
"@unocss/eslint-config": "^0.58.5",
"@unocss/preset-rem-to-px": "^0.59.4",
"@vitejs/plugin-vue": "^5.0.4",
"@zclzone/eslint-config": "^0.0.5",
"esno": "^4.7.0",
"fs-extra": "^11.2.0",
"glob": "^10.3.12",
"rollup-plugin-visualizer": "^5.12.0",
"sass": "^1.75.0",
"unocss": "^0.59.4",
"unplugin-auto-import": "^0.17.5",
"unplugin-vue-components": "^0.26.0",
"vite": "^5.2.10",
"vite-plugin-router-warn": "^1.0.0",
"vite-plugin-simple-html": "^0.1.2",
"vite-plugin-vue-devtools": "^7.1.2"
},
"simple-git-hooks": {
"pre-commit": "npx lint-staged"
},
"lint-staged": {
"*": "eslint --fix"
"eslintConfig": {
"extends": [
"@zclzone",
"@unocss",
".eslint-global-variables.json"
],
"parserOptions": {
"ecmaFeatures": {
"jsx": true
}
}
}
}

9055
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,93 @@
/**********************************
* @Author: Ronnie Zhang
* @LastEditor: Ronnie Zhang
* @LastEditTime: 2023/12/04 22:50:18
* @Email: zclzone@outlook.com
* Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top
**********************************/
.loading-container {
position: fixed;
left: 0;
top: 0;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
}
.loading-spin__container {
width: 56px;
height: 56px;
margin: 36px 0;
}
.loading-spin {
position: relative;
height: 100%;
animation: loadingSpin 1s linear infinite;
}
.left-0 {
left: 0;
}
.right-0 {
right: 0;
}
.top-0 {
top: 0;
}
.bottom-0 {
bottom: 0;
}
.loading-spin-item {
position: absolute;
height: 16px;
width: 16px;
background-color: var(--primary-color);
border-radius: 8px;
-webkit-animation: loadingPulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
animation: loadingPulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
}
@keyframes loadingSpin {
from {
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
to {
-webkit-transform: rotate(360deg);
transform: rotate(360deg);
}
}
@keyframes loadingPulse {
0%, 100% {
opacity: 1;
}
50% {
opacity: .5;
}
}
.loading-delay-500 {
-webkit-animation-delay: 500ms;
animation-delay: 500ms;
}
.loading-delay-1000 {
-webkit-animation-delay: 1000ms;
animation-delay: 1000ms;
}
.loading-delay-1500 {
-webkit-animation-delay: 1500ms;
animation-delay: 1500ms;
}
.loading-title {
font-size: 28px;
font-weight: 500;
color: var(--primary-color);
}

View File

@@ -0,0 +1,18 @@
/**********************************
* @Author: Ronnie Zhang
* @LastEditor: Ronnie Zhang
* @LastEditTime: 2023/12/04 22:50:27
* @Email: zclzone@outlook.com
* Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top
**********************************/
function addThemeColorCssVars() {
const key = '__THEME_COLOR__'
const defaultColor = '#316c72'
const themeColor = localStorage.getItem(key) || defaultColor
const cssVars = `--primary-color: ${themeColor}`
document.documentElement.style.cssText = cssVars
}
addThemeColorCssVars()

View File

@@ -16,29 +16,27 @@
>
<router-view v-if="Layout" v-slot="{ Component, route: curRoute }">
<component :is="Layout">
<transition name="fade-slide" mode="out-in" appear>
<KeepAlive :include="keepAliveNames">
<component :is="Component" v-if="!tabStore.reloading" :key="curRoute.fullPath" />
</KeepAlive>
</transition>
<KeepAlive :include="keepAliveNames">
<component :is="Component" v-if="!tabStore.reloading" :key="curRoute.fullPath" />
</KeepAlive>
</component>
<LayoutSetting v-if="layoutSettingVisible" class="fixed right-12 top-1/2 z-999" />
<LayoutSetting class="fixed right-12 top-1/2 z-999" />
</router-view>
</n-config-provider>
</template>
<script setup>
import { darkTheme, dateZhCN, zhCN } from 'naive-ui'
import { zhCN, dateZhCN, darkTheme } from 'naive-ui'
import { LayoutSetting } from '@/components'
import { useCssVar } from '@vueuse/core'
import { kebabCase } from 'lodash-es'
import { useAppStore, useTabStore } from '@/store'
import { layoutSettingVisible } from './settings'
const layouts = new Map()
function getLayout(name) {
// 利用map将加载过的layout缓存起来防止重新加载layout导致页面闪烁
if (layouts.get(name))
return layouts.get(name)
if (layouts.get(name)) return layouts.get(name)
const layout = markRaw(defineAsyncComponent(() => import(`@/layouts/${name}/index.vue`)))
layouts.set(name, layout)
return layout
@@ -46,20 +44,23 @@ function getLayout(name) {
const route = useRoute()
const appStore = useAppStore()
if (appStore.layout === 'default')
appStore.setLayout('')
if (appStore.layout === 'default') appStore.setLayout('')
const Layout = computed(() => {
if (!route.matched?.length)
return null
if (!route.matched?.length) return null
return getLayout(route.meta?.layout || appStore.layout)
})
function setupCssVar() {
const common = appStore.naiveThemeOverrides?.common || {}
for (const key in common) {
useCssVar(`--${kebabCase(key)}`, document.documentElement).value = common[key] || ''
if (key === 'primaryColor') window.localStorage.setItem('__THEME_COLOR__', common[key] || '')
}
}
setupCssVar()
const tabStore = useTabStore()
const keepAliveNames = computed(() => {
return tabStore.tabs.filter(item => item.keepAlive).map(item => item.name)
})
watchEffect(() => {
appStore.setThemeColor(appStore.primaryColor, appStore.isDark)
return tabStore.tabs.filter((item) => item.keepAlive).map((item) => item.name)
})
</script>

View File

@@ -14,11 +14,11 @@ export default {
// 刷新token
refreshToken: () => request.get('/auth/refresh/token'),
// 登出
logout: () => request.post('/auth/logout', {}, { needTip: false }),
logout: () => request.post('/auth/logout'),
// 切换当前角色
switchCurrentRole: role => request.post(`/auth/current-role/switch/${role}`),
switchCurrentRole: (role) => request.post(`/auth/current-role/switch/${role}`),
// 获取角色权限
getRolePermissions: () => request.get('/role/permissions/tree'),
// 验证菜单路径
validateMenuPath: path => request.get(`/permission/menu/validate?path=${path}`),
validateMenuPath: (path) => request.get(`/permission/menu/validate?path=${path}`),
}

View File

@@ -8,12 +8,11 @@
<template>
<main class="cus-scroll h-full flex-col flex-1 bg-#f5f6fb dark:bg-#121212">
<main :class="{ 'flex-1': full }" class="m-12">
<slot />
</main>
<slot name="footer">
<TheFooter v-if="showFooter" class="mb-12 mt-auto" />
</slot>
<transition name="fade-slide" mode="out-in" appear>
<main :class="{ 'flex-1': full }" class="m-12"><slot /></main>
</transition>
<slot v-if="$slots.footer" name="footer" />
<TheFooter v-else-if="showFooter" class="mb-12 mt-auto" />
<n-back-top :bottom="20" />
</main>
</template>

View File

@@ -7,17 +7,18 @@
--------------------------------->
<template>
<main class="h-full flex-col flex-1 overflow-hidden bg-#f5f6fb dark:bg-#121212">
<AppCard
v-if="showHeader"
class="sticky top-0 z-1 min-h-60 flex items-center justify-between px-24"
border-b="1px solid light_border dark:dark_border"
>
<slot v-if="$slots.header" name="header" />
<template v-else>
<div class="flex items-center">
<slot name="title-prefix">
<template v-if="back">
<main class="h-full flex-1 overflow-hidden bg-#f5f6fb dark:bg-#121212">
<div class="h-full flex-col">
<AppCard
v-if="showHeader"
class="sticky top-0 z-1 min-h-60 flex items-center justify-between px-24"
border-b="1px solid light_border dark:dark_border"
>
<slot v-if="$slots.header" name="header" />
<template v-else>
<div class="flex items-center">
<slot v-if="$slots['title-prefix']" name="title-prefix" />
<template v-else-if="back">
<div
class="mr-16 flex cursor-pointer items-center text-16 opacity-60 transition-all-300 hover:opacity-40"
@click="router.back()"
@@ -26,27 +27,27 @@
<span class="ml-4">返回</span>
</div>
</template>
</slot>
<div class="mr-12 h-16 w-4 rounded-l-2 bg-primary" />
<h2 class="font-normal">
{{ title ?? route.meta?.title }}
</h2>
<slot name="title-suffix" />
</div>
<slot name="action" />
</template>
</AppCard>
<AppCard class="cus-scroll m-12 h-0 flex-1 rounded-8 p-24" bordered>
<slot />
</AppCard>
<slot name="footer">
<AppCard v-if="showFooter" class="flex-shrink-0 py-12">
<TheFooter />
<div class="mr-12 h-16 w-4 rounded-l-2 bg-primary"></div>
<h2 class="font-normal">{{ title ?? route.meta?.title }}</h2>
<slot name="title-suffix" />
</div>
<slot name="action" />
</template>
</AppCard>
</slot>
<transition name="fade-slide" mode="out-in" appear>
<AppCard class="cus-scroll m-12 h-0 flex-1 rounded-8 p-24" bordered>
<slot />
</AppCard>
</transition>
</div>
</main>
<footer class="bg-#f5f6fb dark:bg-#121212">
<slot v-if="$slots.footer" name="footer" />
<AppCard v-else-if="showFooter" class="py-12">
<TheFooter />
</AppCard>
</footer>
</template>
<script setup>

View File

@@ -10,7 +10,7 @@
<div>
<n-tooltip trigger="hover" placement="left">
<template #trigger>
<div id="layout-setting" class="f-c-c rounded-4 bg-primary p-8" @click="modalRef.open()">
<div class="f-c-c rounded-4 bg-primary p-8" @click="modalRef.open()">
<i class="i-fe:settings cursor-pointer bg-white text-20" />
</div>
</template>
@@ -94,8 +94,8 @@
<script setup>
import { MeModal } from '@/components'
import { useModal } from '@/composables'
import { useAppStore } from '@/store'
import { useModal } from '@/composables'
const appStore = useAppStore()
const [modalRef] = useModal()

View File

@@ -6,7 +6,7 @@
- Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top
--------------------------------->
<template>
<div class="h-32 w-32 rounded-4 bg-primary">
<img src="@/assets/images/isme.png" alt="Logo">
<div class="h-40 w-40 rounded-full bg-primary p-1/100">
<img src="@/assets/images/isme.png" alt="Logo" />
</div>
</template>

View File

@@ -1,26 +0,0 @@
<template>
<div class="f-c-c">
<n-tooltip trigger="hover" placement="bottom">
<template #trigger>
<n-color-picker
id="theme-setting"
class="h-32 w-32"
:value="appStore.primaryColor"
:swatches="primaryColors"
:on-update:value="(v) => appStore.setPrimaryColor(v)"
:render-label="() => ''"
/>
</template>
设置主题色
</n-tooltip>
</div>
</template>
<script setup>
import { getPresetColors } from '@arco-design/color'
import { useAppStore } from '@/store'
const appStore = useAppStore()
const primaryColors = Object.entries(getPresetColors()).map(([, value]) => value.primary)
</script>

View File

@@ -1,46 +0,0 @@
<template>
<i
id="toggleTheme"
class="mr-16 cursor-pointer"
:class="isDark ? 'i-fe:moon' : 'i-fe:sun'"
@click="toggleDark"
/>
</template>
<script setup>
import { useDark, useToggle } from '@vueuse/core'
import { useAppStore } from '@/store'
const appStore = useAppStore()
const isDark = useDark()
async function toggleDark({ clientX, clientY }) {
function handler() {
appStore.toggleDark()
useToggle(isDark)()
}
if (!document.startViewTransition) {
return handler()
}
const clipPath = [
`circle(0px at ${clientX}px ${clientY}px)`,
`circle(${Math.hypot(
Math.max(clientX, window.innerWidth - clientX),
Math.max(clientY, window.innerHeight - clientY),
)}px at ${clientX}px ${clientY}px)`,
]
await document.startViewTransition(handler).ready
document.documentElement.animate(
{ clipPath: isDark.value ? clipPath.reverse() : clipPath },
{
duration: 500,
easing: 'ease-in',
pseudoElement: `::view-transition-${isDark.value ? 'old' : 'new'}(root)`,
fill: 'both',
},
)
}
</script>

View File

@@ -1,6 +1,5 @@
export { default as AppCard } from './AppCard.vue'
export { default as TheFooter } from './TheFooter.vue'
export { default as AppPage } from './AppPage.vue'
export { default as CommonPage } from './CommonPage.vue'
export { default as LayoutSetting } from './LayoutSetting.vue'
export { default as TheFooter } from './TheFooter.vue'
export { default as ToggleTheme } from './ToggleTheme.vue'

View File

@@ -8,10 +8,10 @@
<template>
<div class="flex items-center">
<label v-if="label || label === 0" class="flex-shrink-0" :style="{ width: `${labelWidth}px` }">
<label v-if="label || label === 0" class="flex-shrink-0" :style="{ width: labelWidth + 'px' }">
{{ label }}
</label>
<div :style="{ width: `${contentWidth}px` }" class="flex-shrink-0">
<div :style="{ width: contentWidth + 'px' }" class="flex-shrink-0">
<slot />
</div>
</div>

View File

@@ -7,52 +7,35 @@
--------------------------------->
<template>
<div class="h-full flex flex-col overflow-hidden">
<AppCard v-if="$slots.default" bordered bg="#fafafc dark:black" class="mb-30 min-h-60 rounded-4">
<form class="flex justify-between p-16" @submit.prevent="handleSearch()">
<n-scrollbar x-scrollable>
<n-space :wrap="!expand || isExpanded" :size="[32, 16]" class="p-10">
<slot />
</n-space>
</n-scrollbar>
<div class="flex-shrink-0 p-10">
<n-button ghost type="primary" @click="handleReset">
<i class="i-fe:rotate-ccw mr-4" />
重置
</n-button>
<n-button attr-type="submit" class="ml-20" type="primary">
<i class="i-fe:search mr-4" />
搜索
</n-button>
<AppCard v-if="$slots.default" bordered bg="#fafafc dark:black" class="mb-30 min-h-60 rounded-4">
<form class="flex justify-between p-16" @submit.prevent="handleSearch()">
<n-space wrap :size="[32, 16]">
<slot />
</n-space>
<div class="flex-shrink-0">
<n-button ghost type="primary" @click="handleReset">
<i class="i-fe:rotate-ccw mr-4" />
重置
</n-button>
<n-button attr-type="submit" class="ml-20" type="primary">
<i class="i-fe:search mr-4" />
搜索
</n-button>
</div>
</form>
</AppCard>
<template v-if="expand">
<n-button v-if="!isExpanded" type="primary" text @click="toggleExpand">
<i class="i-fe:chevrons-down ml-4" />
展开
</n-button>
<n-button v-else text type="primary" @click="toggleExpand">
<i class="i-fe:chevrons-up ml-4" />
收起
</n-button>
</template>
</div>
</form>
</AppCard>
<NDataTable
:remote="remote"
:loading="loading"
:scroll-x="scrollX"
:columns="columns"
:data="tableData"
:row-key="(row) => row[rowKey]"
:pagination="isPagination ? pagination : false"
flex-height
class="flex-1"
@update:checked-row-keys="onChecked"
@update:page="onPageChange"
/>
</div>
<n-data-table
:remote="remote"
:loading="loading"
:scroll-x="scrollX"
:columns="columns"
:data="tableData"
:row-key="(row) => row[rowKey]"
:pagination="isPagination ? pagination : false"
@update:checked-row-keys="onChecked"
@update:page="onPageChange"
/>
</template>
<script setup>
@@ -68,7 +51,7 @@ const props = defineProps({
default: true,
},
/**
* @isPagination 是否分页
* @remote 是否分页
*/
isPagination: {
type: Boolean,
@@ -95,10 +78,10 @@ const props = defineProps({
},
/**
* ! 约定接口入参出参
* 分页模式需约定分页接口入参
* * 分页模式需约定分页接口入参
* @pageSize 分页参数一页展示多少条默认10
* @pageNo 分页参数页码默认1
* 需约定接口出参
* * 需约定接口出参
* @pageData 分页模式必须,非分页模式如果没有pageData则取上一层data
* @total 分页模式必须非分页模式如果没有total则取上一层data.length
*/
@@ -106,28 +89,13 @@ const props = defineProps({
type: Function,
required: true,
},
/** 是否支持展开 */
expand: Boolean,
})
const emit = defineEmits(['update:queryItems', 'onChecked', 'onDataChange'])
const loading = ref(false)
const initQuery = { ...props.queryItems }
const tableData = ref([])
const pagination = reactive({
page: 1,
pageSize: 10,
prefix({ itemCount }) {
return `${itemCount} 条数据`
},
})
// 是否展开
const isExpanded = ref(false)
function toggleExpand() {
isExpanded.value = !isExpanded.value
}
const pagination = reactive({ page: 1, pageSize: 10 })
async function handleQuery() {
try {
@@ -143,29 +111,17 @@ async function handleQuery() {
})
tableData.value = data?.pageData || data
pagination.itemCount = data.total ?? data.length
if (pagination.itemCount && !tableData.value.length && pagination.page > 1) {
// 如果当前页数据为空且总条数不为0则返回上一页数据
onPageChange(pagination.page - 1)
}
}
catch (error) {
console.error(error)
} catch (error) {
tableData.value = []
pagination.itemCount = 0
}
finally {
} finally {
emit('onDataChange', tableData.value)
loading.value = false
}
}
function handleSearch(keepCurrentPage = false) {
if (keepCurrentPage || !props.remote) {
handleQuery()
}
else {
onPageChange(1)
}
function handleSearch() {
pagination.page = 1
handleQuery()
}
async function handleReset() {
const queryItems = { ...props.queryItems }
@@ -184,17 +140,16 @@ function onPageChange(currentPage) {
}
}
function onChecked(rowKeys) {
if (props.columns.some(item => item.type === 'selection')) {
if (props.columns.some((item) => item.type === 'selection')) {
emit('onChecked', rowKeys)
}
}
function handleExport(columns = props.columns, data = tableData.value) {
if (!data?.length)
return $message.warning('没有数据')
const columnsData = columns.filter(item => !!item.title && !item.hideInExcel)
const thKeys = columnsData.map(item => item.key)
const thData = columnsData.map(item => item.title)
const trData = data.map(item => thKeys.map(key => item[key]))
if (!data?.length) return $message.warning('没有数据')
const columnsData = columns.filter((item) => !!item.title && !item.hideInExcel)
const thKeys = columnsData.map((item) => item.key)
const thData = columnsData.map((item) => item.title)
const trData = data.map((item) => thKeys.map((key) => item[key]))
const sheet = utils.aoa_to_sheet([thData, ...trData])
const workBook = utils.book_new()
utils.book_append_sheet(workBook, sheet, '数据报表')

View File

@@ -1,3 +1,3 @@
export { default as MeModal } from './modal/index.vue'
export { default as MeCrud } from './crud/index.vue'
export { default as MeQueryItem } from './crud/QueryItem.vue'
export { default as MeModal } from './modal/index.vue'

View File

@@ -18,30 +18,27 @@
>
<n-card :style="modalOptions.contentStyle" :closable="modalOptions.closable" @close="close()">
<template #header>
<header class="modal-header">
{{ modalOptions.title }}
</header>
<header class="modal-header">{{ modalOptions.title }}</header>
</template>
<slot />
<slot></slot>
<!-- 底部按钮 -->
<template #footer>
<slot name="footer">
<footer v-if="modalOptions.showFooter" class="flex justify-end">
<n-button v-if="modalOptions.showCancel" @click="handleCancel()">
{{ modalOptions.cancelText }}
</n-button>
<n-button
v-if="modalOptions.showOk"
type="primary"
:loading="modalOptions.okLoading"
class="ml-20"
@click="handleOk()"
>
{{ modalOptions.okText }}
</n-button>
</footer>
</slot>
<slot v-if="$slots.footer" name="footer" />
<footer v-else-if="modalOptions.showFooter" class="flex justify-end">
<n-button v-if="modalOptions.showCancel" @click="handleCancel()">
{{ modalOptions.cancelText }}
</n-button>
<n-button
v-if="modalOptions.showOk"
type="primary"
:loading="modalOptions.okLoading"
class="ml-20"
@click="handleOk()"
>
{{ modalOptions.okText }}
</n-button>
</footer>
</template>
</n-card>
</n-modal>
@@ -49,7 +46,6 @@
<script setup>
import { initDrag } from './utils'
const props = defineProps({
width: {
type: String,
@@ -105,17 +101,6 @@ const show = ref(false)
// 声明一个modalOptions变量用于存储模态框的配置信息
const modalOptions = ref({})
const okLoading = computed({
get() {
return !!modalOptions.value?.okLoading
},
set(v) {
if (modalOptions.value) {
modalOptions.value.okLoading = v
}
},
})
// 打开模态框
async function open(options = {}) {
// 将props和options合并赋值给modalOptions
@@ -126,7 +111,7 @@ async function open(options = {}) {
await nextTick()
initDrag(
Array.prototype.at.call(document.querySelectorAll('.modal-header'), -1),
Array.prototype.at.call(document.querySelectorAll('.modal-box'), -1),
Array.prototype.at.call(document.querySelectorAll('.modal-box'), -1)
)
}
@@ -145,12 +130,10 @@ async function handleOk(data) {
// 调用onOk函数传入data参数
const res = await modalOptions.value.onOk(data)
// 如果onOk函数的返回值不为false则关闭模态框
if (res !== false)
close()
}
catch (error) {
console.error(error)
res !== false && close()
} catch (error) {
okLoading.value = false
console.error(error)
}
}
@@ -165,12 +148,10 @@ async function handleCancel(data) {
const res = await modalOptions.value.onCancel(data)
// 如果onCancel函数的返回值不为false则关闭模态框
if (res !== false)
close()
}
catch (error) {
console.error(error)
res !== false && close()
} catch (error) {
okLoading.value = false
console.error(error)
}
}
@@ -178,10 +159,21 @@ async function onAfterLeave() {
await nextTick()
initDrag(
Array.prototype.at.call(document.querySelectorAll('.modal-header'), -1),
Array.prototype.at.call(document.querySelectorAll('.modal-box'), -1),
Array.prototype.at.call(document.querySelectorAll('.modal-box'), -1)
)
}
const okLoading = computed({
get() {
return !!modalOptions.value?.okLoading
},
set(v) {
if (modalOptions.value) {
modalOptions.value.okLoading = v
}
},
})
// 定义一个defineExpose函数用于暴露open、close、handleOk、handleCancel函数
defineExpose({
open,

View File

@@ -15,8 +15,7 @@ function getCss(element, key) {
// 初始化拖拽
export function initDrag(bar, box) {
if (!bar || !box)
return
if (!bar || !box) return
const params = {
left: 0,
top: 0,
@@ -51,8 +50,7 @@ export function initDrag(bar, box) {
}
}
document.onmousemove = function (e) {
if (e.target !== bar && !params.flag)
return
if (e.target !== bar && !params.flag) return
e.preventDefault() // 阻止默认事件
// 如果拖拽标志为true
@@ -62,11 +60,11 @@ export function initDrag(bar, box) {
const disX = nowX - params.currentX // 鼠标移动的X距离
const disY = nowY - params.currentY // 鼠标移动的Y距离
const left = Number.parseInt(params.left) + disX // 盒子元素的新left值
const top = Number.parseInt(params.top) + disY // 盒子元素的新top值
let left = parseInt(params.left) + disX // 盒子元素的新left值
let top = parseInt(params.top) + disY // 盒子元素的新top值
box.style.left = `${left}px`
box.style.top = `${top}px`
box.style.left = left + 'px'
box.style.top = top + 'px'
}
}
}

View File

@@ -1,4 +1,4 @@
export * from './useAliveData'
export * from './useCrud'
export * from './useForm'
export * from './useModal'
export * from './useAliveData'

View File

@@ -7,7 +7,7 @@
**********************************/
const lastDataMap = new Map()
export function useAliveData(initData = {}, key) {
export const useAliveData = (initData = {}, key) => {
key = key ?? useRoute().name
const lastData = lastDataMap.get(key)
const aliveData = ref(lastData || { ...initData })
@@ -17,7 +17,7 @@ export function useAliveData(initData = {}, key) {
(v) => {
lastDataMap.set(key, v)
},
{ deep: true },
{ deep: true }
)
return {

View File

@@ -6,8 +6,7 @@
* Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top
**********************************/
import { cloneDeep } from 'lodash-es'
import { useForm, useModal } from '.'
import { useModal, useForm } from '.'
const ACTIONS = {
view: '查看',
@@ -15,14 +14,14 @@ const ACTIONS = {
add: '新增',
}
export function useCrud({ name, initForm = {}, doCreate, doDelete, doUpdate, refresh }) {
export const useCrud = ({ name, initForm = {}, doCreate, doDelete, doUpdate, refresh }) => {
const modalAction = ref('')
const [modalRef, okLoading] = useModal()
const [modalFormRef, modalForm, validation] = useForm(initForm)
/** 新增 */
function handleAdd(row = {}, title) {
handleOpen({ action: 'add', title, row: Object.assign({}, cloneDeep(initForm), cloneDeep(row)) })
handleOpen({ action: 'add', title, row: { ...initForm, ...row } })
}
/** 修改 */
@@ -45,8 +44,7 @@ export function useCrud({ name, initForm = {}, doCreate, doDelete, doUpdate, ref
async onOk() {
if (typeof onOk === 'function') {
return await onOk()
}
else {
} else {
return await handleSave()
}
},
@@ -79,9 +77,7 @@ export function useCrud({ name, initForm = {}, doCreate, doDelete, doUpdate, ref
action.cb()
okLoading.value = false
data && refresh(data)
}
catch (error) {
console.error(error)
} catch (error) {
okLoading.value = false
return false
}
@@ -89,8 +85,7 @@ export function useCrud({ name, initForm = {}, doCreate, doDelete, doUpdate, ref
/** 删除 */
function handleDelete(id, confirmOptions) {
if (!id && id !== 0)
return
if (!id && id !== 0) return
const d = $dialog.warning({
content: '确定删除?',
title: '提示',
@@ -102,10 +97,8 @@ export function useCrud({ name, initForm = {}, doCreate, doDelete, doUpdate, ref
const data = await doDelete(id)
$message.success('删除成功')
d.loading = false
refresh(data, true)
}
catch (error) {
console.error(error)
refresh(data)
} catch (error) {
d.loading = false
}
},

View File

@@ -6,11 +6,9 @@
* Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top
**********************************/
import { cloneDeep } from 'lodash-es'
export function useForm(initFormData = {}) {
export const useForm = (initFormData = {}) => {
const formRef = ref(null)
const formModel = ref(cloneDeep(initFormData))
const formModel = ref({ ...initFormData })
const rules = {
required: {
required: true,

View File

@@ -6,7 +6,7 @@
* Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top
**********************************/
export function useModal() {
export const useModal = () => {
const modalRef = ref(null)
const okLoading = computed({
get() {

View File

@@ -6,13 +6,12 @@
* Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top
**********************************/
import { withDirectives } from 'vue'
import { router } from '@/router'
const permission = {
mounted(el, binding) {
const currentRoute = unref(router.currentRoute)
const btns = currentRoute.meta?.btns?.map(item => item.code) || []
const btns = currentRoute.meta?.btns?.map((item) => item.code) || []
if (!btns.includes(binding.value)) {
el.remove()
}
@@ -22,17 +21,3 @@ const permission = {
export function setupDirectives(app) {
app.directive('permission', permission)
}
/**
* 用于h函数使用自定义权限指令
*
* @param {*} vnode 虚拟节点
* @param {*} code 权限码
* @returns 返回一个包含权限指令的vnode
*
* 使用示例withPermission(h('button', {class: 'text-red-500'}, '删除'), 'user:delete')
*
*/
export function withPermission(vnode, code) {
return withDirectives(vnode, [[permission, code]])
}

View File

@@ -1,120 +0,0 @@
<template>
<n-tooltip trigger="hover">
<template #trigger>
<i
class="i-fe:beginner mr-16 cursor-pointer text-20"
@click="show = true"
/>
</template>
操作指引
</n-tooltip>
<Vue3IntroStep
ref="myIntroStep"
v-model:show="show"
:config="config"
>
<template #prev="{ tipItem, index }">
<NButton class="mr-12" type="primary" color="#fff" text-color="#fff" ghost round size="small" @click="prev(tipItem, index)">
上一步
</NButton>
</template>
<template #next="{ tipItem }">
<NButton class="mr-12" type="primary" color="#fff" text-color="#fff" ghost round size="small" @click="next(tipItem)">
下一步
</NButton>
</template>
<template #skip>
<NButton type="primary" color="#fff" text-color="#fff" ghost round size="small" @click="skip">
跳过
</NButton>
</template>
<template #done>
<NButton type="primary" color="#fff" text-color="#fff" ghost round size="small" @click="done">
完成
</NButton>
</template>
</Vue3IntroStep>
</template>
<script setup>
import Vue3IntroStep from 'vue3-intro-step'
const myIntroStep = shallowRef(null)
const show = shallowRef(false)
const config = {
backgroundOpacity: 0.8,
titleStyle: {
textAlign: 'left',
fontSize: '18px',
},
contentStyle: {
textAlign: 'left',
fontSize: '14px',
},
tips: [
{
el: '#toggleTheme',
tipPosition: 'bottom',
title: '切换系统主题',
content: '一键开启护眼模式',
},
{
el: '#fullscreen',
tipPosition: 'bottom',
title: '全屏/退出全屏',
content: '一键开启全屏',
},
{
el: '#theme-setting',
tipPosition: 'bottom',
title: '设置主题色',
content: '调整为你喜欢的主题色',
},
{
el: '#user-dropdown',
tipPosition: 'bottom',
title: '个人中心',
content: '查看个人资料和退出系统',
},
{
el: '#menu-collapse',
tipPosition: 'bottom',
title: '展开/收起菜单',
content: '一键展开/收起菜单',
},
{
el: '#top-tab',
tipPosition: 'bottom',
title: '标签栏',
content: '鼠标滚轮滑动可调整至最佳视野',
},
{
el: '#layout-setting',
tipPosition: 'left',
title: '调整系统布局',
content: '将系统布局调整为你喜欢的样子',
},
],
}
function skip() {
show.value = false
}
function done() {
show.value = false
}
function next() {
// tipItem当前的提示项信息
// 调用vue3-intro-step的next方法 手动触发下一步
myIntroStep.value.next()
}
function prev() {
// 调用vue3-intro-step的prev方法 手动触发上一步
myIntroStep.value.prev()
}
</script>

View File

@@ -33,7 +33,6 @@
<script setup>
import { usePermissionStore } from '@/store'
const router = useRouter()
const route = useRoute()
const permissionStore = usePermissionStore()
@@ -44,7 +43,7 @@ watch(
(v) => {
breadItems.value = findMatchs(permissionStore.permissions, v)
},
{ immediate: true },
{ immediate: true }
)
function findMatchs(tree, code, parents = []) {
@@ -70,8 +69,8 @@ function handleItemClick(item) {
function getDropOptions(list = []) {
return list
.filter(item => item.show)
.map(child => ({
.filter((item) => item.show)
.map((child) => ({
label: child.name,
key: child.code,
icon: () => h('i', { class: child.icon }),

View File

@@ -1,14 +0,0 @@
<template>
<i
id="fullscreen"
class="mr-16 cursor-pointer"
:class="isFullscreen ? 'i-fe:minimize' : 'i-fe:maximize'"
@click="toggle"
/>
</template>
<script setup>
import { useFullscreen } from '@vueuse/core'
const { isFullscreen, toggle } = useFullscreen()
</script>

View File

@@ -8,8 +8,7 @@
<template>
<div
id="menu-collapse"
class="f-c-c cursor-pointer rounded-4 auto-bg-hover p-6 text-22 transition-all-300"
class="f-c-c cursor-pointer rounded-4 p-6 text-22 transition-all-300 auto-bg-hover"
@click="appStore.switchCollapsed"
>
<i :class="appStore.collapsed ? 'i-line-md-menu-unfold-left' : 'i-line-md-menu-fold-left'" />

View File

@@ -24,9 +24,7 @@
<template #footer>
<div class="flex">
<n-button class="flex-1" size="large" @click="logout()">
退出登录
</n-button>
<n-button class="flex-1" size="large" @click="logout()">退出登录</n-button>
<n-button
:loading="okLoading"
class="ml-20 flex-1"
@@ -46,13 +44,13 @@
import api from '@/api'
import { MeModal } from '@/components'
import { useModal } from '@/composables'
import { useAuthStore, useUserStore } from '@/store'
import { useUserStore, useAuthStore } from '@/store'
const userStore = useUserStore()
const authStore = useAuthStore()
const roles = ref(userStore.roles || [])
const roleCode = ref(userStore.currentRole?.code ?? roles.value[0]?.code ?? '')
const roleCode = ref(userStore.currentRole?.code ?? roles[0]?.code ?? '')
const [modalRef, okLoading] = useModal()
function open(options) {
@@ -69,8 +67,7 @@ async function setCurrentRole() {
okLoading.value = false
$message.success('切换成功')
modalRef.value?.handleOk()
}
catch (error) {
} catch (error) {
console.error(error)
okLoading.value = false
return false

View File

@@ -8,7 +8,8 @@
<template>
<router-link class="h-60 f-c-c" to="/">
<TheLogo />
<!-- <img src="@/assets/images/logo.png" class="h-40" /> -->
<TheLogo class="rounded-8!" />
<h2
v-show="!appStore.collapsed"
class="ml-10 max-w-140 flex-shrink-0 text-16 color-primary font-bold"
@@ -20,7 +21,6 @@
<script setup>
import { useAppStore } from '@/store'
const title = import.meta.env.VITE_TITLE
const appStore = useAppStore()

View File

@@ -52,24 +52,26 @@ function handleMenuSelect(key, item) {
router.push(item.path)
},
})
}
else {
if (!item.path)
return
} else {
router.push(item.path)
}
}
</script>
<style>
.side-menu:not(.n-menu--collapsed) {
.n-menu-item-content {
&::before {
left: 8px;
right: 8px;
}
&.n-menu-item-content--selected::before {
border-left: 4px solid rgb(var(--primary-color));
<style lang="scss">
.side-menu {
.n-menu-item-content__icon {
border: 1px solid rgb(229, 231, 235);
border-radius: 4px;
}
.n-menu-item-content--child-active,
.n-menu-item-content--selected {
.n-menu-item-content__icon {
border-color: var(--primary-color);
background-color: var(--primary-color);
i {
color: #fff;
}
}
}
}

View File

@@ -8,7 +8,7 @@
<template>
<n-dropdown :options="options" @select="handleSelect">
<div id="user-dropdown" class="flex cursor-pointer items-center">
<div class="flex cursor-pointer items-center">
<n-avatar round :size="36" :src="userStore.avatar" />
<div v-if="userStore.userInfo" class="ml-12 flex-col flex-shrink-0 items-center">
<span class="text-14">{{ userStore.nickName ?? userStore.username }}</span>
@@ -21,9 +21,9 @@
</template>
<script setup>
import api from '@/api'
import { useUserStore, useAuthStore, usePermissionStore } from '@/store'
import { RoleSelect } from '@/layouts/components'
import { useAuthStore, usePermissionStore, useUserStore } from '@/store'
import api from '@/api'
const router = useRouter()
const userStore = useUserStore()
@@ -35,7 +35,7 @@ const options = reactive([
label: '个人资料',
key: 'profile',
icon: () => h('i', { class: 'i-material-symbols:person-outline text-14' }),
show: computed(() => permissionStore.accessRoutes?.some(item => item.path === '/profile')),
show: computed(() => permissionStore.accessRoutes?.some((item) => item.path === '/profile')),
},
{
label: '切换角色',
@@ -71,8 +71,7 @@ function handleSelect(key) {
async confirm() {
try {
await api.logout()
}
catch (error) {
} catch (error) {
console.error(error)
}
authStore.logout()

View File

@@ -1,9 +1,7 @@
export { default as BeginnerGuide } from './BeginnerGuide.vue'
export { default as BreadCrumb } from './BreadCrumb.vue'
export { default as Fullscreen } from './Fullscreen.vue'
export { default as MenuCollapse } from './MenuCollapse.vue'
export { default as RoleSelect } from './RoleSelect.vue'
export { default as UserAvatar } from './UserAvatar.vue'
export { default as MenuCollapse } from './MenuCollapse.vue'
export { default as BreadCrumb } from './BreadCrumb.vue'
export { default as AppTab } from './tab/index.vue'
export { default as SideLogo } from './SideLogo.vue'
export { default as SideMenu } from './SideMenu.vue'
export { default as AppTab } from './tab/index.vue'
export { default as UserAvatar } from './UserAvatar.vue'

View File

@@ -73,8 +73,8 @@ const options = computed(() => [
label: '关闭右侧',
key: 'close-right',
disabled:
tabStore.tabs.length <= 1
|| props.currentPath === tabStore.tabs[tabStore.tabs.length - 1].path,
tabStore.tabs.length <= 1 ||
props.currentPath === tabStore.tabs[tabStore.tabs.length - 1].path,
icon: () => h('i', { class: 'i-mdi:arrow-expand-right text-14' }),
},
])
@@ -119,8 +119,7 @@ function handleHideDropdown() {
function handleSelect(key) {
const actionFn = actionMap.get(key)
if (typeof actionFn === 'function')
actionFn()
actionFn && actionFn()
handleHideDropdown()
}
</script>

View File

@@ -7,23 +7,27 @@
--------------------------------->
<template>
<div id="top-tab">
<n-tabs
:value="tabStore.activeTab"
:closable="tabStore.tabs.length > 1"
type="card"
@close="(path) => tabStore.removeTab(path)"
>
<n-tab
v-for="item in tabStore.tabs"
:key="item.path"
:name="item.path"
@click="handleItemClick(item.path)"
@contextmenu.prevent="handleContextMenu($event, item)"
<div>
<n-scrollbar x-scrollable>
<n-tabs
:value="tabStore.activeTab"
:closable="tabStore.tabs.length > 1"
class="py-10"
:style="`--selected-bg: ${appStore.isDark ? '#1b2429' : '#eaf0f1'}`"
type="card"
@close="(path) => tabStore.removeTab(path)"
>
{{ item.title }}
</n-tab>
</n-tabs>
<n-tab
v-for="item in tabStore.tabs"
:key="item.path"
:name="item.path"
@click="handleItemClick(item.path)"
@contextmenu.prevent="handleContextMenu($event, item)"
>
{{ item.title }}
</n-tab>
</n-tabs>
</n-scrollbar>
<ContextMenu
v-if="contextMenuOption.show"
@@ -36,10 +40,11 @@
</template>
<script setup>
import { useTabStore } from '@/store'
import ContextMenu from './ContextMenu.vue'
import { useTabStore, useAppStore } from '@/store'
const router = useRouter()
const appStore = useAppStore()
const tabStore = useTabStore()
const contextMenuOption = reactive({
@@ -49,7 +54,7 @@ const contextMenuOption = reactive({
currentPath: '',
})
function handleItemClick(path) {
const handleItemClick = (path) => {
tabStore.setActiveTab(path)
router.push(path)
}
@@ -74,7 +79,7 @@ async function handleContextMenu(e, tagItem) {
}
</script>
<style scoped>
<style scoped lang="scss">
:deep(.n-tabs) {
.n-tabs-tab {
padding-left: 16px;
@@ -83,12 +88,12 @@ async function handleContextMenu(e, tagItem) {
border-radius: 4px !important;
margin-right: 4px;
&:hover {
border: 1px solid rgb(var(--primary-color)) !important;
border: 1px solid var(--primary-color) !important;
}
}
.n-tabs-tab--active {
border: 1px solid rgb(var(--primary-color)) !important;
background-color: rgba(var(--primary-color), 0.1) !important;
border: 1px solid var(--primary-color) !important;
background-color: var(--selected-bg) !important;
}
.n-tabs-pad,
.n-tabs-tab-pad,

View File

@@ -13,11 +13,16 @@
<BreadCrumb />
<div class="ml-auto flex flex-shrink-0 items-center px-12 text-18">
<BeginnerGuide />
<ToggleTheme />
<Fullscreen />
<i
class="mr-16 cursor-pointer"
:class="isDark ? 'i-fe:moon' : 'i-fe:sun'"
@click="toggleDark"
/>
<i
class="mr-16 cursor-pointer"
:class="isFullscreen ? 'i-fe:minimize' : 'i-fe:maximize'"
@click="toggle"
/>
<i
class="i-fe:github mr-16 cursor-pointer"
@@ -27,17 +32,24 @@
class="i-me:gitee mr-16 cursor-pointer"
@click="handleLinkClick('https://gitee.com/isme-admin/vue-naive-admin/tree/2.x')"
/>
<ThemeSetting class="mr-16" />
<UserAvatar />
</div>
</AppCard>
</template>
<script setup>
import { ToggleTheme } from '@/components'
import { BeginnerGuide, BreadCrumb, Fullscreen, MenuCollapse, UserAvatar } from '@/layouts/components'
import { MenuCollapse, UserAvatar, BreadCrumb } from '@/layouts/components'
import { useAppStore } from '@/store'
import { useDark, useToggle, useFullscreen } from '@vueuse/core'
const appStore = useAppStore()
const isDark = useDark()
const toggleDark = () => {
appStore.toggleDark()
useToggle(isDark)()
}
const { isFullscreen, toggle } = useFullscreen()
function handleLinkClick(link) {
window.open(link)

View File

@@ -27,10 +27,10 @@
</template>
<script setup>
import { AppTab } from '@/layouts/components'
import { useAppStore } from '@/store'
import AppHeader from './header/index.vue'
import { AppTab } from '@/layouts/components'
import SideBar from './sidebar/index.vue'
import AppHeader from './header/index.vue'
const appStore = useAppStore()
</script>

View File

@@ -15,11 +15,16 @@
<span class="mx-6 opacity-20">|</span>
<div class="flex flex-shrink-0 items-center px-12 text-18">
<BeginnerGuide />
<ToggleTheme />
<Fullscreen />
<i
class="mr-16 cursor-pointer"
:class="isDark ? 'i-fe:moon' : 'i-fe:sun'"
@click="toggleDark"
/>
<i
class="mr-16 cursor-pointer"
:class="isFullscreen ? 'i-fe:minimize' : 'i-fe:maximize'"
@click="toggle"
/>
<i
class="i-fe:github mr-16 cursor-pointer"
@@ -29,17 +34,24 @@
class="i-me:gitee mr-16 cursor-pointer"
@click="handleLinkClick('https://gitee.com/isme-admin/vue-naive-admin/tree/2.x')"
/>
<ThemeSetting class="mr-16" />
<UserAvatar />
</div>
</AppCard>
</template>
<script setup>
import { ToggleTheme } from '@/components'
import { AppTab, BeginnerGuide, Fullscreen, MenuCollapse, UserAvatar } from '@/layouts/components'
import { UserAvatar, MenuCollapse, AppTab } from '@/layouts/components'
import { useDark, useToggle, useFullscreen } from '@vueuse/core'
import { useAppStore } from '@/store'
const appStore = useAppStore()
const isDark = useDark()
const toggleDark = () => {
appStore.toggleDark()
useToggle(isDark)()
}
const { isFullscreen, toggle } = useFullscreen()
function handleLinkClick(link) {
window.open(link)

View File

@@ -25,8 +25,8 @@
<script setup>
import { useAppStore } from '@/store'
import AppHeader from './header/index.vue'
import SideBar from './sidebar/index.vue'
import AppHeader from './header/index.vue'
const appStore = useAppStore()
</script>

View File

@@ -16,7 +16,7 @@
</template>
<script setup>
import { MenuCollapse, SideLogo, SideMenu, UserAvatar } from '@/layouts/components'
import { UserAvatar, MenuCollapse, SideLogo, SideMenu } from '@/layouts/components'
import { useAppStore } from '@/store'
const appStore = useAppStore()

View File

@@ -8,16 +8,16 @@
* Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top
**********************************/
import '@/styles/reset.css'
import '@/styles/global.scss'
import 'uno.css'
import { createApp } from 'vue'
import App from './App.vue'
import { setupDirectives } from './directives'
import { setupRouter } from './router'
import { setupStore } from './store'
import { setupNaiveDiscreteApi } from './utils'
import '@/styles/reset.css'
import '@/styles/global.css'
import 'uno.css'
import { setupDirectives } from './directives'
async function bootstrap() {
const app = createApp(App)

View File

@@ -13,8 +13,7 @@ export function createPageTitleGuard(router) {
const pageTitle = to.meta?.title
if (pageTitle) {
document.title = `${pageTitle} | ${baseTitle}`
}
else {
} else {
document.title = baseTitle
}
})

View File

@@ -6,8 +6,8 @@
* Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top
**********************************/
import api from '@/api'
import { useAuthStore, usePermissionStore, useUserStore } from '@/store'
import api from '@/api'
import { getPermissions, getUserInfo } from '@/store/helper'
const WHITE_LIST = ['/login', '/404']
@@ -18,16 +18,13 @@ export function createPermissionGuard(router) {
/** 没有token */
if (!token) {
if (WHITE_LIST.includes(to.path))
return true
if (WHITE_LIST.includes(to.path)) return true
return { path: 'login', query: { ...to.query, redirect: to.path } }
}
// 有token的情况
if (to.path === '/login')
return { path: '/' }
if (WHITE_LIST.includes(to.path))
return true
if (to.path === '/login') return { path: '/' }
if (WHITE_LIST.includes(to.path)) return true
const userStore = useUserStore()
const permissionStore = usePermissionStore()
@@ -44,8 +41,7 @@ export function createPermissionGuard(router) {
}
const routes = router.getRoutes()
if (routes.find(route => route.name === to.name))
return true
if (routes.find((route) => route.name === to.name)) return true
// 判断是无权限还是404
const { data: hasMenu } = await api.validateMenuPath(to.path)

View File

@@ -12,8 +12,7 @@ export const EXCLUDE_TAB = ['/404', '/403', '/login']
export function createTabGuard(router) {
router.afterEach((to) => {
if (EXCLUDE_TAB.includes(to.path))
return
if (EXCLUDE_TAB.includes(to.path)) return
const tabStore = useTabStore()
const { name, fullPath: path } = to
const title = to.meta?.title

View File

@@ -6,9 +6,9 @@
* Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top
**********************************/
import { createRouter, createWebHashHistory, createWebHistory } from 'vue-router'
import { basicRoutes } from './basic-routes'
import { createRouter, createWebHistory, createWebHashHistory } from 'vue-router'
import { setupRouterGuards } from './guards'
import { basicRoutes } from './basic-routes'
export const router = createRouter({
history:

View File

@@ -8,17 +8,32 @@
export const defaultLayout = 'normal'
export const defaultPrimaryColor = '#316C72'
// 控制 LayoutSetting 组件是否可见
export const layoutSettingVisible = true
export const naiveThemeOverrides = {
common: {
primaryColor: '#316C72FF',
primaryColorHover: '#316C72E3',
primaryColorPressed: '#2B4C59FF',
primaryColorSuppl: '#316C72E3',
infoColor: '#2080F0FF',
infoColorHover: '#4098FCFF',
infoColorPressed: '#1060C9FF',
infoColorSuppl: '#4098FCFF',
successColor: '#18A058FF',
successColorHover: '#36AD6AFF',
successColorPressed: '#0C7A43FF',
successColorSuppl: '#36AD6AFF',
warningColor: '#F0A020FF',
warningColorHover: '#FCB040FF',
warningColorPressed: '#C97C10FF',
warningColorSuppl: '#FCB040FF',
errorColor: '#D03050FF',
errorColorHover: '#DE576DFF',
errorColorPressed: '#AB1F3FFF',
errorColorSuppl: '#DE576DFF',
},
}
@@ -36,7 +51,7 @@ export const basePermissions = [
code: 'ShowDocs',
name: '项目文档',
type: 'MENU',
path: 'https://isme.top',
path: 'https://docs.isme.top/web/#/624306705/188522224',
icon: 'i-me:docs',
order: 1,
enable: true,

View File

@@ -1,6 +1,5 @@
import { cloneDeep } from 'lodash-es'
import api from '@/api'
import { basePermissions } from '@/settings'
import api from '@/api'
export async function getUserInfo() {
const res = await api.getUser()
@@ -23,9 +22,8 @@ export async function getPermissions() {
try {
const res = await api.getRolePermissions()
asyncPermissions = res?.data || []
}
catch (error) {
} catch (error) {
console.error(error)
}
return cloneDeep(basePermissions).concat(asyncPermissions)
return basePermissions.concat(asyncPermissions)
}

View File

@@ -6,17 +6,15 @@
* Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top
**********************************/
import { generate, getRgbStr } from '@arco-design/color'
import { useDark } from '@vueuse/core'
import { defineStore } from 'pinia'
import { defaultLayout, defaultPrimaryColor, naiveThemeOverrides } from '@/settings'
import { useDark } from '@vueuse/core'
import { defaultLayout, naiveThemeOverrides } from '@/settings'
export const useAppStore = defineStore('app', {
state: () => ({
collapsed: false,
isDark: useDark(),
layout: defaultLayout,
primaryColor: defaultPrimaryColor,
naiveThemeOverrides,
}),
actions: {
@@ -32,25 +30,9 @@ export const useAppStore = defineStore('app', {
setLayout(v) {
this.layout = v
},
setPrimaryColor(color) {
this.primaryColor = color
},
setThemeColor(color = this.primaryColor, isDark = this.isDark) {
const colors = generate(color, {
list: true,
dark: isDark,
})
document.body.style.setProperty('--primary-color', getRgbStr(colors[5]))
this.naiveThemeOverrides.common = Object.assign(this.naiveThemeOverrides.common || {}, {
primaryColor: colors[5],
primaryColorHover: colors[4],
primaryColorSuppl: colors[4],
primaryColorPressed: colors[6],
})
},
},
persist: {
pick: ['collapsed', 'layout', 'primaryColor', 'naiveThemeOverrides'],
paths: ['collapsed', 'naiveThemeOverrides'],
storage: sessionStorage,
},
})

View File

@@ -7,7 +7,7 @@
**********************************/
import { defineStore } from 'pinia'
import { usePermissionStore, useRouterStore, useTabStore, useUserStore } from '@/store'
import { useUserStore, usePermissionStore, useTabStore, useRouterStore } from '@/store'
export const useAuthStore = defineStore('auth', {
state: () => ({

View File

@@ -1,6 +1,6 @@
export * from './app'
export * from './auth'
export * from './permission'
export * from './router'
export * from './tab'
export * from './user'
export * from './router'

View File

@@ -6,9 +6,9 @@
* Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top
**********************************/
import { isExternal } from '@/utils'
import { hyphenate } from '@vueuse/core'
import { defineStore } from 'pinia'
import { isExternal } from '@/utils'
export const usePermissionStore = defineStore('permission', {
state: () => ({
@@ -20,38 +20,35 @@ export const usePermissionStore = defineStore('permission', {
setPermissions(permissions) {
this.permissions = permissions
this.menus = this.permissions
.filter(item => item.type === 'MENU')
.map(item => this.getMenuItem(item))
.filter(item => !!item)
.filter((item) => item.type === 'MENU')
.map((item) => this.getMenuItem(item))
.filter((item) => !!item)
.sort((a, b) => a.order - b.order)
},
getMenuItem(item, parent) {
const route = this.generateRoute(item, item.show ? null : parent?.key)
if (item.enable && route.path && !route.path.startsWith('http'))
this.accessRoutes.push(route)
if (item.enable && route.path && !route.path.startsWith('http')) this.accessRoutes.push(route)
if (!item.show) return null
const menuItem = {
label: route.meta.title,
key: route.name,
path: route.path,
originPath: route.meta.originPath,
icon: () => h('i', { class: `${route.meta.icon} text-16` }),
icon: () => h('i', { class: `${route.meta.icon} text-14` }),
order: item.order ?? 0,
}
const children = item.children?.filter(item => item.type === 'MENU') || []
const children = item.children?.filter((item) => item.type === 'MENU') || []
if (children.length) {
menuItem.children = children
.map(child => this.getMenuItem(child, menuItem))
.filter(item => !!item)
.map((child) => this.getMenuItem(child, menuItem))
.filter((item) => !!item)
.sort((a, b) => a.order - b.order)
if (!menuItem.children.length)
delete menuItem.children
if (!menuItem.children.length) delete menuItem.children
}
if (!item.show)
return null
return menuItem
},
generateRoute(item, parentKey) {
let originPath
let originPath = undefined
if (isExternal(item.path)) {
originPath = item.path
item.component = '/src/views/iframe/index.vue'
@@ -64,14 +61,14 @@ export const usePermissionStore = defineStore('permission', {
component: item.component,
meta: {
originPath,
icon: `${item.icon}?mask`,
icon: item.icon + '?mask',
title: item.name,
layout: item.layout,
keepAlive: !!item.keepAlive,
parentKey,
btns: item.children
?.filter(item => item.type === 'BUTTON')
.map(item => ({ code: item.code, name: item.name })),
?.filter((item) => item.type === 'BUTTON')
.map((item) => ({ code: item.code, name: item.name })),
},
}
},

View File

@@ -17,7 +17,7 @@ export const useTabStore = defineStore('tab', {
}),
getters: {
activeIndex() {
return this.tabs.findIndex(item => item.path === this.activeTab)
return this.tabs.findIndex((item) => item.path === this.activeTab)
},
},
actions: {
@@ -29,22 +29,19 @@ export const useTabStore = defineStore('tab', {
this.tabs = tabs
},
addTab(tab = {}) {
const findIndex = this.tabs.findIndex(item => item.path === tab.path)
const findIndex = this.tabs.findIndex((item) => item.path === tab.path)
if (findIndex !== -1) {
this.tabs.splice(findIndex, 1, tab)
}
else {
} else {
this.setTabs([...this.tabs, tab])
}
this.setActiveTab(tab.path)
},
async reloadTab(path, keepAlive) {
const findItem = this.tabs.find(item => item.path === path)
if (!findItem)
return
const findItem = this.tabs.find((item) => item.path === path)
if (!findItem) return
// 更新key可让keepAlive失效
if (keepAlive)
findItem.keepAlive = false
if (keepAlive) findItem.keepAlive = false
$loadingBar.start()
this.reloading = true
await nextTick()
@@ -56,30 +53,30 @@ export const useTabStore = defineStore('tab', {
}, 100)
},
async removeTab(path) {
this.setTabs(this.tabs.filter(tab => tab.path !== path))
this.setTabs(this.tabs.filter((tab) => tab.path !== path))
if (path === this.activeTab) {
useRouterStore().router?.push(this.tabs[this.tabs.length - 1].path)
}
},
removeOther(curPath = this.activeTab) {
this.setTabs(this.tabs.filter(tab => tab.path === curPath))
this.setTabs(this.tabs.filter((tab) => tab.path === curPath))
if (curPath !== this.activeTab) {
useRouterStore().router?.push(this.tabs[this.tabs.length - 1].path)
}
},
removeLeft(curPath) {
const curIndex = this.tabs.findIndex(item => item.path === curPath)
const curIndex = this.tabs.findIndex((item) => item.path === curPath)
const filterTabs = this.tabs.filter((item, index) => index >= curIndex)
this.setTabs(filterTabs)
if (!filterTabs.find(item => item.path === this.activeTab)) {
if (!filterTabs.find((item) => item.path === this.activeTab)) {
useRouterStore().router?.push(filterTabs[filterTabs.length - 1].path)
}
},
removeRight(curPath) {
const curIndex = this.tabs.findIndex(item => item.path === curPath)
const curIndex = this.tabs.findIndex((item) => item.path === curPath)
const filterTabs = this.tabs.filter((item, index) => index <= curIndex)
this.setTabs(filterTabs)
if (!filterTabs.find(item => item.path === this.activeTab.value)) {
if (!filterTabs.find((item) => item.path === this.activeTab.value)) {
useRouterStore().router?.push(filterTabs[filterTabs.length - 1].path)
}
},
@@ -88,7 +85,7 @@ export const useTabStore = defineStore('tab', {
},
},
persist: {
pick: ['tabs'],
paths: ['tabs'],
storage: sessionStorage,
},
})

View File

@@ -26,12 +26,12 @@ body {
.fade-slide-enter-from {
opacity: 0;
transform: translateX(-2%);
transform: translateX(-30px);
}
.fade-slide-leave-to {
opacity: 0;
transform: translateX(2%);
transform: translateX(30px);
}
/* 自定义滚动条样式 */
@@ -68,24 +68,7 @@ body {
background: #bfbfbf;
}
&::-webkit-scrollbar-thumb:hover {
background: rgb(var(--primary-color));
background: var(--primary-color);
}
}
}
/* 切换主题的动画效果 */
::view-transition-old(root),
::view-transition-new(root) {
animation: none;
mix-blend-mode: normal;
}
::view-transition-old(root),
.dark::view-transition-new(root) {
z-index: 1;
}
::view-transition-new(root),
.dark::view-transition-old(root) {
z-index: 9999;
}

View File

@@ -1,389 +1,49 @@
/*
1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4)
2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116)
2. [UnoCSS]: allow to override the default border color with css var `--un-default-border-color`
*/
/**********************************
* @Author: Ronnie Zhang
* @LastEditor: Ronnie Zhang
* @LastEditTime: 2023/12/05 21:26:38
* @Email: zclzone@outlook.com
* Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top
**********************************/
html {
box-sizing: border-box;
}
*,
::before,
::after {
box-sizing: border-box; /* 1 */
border-width: 0; /* 2 */
border-style: solid; /* 2 */
border-color: var(--un-default-border-color, #e5e7eb); /* 2 */
margin: 0;
padding: 0;
box-sizing: inherit;
}
::before,
::after {
--un-content: '';
}
/*
1. Use a consistent sensible line-height in all browsers.
2. Prevent adjustments of font size after orientation changes in iOS.
3. Use a more readable tab size.
4. Use the user's configured `sans` font-family by default.
5. Use the user's configured `sans` font-feature-settings by default.
6. Use the user's configured `sans` font-variation-settings by default.
7. Disable tap highlights on iOS.
*/
html,
:host {
line-height: 1.5; /* 1 */
-webkit-text-size-adjust: 100%; /* 2 */
-moz-tab-size: 4; /* 3 */
tab-size: 4; /* 3 */
font-family:
ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'; /* 4 */
font-feature-settings: normal; /* 5 */
font-variation-settings: normal; /* 6 */
-webkit-tap-highlight-color: transparent; /* 7 */
}
/*
1. Remove the margin in all browsers.
2. Inherit line-height from `html` so users can set them as a class directly on the `html` element.
*/
body {
margin: 0; /* 1 */
line-height: inherit; /* 2 */
}
/*
1. Add the correct height in Firefox.
2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655)
3. Ensure horizontal rules are visible by default.
*/
hr {
height: 0; /* 1 */
color: inherit; /* 2 */
border-top-width: 1px; /* 3 */
}
/*
Add the correct text decoration in Chrome, Edge, and Safari.
*/
abbr:where([title]) {
text-decoration: underline dotted;
}
/*
Remove the default font size and weight for headings.
*/
h1,
h2,
h3,
h4,
h5,
h6 {
font-size: inherit;
font-weight: inherit;
}
/*
Reset links to optimize for opt-in styling instead of opt-out.
*/
a {
text-decoration: none;
color: inherit;
text-decoration: inherit;
}
/*
Add the correct font weight in Edge and Safari.
*/
b,
strong {
font-weight: bolder;
}
/*
1. Use the user's configured `mono` font-family by default.
2. Use the user's configured `mono` font-feature-settings by default.
3. Use the user's configured `mono` font-variation-settings by default.
4. Correct the odd `em` font sizing in all browsers.
*/
code,
kbd,
samp,
pre {
font-family:
ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace; /* 1 */
font-feature-settings: normal; /* 2 */
font-variation-settings: normal; /* 3 */
font-size: 1em; /* 4 */
}
/*
Add the correct font size in all browsers.
*/
small {
font-size: 80%;
}
/*
Prevent `sub` and `sup` elements from affecting the line height in all browsers.
*/
sub,
sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
/*
1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297)
2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016)
3. Remove gaps between table borders by default.
*/
table {
text-indent: 0; /* 1 */
border-color: inherit; /* 2 */
border-collapse: collapse; /* 3 */
}
/*
1. Change the font styles in all browsers.
2. Remove the margin in Firefox and Safari.
3. Remove default padding in all browsers.
*/
button,
input,
optgroup,
select,
textarea {
font-family: inherit; /* 1 */
font-feature-settings: inherit; /* 1 */
font-variation-settings: inherit; /* 1 */
font-size: 100%; /* 1 */
font-weight: inherit; /* 1 */
line-height: inherit; /* 1 */
color: inherit; /* 1 */
margin: 0; /* 2 */
padding: 0; /* 3 */
}
/*
Remove the inheritance of text transform in Edge and Firefox.
*/
button,
select {
text-transform: none;
}
/*
1. Correct the inability to style clickable types in iOS and Safari.
2. Remove default button styles.
*/
button,
[type='button'],
[type='reset'],
[type='submit'] {
-webkit-appearance: button; /* 1 */
/* background-color: transparent; */
background-image: none; /* 2 */
}
/*
Use the modern Firefox focus style for all focusable elements.
*/
:-moz-focusring {
outline: auto;
}
/*
Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737)
*/
:-moz-ui-invalid {
box-shadow: none;
}
/*
Add the correct vertical alignment in Chrome and Firefox.
*/
progress {
vertical-align: baseline;
}
/*
Correct the cursor style of increment and decrement buttons in Safari.
*/
::-webkit-inner-spin-button,
::-webkit-outer-spin-button {
height: auto;
}
/*
1. Correct the odd appearance in Chrome and Safari.
2. Correct the outline style in Safari.
*/
[type='search'] {
-webkit-appearance: textfield; /* 1 */
outline-offset: -2px; /* 2 */
}
/*
Remove the inner padding in Chrome and Safari on macOS.
*/
::-webkit-search-decoration {
-webkit-appearance: none;
}
/*
1. Correct the inability to style clickable types in iOS and Safari.
2. Change font properties to `inherit` in Safari.
*/
::-webkit-file-upload-button {
-webkit-appearance: button; /* 1 */
font: inherit; /* 2 */
}
/*
Add the correct display in Chrome and Safari.
*/
summary {
display: list-item;
}
/*
Removes the default spacing for appropriate elements.
*/
blockquote,
dl,
dd,
h1,
h2,
h3,
h4,
h5,
h6,
hr,
figure,
p,
pre {
margin: 0;
}
fieldset {
margin: 0;
padding: 0;
}
legend {
padding: 0;
a:hover,
a:link,
a:visited,
a:active {
text-decoration: none;
}
ol,
ul,
menu {
ul {
list-style: none;
margin: 0;
padding: 0;
}
dialog {
padding: 0;
}
/*
Prevent resizing textareas horizontally by default.
*/
input,
textarea {
resize: vertical;
outline: none;
border: none;
resize: none;
}
/*
1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300)
2. Set the default placeholder color to the user's configured gray 400 color.
*/
input::placeholder,
textarea::placeholder {
opacity: 1; /* 1 */
color: #9ca3af; /* 2 */
}
/*
Set the default cursor for buttons.
*/
button,
[role='button'] {
cursor: pointer;
}
/*
Make sure disabled buttons don't get the pointer cursor.
*/
:disabled {
cursor: default;
}
/*
1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14)
2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210)
This can trigger a poorly considered lint error in some tools but is included by design.
*/
img,
svg,
video,
canvas,
audio,
iframe,
embed,
object {
display: block; /* 1 */
vertical-align: middle; /* 2 */
}
/*
Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14)
*/
img,
video {
max-width: 100%;
height: auto;
}
/*
Make elements with the HTML hidden attribute stay hidden by default.
*/
[hidden] {
display: none;
}

View File

@@ -10,10 +10,10 @@
import dayjs from 'dayjs'
/**
* @param {(object | string | number)} time
* @desc 格式化时间
* @param {(Object|string|number)} time
* @param {string} format
* @returns {string | null} 格式化后的时间字符串
*
* @returns {string | null}
*/
export function formatDateTime(time = undefined, format = 'YYYY-MM-DD HH:mm:ss') {
return dayjs(time).format(format)
@@ -24,19 +24,19 @@ export function formatDate(date = undefined, format = 'YYYY-MM-DD') {
}
/**
* @desc 函数节流
* @param {Function} fn
* @param {number} wait
* @returns {Function} 节流函数
*
* @param {Number} wait
* @returns {Function}
*/
export function throttle(fn, wait) {
let context, args
let previous = 0
var context, args
var previous = 0
return function (...argArr) {
const now = +new Date()
return function () {
var now = +new Date()
context = this
args = argArr
args = arguments
if (now - previous > wait) {
fn.apply(context, args)
previous = now
@@ -45,15 +45,16 @@ export function throttle(fn, wait) {
}
/**
* @param {Function} method
* @desc 函数防抖
* @param {Function} func
* @param {number} wait
* @param {boolean} immediate
* @return {*} 防抖函数
* @return {*}
*/
export function debounce(method, wait, immediate) {
let timeout
return function (...args) {
const context = this
let context = this
if (timeout) {
clearTimeout(timeout)
}
@@ -63,15 +64,14 @@ export function debounce(method, wait, immediate) {
* 如果定时器不存在则立即执行并设置一个定时器wait毫秒后将定时器置为null
* 这样确保立即执行后wait毫秒内不会被再次触发
*/
const callNow = !timeout
let callNow = !timeout
timeout = setTimeout(() => {
timeout = null
}, wait)
if (callNow) {
method.apply(context, args)
}
}
else {
} else {
// 如果immediate为false则函数wait毫秒后执行
timeout = setTimeout(() => {
/**
@@ -85,11 +85,12 @@ export function debounce(method, wait, immediate) {
}
/**
* @desc 睡一会儿,让子弹暂停一下
* @param {number} time 毫秒数
* @returns 睡一会儿,让子弹暂停一下
* @returns
*/
export function sleep(time) {
return new Promise(resolve => setTimeout(resolve, time))
return new Promise((resolve) => setTimeout(resolve, time))
}
/**

View File

@@ -10,34 +10,43 @@
import { useAuthStore } from '@/store'
let isConfirming = false
function handleAuthExpired(content, needTip) {
if (isConfirming || !needTip)
return
isConfirming = true
$dialog.confirm({
title: '提示',
type: 'info',
content,
confirm() {
useAuthStore().logout()
window.$message?.success('已退出登录')
isConfirming = false
},
cancel() {
isConfirming = false
},
})
return false
}
export function resolveResError(code, message, needTip = true) {
export function resolveResError(code, message) {
switch (code) {
case 401:
return handleAuthExpired('登录已过期,是否重新登录?', needTip)
if (isConfirming) return
isConfirming = true
$dialog.confirm({
title: '提示',
type: 'info',
content: '登录已过期,是否重新登录?',
confirm() {
useAuthStore().logout()
window.$message?.success('已退出登录')
isConfirming = false
},
cancel() {
isConfirming = false
},
})
return false
case 11007:
case 11008:
return handleAuthExpired(`${message},是否重新登录?`, needTip)
if (isConfirming) return
isConfirming = true
$dialog.confirm({
title: '提示',
type: 'info',
content: `${message},是否重新登录?`,
confirm() {
useAuthStore().logout()
window.$message?.success('已退出登录')
isConfirming = false
},
cancel() {
isConfirming = false
},
})
return false
case 403:
message = '请求被拒绝'
break
@@ -51,6 +60,5 @@ export function resolveResError(code, message, needTip = true) {
message = message ?? `${code}】: 未知异常!`
break
}
needTip && window.$message?.error(message)
return message
}

View File

@@ -12,7 +12,7 @@ import { setupInterceptors } from './interceptors'
export function createAxios(options = {}) {
const defaultOptions = {
baseURL: import.meta.env.VITE_AXIOS_BASE_URL,
baseURL: '/api',
timeout: 12000,
}
const service = axios.create({

View File

@@ -7,10 +7,29 @@
* Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top
**********************************/
import { useAuthStore } from '@/store'
import { resolveResError } from './helpers'
import { useAuthStore } from '@/store'
export function setupInterceptors(axiosInstance) {
function reqResolve(config) {
// 处理不需要token的请求
if (config.noNeedToken) {
return config
}
const { accessToken } = useAuthStore()
if (accessToken) {
// token: Bearer + xxx
config.headers.Authorization = 'Bearer ' + accessToken
}
return config
}
function reqReject(error) {
return Promise.reject(error)
}
const SUCCESS_CODES = [0, 200]
function resResolve(response) {
const { data, status, config, statusText, headers } = response
@@ -20,51 +39,34 @@ export function setupInterceptors(axiosInstance) {
}
const code = data?.code ?? status
const needTip = config?.needTip !== false
// 根据code处理对应的操作并返回处理后的message
const message = resolveResError(code, data?.message ?? statusText, needTip)
const message = resolveResError(code, data?.message ?? statusText)
//需要错误提醒
!config?.noNeedTip && message && window.$message?.error(message)
return Promise.reject({ code, message, error: data ?? response })
}
return Promise.resolve(data ?? response)
}
async function resReject(error) {
if (!error || !error.response) {
const code = error?.code
/** 根据code处理对应的操作并返回处理后的message */
const message = resolveResError(code, error.message)
window.$message?.error(message)
return Promise.reject({ code, message, error })
}
const { data, status, config } = error.response
const code = data?.code ?? status
const message = resolveResError(code, data?.message ?? error.message)
/** 需要错误提醒 */
!config?.noNeedTip && message && window.$message?.error(message)
return Promise.reject({ code, message, error: error.response?.data || error.response })
}
axiosInstance.interceptors.request.use(reqResolve, reqReject)
axiosInstance.interceptors.response.use(resResolve, resReject)
}
function reqResolve(config) {
// 处理不需要token的请求
if (config.needToken === false) {
return config
}
const { accessToken } = useAuthStore()
if (accessToken) {
// token: Bearer + xxx
config.headers.Authorization = `Bearer ${accessToken}`
}
return config
}
function reqReject(error) {
return Promise.reject(error)
}
async function resReject(error) {
if (!error || !error.response) {
const code = error?.code
/** 根据code处理对应的操作并返回处理后的message */
const message = resolveResError(code, error.message)
return Promise.reject({ code, message, error })
}
const { data, status, config } = error.response
const code = data?.code ?? status
const needTip = config?.needTip !== false
const message = resolveResError(code, data?.message ?? error.message, needTip)
return Promise.reject({ code, message, error: error.response?.data || error.response })
}

View File

@@ -99,27 +99,28 @@ export function isEmpty(val) {
}
/**
* 类似mysql的IFNULL函数
*
* @param {number | boolean | string} val
* @param {number | boolean | string} def
* @returns 第一个参数为null | undefined | '' 则返回第二个参数作为备用值,否则返回第一个参数
* * 类似mysql的IFNULL函数
* * 第一个参数为null/undefined/'' 则返回第二个参数作为备用值,否则返回第一个参数
* @param {Number|Boolean|String} val
* @param {Number|Boolean|String} def
* @returns
*/
export function ifNull(val, def = '') {
return isNullOrWhitespace(val) ? def : val
}
export function isUrl(path) {
const reg = /^https?:\/\/[-\w+&@#/%?=~|!:,.;]+[-\w+&@#/%=~|]$/
const reg =
/(((^https?:(?:\/\/)?)(?:[-;:&=+$,\w]+@)?[A-Za-z0-9.-]+(?::\d+)?|(?:www.|[-;:&=+$,\w]+@)[A-Za-z0-9.-]+)((?:\/[+~%/.\w-_]*)?\??(?:[-+=&;%@.\w_]*)#?(?:[\w]*))?)$/
return reg.test(path)
}
/**
* @param {string} path
* @returns {boolean} 是否是外部链接
* @returns {Boolean}
*/
export function isExternal(path) {
return /^https?:|mailto:|tel:/.test(path)
return /^(https?:|mailto:|tel:)/.test(path)
}
export const isServer = typeof window === 'undefined'

View File

@@ -8,16 +8,15 @@
**********************************/
import * as NaiveUI from 'naive-ui'
import { useAppStore } from '@/store'
import { isNullOrUndef } from '@/utils'
import { useAppStore } from '@/store'
export function setupMessage(NMessage) {
class Message {
static instance
constructor() {
// 单例模式
if (Message.instance)
return Message.instance
if (Message.instance) return Message.instance
Message.instance = this
this.message = {}
this.removeTimer = {}
@@ -38,7 +37,7 @@ export function setupMessage(NMessage) {
showMessage(type, content, option = {}) {
if (Array.isArray(content)) {
return content.forEach(msg => NMessage[type](msg, option))
return content.forEach((msg) => NMessage[type](msg, option))
}
if (!option.key) {
@@ -49,8 +48,7 @@ export function setupMessage(NMessage) {
if (currentMessage) {
currentMessage.type = type
currentMessage.content = content
}
else {
} else {
this.message[option.key] = NMessage[type](content, {
...option,
duration: 0,
@@ -111,7 +109,7 @@ export function setupNaiveDiscreteApi() {
}))
const { message, dialog, notification, loadingBar } = NaiveUI.createDiscreteApi(
['message', 'dialog', 'notification', 'loadingBar'],
{ configProviderProps },
{ configProviderProps }
)
window.$loadingBar = loadingBar

View File

@@ -11,14 +11,14 @@ import { createStorage } from './storage'
const prefixKey = 'vue-naive-admin_'
export function createLocalStorage(option = {}) {
export const createLocalStorage = function (option = {}) {
return createStorage({
prefixKey: option.prefixKey || '',
storage: localStorage,
})
}
export function createSessionStorage(option = {}) {
export const createSessionStorage = function (option = {}) {
return createStorage({
prefixKey: option.prefixKey || '',
storage: sessionStorage,

View File

@@ -35,8 +35,7 @@ class Storage {
getItem(key, def = null) {
const val = this.storage.getItem(this.getKey(key))
if (!val)
return def
if (!val) return def
try {
const data = JSON.parse(val)
const { value, time, expire } = data
@@ -45,9 +44,7 @@ class Storage {
}
this.remove(key)
return def
}
catch (error) {
console.error(error)
} catch (error) {
this.remove(key)
return def
}

View File

@@ -12,24 +12,12 @@
<n-card title="按钮 Button">
<n-space>
<n-button>Default</n-button>
<n-button type="tertiary">
Tertiary
</n-button>
<n-button type="primary">
Primary
</n-button>
<n-button type="info">
Info
</n-button>
<n-button type="success">
Success
</n-button>
<n-button type="warning">
Warning
</n-button>
<n-button type="error">
Error
</n-button>
<n-button type="tertiary">Tertiary</n-button>
<n-button type="primary">Primary</n-button>
<n-button type="info">Info</n-button>
<n-button type="success">Success</n-button>
<n-button type="warning">Warning</n-button>
<n-button type="error">Error</n-button>
</n-space>
</n-card>
@@ -58,18 +46,10 @@
<n-space size="large" mt-30>
<n-card min-w-340 title="通知 Notification">
<n-space>
<n-button @click="notify('info')">
信息
</n-button>
<n-button @click="notify('success')">
成功
</n-button>
<n-button @click="notify('warning')">
警告
</n-button>
<n-button @click="notify('error')">
错误
</n-button>
<n-button @click="notify('info')">信息</n-button>
<n-button @click="notify('success')">成功</n-button>
<n-button @click="notify('warning')">警告</n-button>
<n-button @click="notify('error')">错误</n-button>
</n-space>
</n-card>
@@ -86,9 +66,7 @@
<i v-show="!loading" class="i-mdi:login mr-4" />
登录
</n-button>
<n-button type="error" @click="handleMultiMessage">
多个错误提醒
</n-button>
<n-button type="error" @click="handleMultiMessage">多个错误提醒</n-button>
</n-space>
</n-card>
</n-space>
@@ -97,8 +75,7 @@
<script setup>
import { sleep } from '@/utils'
function handleDelete() {
const handleDelete = function () {
$dialog.confirm({
content: '确认删除?',
confirm() {

View File

@@ -11,9 +11,7 @@
<div w-350>
<n-input v-model:value="inputVal" />
<n-input-number v-model:value="number" mt-30 />
<p mt-20 text-center text-14 color-gray>
右击标签重新加载可重置keep-alive
</p>
<p mt-20 text-center text-14 color-gray>右击标签重新加载可重置keep-alive</p>
</div>
</CommonPage>
</template>

View File

@@ -8,9 +8,7 @@
<template>
<CommonPage show-footer>
<n-button type="primary" @click="openModal1">
打开第一个弹个窗
</n-button>
<n-button type="primary" @click="openModal1">打开第一个弹个窗</n-button>
<MeModal ref="$modal1">
<n-input v-model:value="text" />
</MeModal>
@@ -22,8 +20,8 @@
<script setup>
import { MeModal } from '@/components'
import { useModal } from '@/composables'
import { sleep } from '@/utils'
import { useModal } from '@/composables'
const text = ref('')
const [$modal1, okLoading1] = useModal()

View File

@@ -9,27 +9,16 @@
<template>
<CommonPage show-footer>
<template #title-suffix>
<n-tooltip
placement="bottom"
trigger="hover"
>
<template #trigger>
<a href="https://juejin.cn/post/7394789388154241033" target="_blank" class="ml-12 flex cursor-pointer items-center hover:underline">
<i class="i-simple-icons:juejin text-#1E80FF" />
<span class="ml-4">Unocss 图标</span>
</a>
</template>
点击查看如何使用 Unocss 图标
</n-tooltip>
<n-tag class="ml-12" type="primary">feather图标集 + isme自定义图标</n-tag>
</template>
<ul class="grid grid-cols-[repeat(auto-fill,minmax(160px,1fr))] justify-items-center gap-16">
<ul class="flex flex-wrap justify-between">
<li
v-for="item in icons"
:key="item"
class="w-160 f-c-c flex-col cursor-pointer card-border rounded-12 auto-bg px-12 py-24"
class="m-16 w-160 f-c-c flex-col cursor-pointer rounded-12 px-12 py-24 card-border auto-bg"
@click="copy(`&lt;i class=&quot;${item}&quot; /&gt;`)"
>
<i :class="`${item}?mask`" class="text-28 text-gray-600 hover:bg-primary" />
<i :class="item + '?mask'" class="text-28 text-gray-600 hover:bg-primary" />
<span
class="mt-16 text-center text-14 text-gray-400 hover:color-primary"
@click.stop="copy(item)"
@@ -37,6 +26,13 @@
{{ item }}
</span>
</li>
<li class="mx-16 h-0 w-160"></li>
<li class="mx-16 h-0 w-160"></li>
<li class="mx-16 h-0 w-160"></li>
<li class="mx-16 h-0 w-160"></li>
<li class="mx-16 h-0 w-160"></li>
<li class="mx-16 h-0 w-160"></li>
<li class="mx-16 h-0 w-160"></li>
</ul>
</CommonPage>
</template>
@@ -48,7 +44,6 @@ import icons from 'isme:icons'
const { copy, copied } = useClipboard()
watch(copied, (val) => {
if (val)
$message.success('已复制到剪切板')
val && $message.success('已复制到剪切板')
})
</script>

View File

@@ -22,7 +22,7 @@
</p>
<div mt-20 w-350 f-c-c flex-col>
<div flex flex-wrap justify-around card-border rounded-10 p-10>
<div flex flex-wrap justify-around rounded-10 p-10 card-border>
<div m-20 h-50 w-50 f-c-c rounded-5 p-10 border="1 solid">
<span h-6 w-6 rounded-3 bg-black dark:bg-white />
</div>

View File

@@ -18,9 +18,7 @@
<n-upload-dragger>
<div class="h-150 f-c-c flex-col">
<i class="i-mdi:upload mb-12 text-68 color-primary" />
<n-text class="text-14 color-gray">
点击或者拖动文件到该区域来上传
</n-text>
<n-text class="text-14 color-gray">点击或者拖动文件到该区域来上传</n-text>
</div>
</n-upload-dragger>
</n-upload>
@@ -33,9 +31,7 @@
<n-image width="200" :src="item.url" />
</div>
<n-space class="mt-16" justify="space-evenly">
<n-button dashed type="primary" @click="copy(item.url)">
url
</n-button>
<n-button dashed type="primary" @click="copy(item.url)">url</n-button>
<n-button dashed type="primary" @click="copy(`![${item.fileName}](${item.url})`)">
MD
</n-button>
@@ -57,16 +53,19 @@
<script setup>
import { useClipboard } from '@vueuse/core'
defineOptions({ name: 'ImgUpload' })
const { copy, copied } = useClipboard()
const imgList = reactive([])
const imgList = reactive([
{ url: 'https://cdn.isme.top/images/5c23d52f880511ebb6edd017c2d2eca2.jpg' },
{ url: 'https://cdn.isme.top/images/5c23d52f880511ebb6edd017c2d2eca2.jpg' },
{ url: 'https://cdn.isme.top/images/5c23d52f880511ebb6edd017c2d2eca2.jpg' },
{ url: 'https://cdn.isme.top/images/5c23d52f880511ebb6edd017c2d2eca2.jpg' },
])
watch(copied, (val) => {
if (val)
$message.success('已复制到剪切板')
val && $message.success('已复制到剪切板')
})
function onBeforeUpload({ file }) {

View File

@@ -20,9 +20,7 @@
<n-button v-if="back" type="primary" ghost @click="router.replace(back)">
返回上一页
</n-button>
<n-button type="primary" class="ml-20" @click="router.replace('/')">
返回首页
</n-button>
<n-button type="primary" class="ml-20" @click="router.replace('/')">返回首页</n-button>
</template>
</n-result>
</div>
@@ -37,8 +35,7 @@ const back = history.state.back
if (history.state.from === 'permission-guard') {
delete history.state.from
}
else if (route.query.path) {
} else if (route.query.path) {
router.replace(route.query.path)
}
</script>

View File

@@ -17,9 +17,7 @@
size="large"
>
<template #footer>
<n-button type="primary" ghost @click="replace('/')">
返回首页
</n-button>
<n-button type="primary" ghost @click="replace('/')">返回首页</n-button>
</template>
</n-result>
</div>

View File

@@ -20,19 +20,16 @@
</div>
</div>
<p class="mt-28 text-14 opacity-60">
一个人几乎可以在任何他怀有无限热忱的事情上成功
</p>
<p class="mt-12 text-right text-12 opacity-40">
查尔斯·史考伯
</p>
<p class="mt-28 text-14 opacity-60">一个人几乎可以在任何他怀有无限热忱的事情上成功</p>
<p class="mt-12 text-right text-12 opacity-40"> 查尔斯·史考伯</p>
</n-card>
<n-card class="ml-12 w-70%" title="✨ 欢迎使用 Vue Naive Admin 2.0">
<template #header-extra>
<a
class="text-highlight text-14 text-primary hover:underline hover:opacity-80"
class="text-14 text-primary text-highlight hover:underline hover:opacity-80"
href="https://isme.top"
target="_blank"
@click.prevent="message?.info('官网正在火速开发中...')"
>
isme.top
</a>
@@ -48,7 +45,7 @@
type="primary"
ghost
tag="a"
href="https://isme.top"
href="https://docs.isme.top/web/#/624306705/188522224"
target="__blank"
>
开发文档
@@ -68,7 +65,7 @@
<div class="mt-12 flex">
<n-card class="w-50%" title="💯 特性" segmented>
<template #header-extra>
<span class="text-highlight opacity-90">👏 历经十几次重构和细节打磨</span>
<span class="opacity-90 text-highlight">👏 历经十几次重构和细节打磨</span>
</template>
<ul class="opacity-90">
@@ -159,13 +156,13 @@
</template>
<script setup>
import { BarChart, LineChart, PieChart } from 'echarts/charts'
import { GridComponent, LegendComponent, TooltipComponent } from 'echarts/components'
import { useUserStore } from '@/store'
import * as echarts from 'echarts/core'
import { TooltipComponent, GridComponent, LegendComponent } from 'echarts/components'
import { BarChart, LineChart, PieChart } from 'echarts/charts'
import { UniversalTransition } from 'echarts/features'
import { CanvasRenderer } from 'echarts/renderers'
import VChart from 'vue-echarts'
import { useUserStore } from '@/store'
const userStore = useUserStore()
@@ -250,7 +247,7 @@ const skillOption = {
},
series: [
{
bottom: '12%',
top: '12%',
type: 'pie',
radius: ['35%', '90%'],
avoidLabelOverlap: true,
@@ -283,4 +280,6 @@ const skillOption = {
},
],
}
const message = $message
</script>

View File

@@ -1,6 +1,6 @@
<template>
<AppPage full>
<iframe :src="route.meta.originPath" frameborder="0" class="wh-full" />
<iframe :src="route.meta.originPath" frameborder="0" class="wh-full"></iframe>
</AppPage>
</template>

View File

@@ -9,7 +9,7 @@
import { request } from '@/utils'
export default {
toggleRole: data => request.post('/auth/role/toggle', data),
login: data => request.post('/auth/login', data, { needToken: false }),
toggleRole: (data) => request.post('/auth/role/toggle', data),
login: (data) => request.post('/auth/login', data, { noNeedToken: true }),
getUser: () => request.get('/user/detail'),
}

View File

@@ -9,15 +9,15 @@
<template>
<div class="wh-full flex-col bg-[url(@/assets/images/login_bg.webp)] bg-cover">
<div
class="m-auto max-w-700 min-w-345 f-c-c rounded-8 auto-bg bg-opacity-20 bg-cover p-12 card-shadow"
class="m-auto max-w-700 min-w-345 f-c-c rounded-8 bg-opacity-20 bg-cover p-12 card-shadow auto-bg"
>
<div class="hidden w-380 px-20 py-35 md:block">
<img src="@/assets/images/login_banner.webp" class="w-full" alt="login_banner">
<img src="@/assets/images/login_banner.webp" class="w-full" alt="login_banner" />
</div>
<div class="w-320 flex-col px-20 py-32">
<h2 class="f-c-c text-24 text-#6a6a6a font-normal">
<img src="@/assets/images/logo.png" class="mr-12 h-50">
<img src="@/assets/images/logo.png" class="mr-12 h-50" />
{{ title }}
</h2>
<n-input
@@ -64,7 +64,7 @@
height="40"
class="ml-12 w-80 cursor-pointer"
@click="initCaptcha"
>
/>
</div>
<n-checkbox
@@ -101,10 +101,10 @@
</template>
<script setup>
import { throttle, lStorage } from '@/utils'
import { useStorage } from '@vueuse/core'
import { useAuthStore } from '@/store'
import { lStorage, throttle } from '@/utils'
import api from './api'
import { useAuthStore } from '@/store'
const authStore = useAuthStore()
const router = useRouter()
@@ -118,7 +118,7 @@ const loginInfo = ref({
const captchaUrl = ref('')
const initCaptcha = throttle(() => {
captchaUrl.value = `${import.meta.env.VITE_AXIOS_BASE_URL}/auth/captcha?${Date.now()}`
captchaUrl.value = '/api/auth/captcha?' + Date.now()
}, 500)
const localLoginInfo = lStorage.get('loginInfo')
@@ -138,23 +138,19 @@ const isRemember = useStorage('isRemember', true)
const loading = ref(false)
async function handleLogin(isQuick) {
const { username, password, captcha } = loginInfo.value
if (!username || !password)
return $message.warning('请输入用户名和密码')
if (!isQuick && !captcha)
return $message.warning('请输入验证码')
if (!username || !password) return $message.warning('请输入用户名和密码')
if (!isQuick && !captcha) return $message.warning('请输入验证码')
try {
loading.value = true
$message.loading('正在验证,请稍后...', { key: 'login' })
const { data } = await api.login({ username, password: password.toString(), captcha, isQuick })
if (isRemember.value) {
lStorage.set('loginInfo', { username, password })
}
else {
} else {
lStorage.remove('loginInfo')
}
onLoginSuccess(data)
}
catch (error) {
} catch (error) {
// 10003为验证码错误专属业务码
if (error?.code === 10003) {
// 为防止爆破,验证码错误则刷新验证码
@@ -175,12 +171,10 @@ async function onLoginSuccess(data = {}) {
const path = route.query.redirect
delete route.query.redirect
router.push({ path, query: route.query })
}
else {
} else {
router.push('/')
}
}
catch (error) {
} catch (error) {
console.error(error)
$message.destroy('login')
}

View File

@@ -13,7 +13,7 @@ export default {
getMenuTree: () => request.get('/permission/menu/tree'),
getButtons: ({ parentId }) => request.get(`/permission/button/${parentId}`),
getComponents: () => axios.get(`${import.meta.env.VITE_PUBLIC_PATH}components.json`),
addPermission: data => request.post('/permission', data),
addPermission: (data) => request.post('/permission', data),
savePermission: (id, data) => request.patch(`/permission/${id}`, data),
deletePermission: id => request.delete(`permission/${id}`),
deletePermission: (id) => request.delete(`permission/${id}`),
}

View File

@@ -12,10 +12,10 @@
<h3>菜单</h3>
<div class="flex">
<n-input v-model:value="pattern" placeholder="搜索" clearable />
<NButton class="ml-12" type="primary" @click="handleAdd()">
<n-button class="ml-12" type="primary" @click="handleAdd()">
<i class="i-material-symbols:add mr-4 text-14" />
新增
</NButton>
</n-button>
</div>
<n-tree
@@ -28,8 +28,8 @@
:on-update:selected-keys="onSelect"
key-field="code"
label-field="name"
block-line default-expand-all
block-line
default-expand-all
/>
</n-space>
@@ -38,10 +38,10 @@
</template>
<script setup>
import { NButton } from 'naive-ui'
import { withModifiers } from 'vue'
import api from '../api'
import ResAddOrEdit from './ResAddOrEdit.vue'
import { NButton } from 'naive-ui'
import api from '../api'
defineProps({
treeData: {
@@ -86,7 +86,7 @@ function renderSuffix({ option }) {
size: 'tiny',
onClick: withModifiers(() => handleAdd({ parentId: option.id }), ['stop']),
},
{ default: () => '新增' },
{ default: () => '新增' }
),
h(
@@ -98,7 +98,7 @@ function renderSuffix({ option }) {
style: 'margin-left: 12px;',
onClick: withModifiers(() => handleDelete(option), ['stop']),
},
{ default: () => '删除' },
{ default: () => '删除' }
),
]
}
@@ -113,9 +113,7 @@ function handleDelete(item) {
$message.success('删除成功', { key: 'deleteMenu' })
emit('refresh')
emit('update:currentMenu', null)
}
catch (error) {
console.error(error)
} catch (error) {
$message.destroy('deleteMenu')
}
},

View File

@@ -99,12 +99,8 @@
<QuestionLabel label="显示状态" content="控制是否在菜单栏显示,不影响路由注册" />
</template>
<n-switch v-model:value="modalForm.show">
<template #checked>
显示
</template>
<template #unchecked>
隐藏
</template>
<template #checked>显示</template>
<template #unchecked>隐藏</template>
</n-switch>
</n-form-item-gi>
<n-form-item-gi :span="12" path="enable">
@@ -115,12 +111,8 @@
/>
</template>
<n-switch v-model:value="modalForm.enable">
<template #checked>
启用
</template>
<template #unchecked>
禁用
</template>
<template #checked>启用</template>
<template #unchecked>禁用</template>
</n-switch>
</n-form-item-gi>
<n-form-item-gi v-if="modalForm.type === 'MENU'" :span="12" path="keepAlive">
@@ -131,12 +123,8 @@
/>
</template>
<n-switch v-model:value="modalForm.keepAlive">
<template #checked>
</template>
<template #unchecked>
</template>
<template #checked></template>
<template #unchecked></template>
</n-switch>
</n-form-item-gi>
<n-form-item-gi
@@ -159,12 +147,12 @@
</template>
<script setup>
import icons from 'isme:icons'
import pagePathes from 'isme:page-pathes'
import { MeModal } from '@/components'
import QuestionLabel from './QuestionLabel.vue'
import { useForm, useModal } from '@/composables'
import api from '../api'
import QuestionLabel from './QuestionLabel.vue'
import pagePathes from 'isme:page-pathes'
import icons from 'isme:icons'
const props = defineProps({
menus: {
@@ -177,10 +165,10 @@ const emit = defineEmits(['refresh'])
const menuOptions = computed(() => {
return [{ name: '根菜单', id: '', children: props.menus || [] }]
})
const componentOptions = pagePathes.map(path => ({ label: path, value: path }))
const iconOptions = icons.map(item => ({
const componentOptions = pagePathes.map((path) => ({ label: path, value: path }))
const iconOptions = icons.map((item) => ({
label: () =>
h('span', { class: 'flex items-center' }, [h('i', { class: `${item} text-18 mr-8` }), item]),
h('span', { class: 'flex items-center' }, [h('i', { class: item + ' text-18 mr-8' }), item]),
value: item,
}))
const layoutOptions = [
@@ -215,20 +203,17 @@ async function onSave() {
okLoading.value = true
try {
let newFormData
if (!modalForm.value.parentId)
modalForm.value.parentId = null
if (!modalForm.value.parentId) modalForm.value.parentId = null
if (modalAction.value === 'add') {
const res = await api.addPermission(modalForm.value)
newFormData = res.data
}
else if (modalAction.value === 'edit') {
} else if (modalAction.value === 'edit') {
await api.savePermission(modalForm.value.id, modalForm.value)
}
okLoading.value = false
$message.success('保存成功')
emit('refresh', modalAction.value === 'add' ? newFormData : modalForm.value)
}
catch (error) {
} catch (error) {
console.error(error)
okLoading.value = false
return false

View File

@@ -11,7 +11,7 @@
<div class="flex">
<n-spin size="small" :show="treeLoading">
<MenuTree
v-model:current-menu="currentMenu"
v-model:currentMenu="currentMenu"
class="w-320 shrink-0"
:tree-data="treeData"
@refresh="initData"
@@ -21,21 +21,15 @@
<div class="ml-40 w-0 flex-1">
<template v-if="currentMenu">
<div class="flex justify-between">
<h3 class="mb-12">
{{ currentMenu.name }}
</h3>
<NButton size="small" type="primary" @click="handleEdit(currentMenu)">
<h3 class="mb-12">{{ currentMenu.name }}</h3>
<n-button size="small" type="primary" @click="handleEdit(currentMenu)">
<i class="i-material-symbols:edit-outline mr-4 text-14" />
编辑
</NButton>
</n-button>
</div>
<n-descriptions label-placement="left" bordered :column="2">
<n-descriptions-item label="编码">
{{ currentMenu.code }}
</n-descriptions-item>
<n-descriptions-item label="名称">
{{ currentMenu.name }}
</n-descriptions-item>
<n-descriptions-item label="编码">{{ currentMenu.code }}</n-descriptions-item>
<n-descriptions-item label="名称">{{ currentMenu.name }}</n-descriptions-item>
<n-descriptions-item label="路由地址">
{{ currentMenu.path ?? '--' }}
</n-descriptions-item>
@@ -67,13 +61,11 @@
</n-descriptions>
<div class="mt-32 flex justify-between">
<h3 class="mb-12">
按钮
</h3>
<NButton size="small" type="primary" @click="handleAddBtn">
<h3 class="mb-12">按钮</h3>
<n-button size="small" type="primary" @click="handleAddBtn">
<i class="i-fe:plus mr-4 text-14" />
新增
</NButton>
</n-button>
</div>
<MeCrud
@@ -82,7 +74,7 @@
:scroll-x="-1"
:get-data="api.getButtons"
:query-items="{ parentId: currentMenu.id }"
/>
></MeCrud>
</template>
<n-empty v-else class="h-450 f-c-c" size="large" description="请选择菜单查看详情" />
</div>
@@ -93,15 +85,13 @@
<script setup>
import { NButton, NSwitch } from 'naive-ui'
import { MeCrud } from '@/components'
import api from './api'
import MenuTree from './components/MenuTree.vue'
import ResAddOrEdit from './components/ResAddOrEdit.vue'
import { MeCrud } from '@/components'
import api from './api'
const treeData = ref([])
const treeLoading = ref(false)
const $table = ref(null)
const currentMenu = ref(null)
async function initData(data) {
if (data?.type === 'BUTTON') {
$table.value.handleSearch()
@@ -112,16 +102,17 @@ async function initData(data) {
treeData.value = res?.data || []
treeLoading.value = false
if (data)
currentMenu.value = data
if (data) currentMenu.value = data
}
initData()
const currentMenu = ref(null)
const modalRef = ref(null)
function handleEdit(item = {}) {
modalRef.value?.handleOpen({
action: 'edit',
title: `编辑菜单 - ${item.name}`,
title: '编辑菜单 - ' + item.name,
row: item,
okText: '保存',
})
@@ -133,7 +124,7 @@ const btnsColumns = [
{
title: '状态',
key: 'enable',
render: row =>
render: (row) =>
h(
NSwitch,
{
@@ -146,7 +137,7 @@ const btnsColumns = [
{
checked: () => '启用',
unchecked: () => '停用',
},
}
),
},
{
@@ -168,7 +159,7 @@ const btnsColumns = [
{
default: () => '编辑',
icon: () => h('i', { class: 'i-material-symbols:edit-outline text-14' }),
},
}
),
h(
@@ -182,20 +173,20 @@ const btnsColumns = [
{
default: () => '删除',
icon: () => h('i', { class: 'i-material-symbols:delete-outline text-14' }),
},
}
),
]
},
},
]
const $table = ref(null)
watch(
() => currentMenu.value,
async (v) => {
await nextTick()
if (v)
$table.value.handleSearch()
},
if (v) $table.value.handleSearch()
}
)
function handleAddBtn() {
@@ -210,7 +201,7 @@ function handleAddBtn() {
function handleEditBtn(row) {
modalRef.value?.handleOpen({
action: 'edit',
title: `编辑按钮 - ${row.name}`,
title: '编辑按钮 - ' + row.name,
row,
okText: '保存',
})
@@ -229,9 +220,7 @@ function handleDeleteBtn(id) {
$message.success('删除成功')
$table.value.handleSearch()
d.loading = false
}
catch (error) {
console.error(error)
} catch (error) {
d.loading = false
}
},
@@ -247,9 +236,7 @@ async function handleEnable(item) {
$message.success('操作成功')
$table.value?.handleSearch()
item.enableLoading = false
}
catch (error) {
console.error(error)
} catch (error) {
item.enableLoading = false
}
}

View File

@@ -9,10 +9,10 @@
import { request } from '@/utils'
export default {
create: data => request.post('/role', data),
create: (data) => request.post('/role', data),
read: (params = {}) => request.get('/role/page', { params }),
update: data => request.patch(`/role/${data.id}`, data),
delete: id => request.delete(`/role/${id}`),
update: (data) => request.patch(`/role/${data.id}`, data),
delete: (id) => request.delete(`/role/${id}`),
getAllPermissionTree: () => request.get('/permission/tree'),
getAllUsers: (params = {}) => request.get('/user', { params }),

View File

@@ -9,10 +9,10 @@
<template>
<CommonPage>
<template #action>
<NButton type="primary" @click="handleAdd()">
<n-button type="primary" @click="handleAdd()">
<i class="i-material-symbols:add mr-4 text-18" />
新增角色
</NButton>
</n-button>
</template>
<MeCrud
@@ -74,20 +74,17 @@
:data="permissionTree"
:checked-keys="modalForm.permissionIds"
:on-update:checked-keys="(keys) => (modalForm.permissionIds = keys)"
checkable check-on-click default-expand-all
default-expand-all
checkable
check-on-click
class="cus-scroll max-h-200 w-full"
/>
</n-form-item>
<n-form-item label="状态" path="enable">
<NSwitch v-model:value="modalForm.enable">
<template #checked>
启用
</template>
<template #unchecked>
停用
</template>
</NSwitch>
<n-switch v-model:value="modalForm.enable">
<template #checked>启用</template>
<template #unchecked>停用</template>
</n-switch>
</n-form-item>
</n-form>
</MeModal>
@@ -96,7 +93,7 @@
<script setup>
import { NButton, NSwitch } from 'naive-ui'
import { MeCrud, MeModal, MeQueryItem } from '@/components'
import { MeCrud, MeQueryItem, MeModal } from '@/components'
import { useCrud } from '@/composables'
import api from './api'
@@ -112,23 +109,13 @@ onMounted(() => {
$table.value?.handleSearch()
})
const { modalRef, modalFormRef, modalAction, modalForm, handleAdd, handleDelete, handleEdit }
= useCrud({
name: '角色',
doCreate: api.create,
doDelete: api.delete,
doUpdate: api.update,
initForm: { enable: true },
refresh: (_, keepCurrentPage) => $table.value?.handleSearch(keepCurrentPage),
})
const columns = [
{ title: '角色名', key: 'name' },
{ title: '角色编码', key: 'code' },
{
title: '状态',
key: 'enable',
render: row =>
render: (row) =>
h(
NSwitch,
{
@@ -142,7 +129,7 @@ const columns = [
{
checked: () => '启用',
unchecked: () => '停用',
},
}
),
},
{
@@ -165,7 +152,7 @@ const columns = [
{
default: () => '分配用户',
icon: () => h('i', { class: 'i-fe:user-plus text-14' }),
},
}
),
h(
NButton,
@@ -179,7 +166,7 @@ const columns = [
{
default: () => '编辑',
icon: () => h('i', { class: 'i-material-symbols:edit-outline text-14' }),
},
}
),
h(
@@ -194,7 +181,7 @@ const columns = [
{
default: () => '删除',
icon: () => h('i', { class: 'i-material-symbols:delete-outline text-14' }),
},
}
),
]
},
@@ -208,13 +195,21 @@ async function handleEnable(row) {
row.enableLoading = false
$message.success('操作成功')
$table.value?.handleSearch()
}
catch (error) {
console.error(error)
} catch (error) {
row.enableLoading = false
}
}
const { modalRef, modalFormRef, modalAction, modalForm, handleAdd, handleDelete, handleEdit } =
useCrud({
name: '角色',
doCreate: api.create,
doDelete: api.delete,
doUpdate: api.update,
initForm: { enable: true },
refresh: () => $table.value?.handleSearch(),
})
const permissionTree = ref([])
api.getAllPermissionTree().then(({ data = [] }) => (permissionTree.value = data))
</script>

View File

@@ -9,17 +9,15 @@
<template>
<CommonPage back>
<template #title-suffix>
<NTag class="ml-12" type="warning">
{{ route.query.roleName }}
</NTag>
<n-tag class="ml-12" type="warning">{{ route.query.roleName }}</n-tag>
</template>
<template #action>
<div class="flex items-center">
<NButton :disabled="!userIds.length" type="error" @click="handleBatchRemove()">
<n-button :disabled="!userIds.length" type="error" @click="handleBatchRemove()">
<i v-if="userIds.length" class="i-material-symbols:delete-outline mr-4 text-18" />
批量取消授权
</NButton>
<NButton
</n-button>
<n-button
class="ml-12"
:disabled="!userIds.length"
type="primary"
@@ -27,7 +25,7 @@
>
<i v-if="userIds.length" class="i-line-md:confirm-circle mr-4 text-18" />
批量授权
</NButton>
</n-button>
</div>
</template>
@@ -68,10 +66,10 @@
<script setup>
import { NAvatar, NButton, NSwitch, NTag } from 'naive-ui'
import { h } from 'vue'
import { MeCrud, MeQueryItem } from '@/components'
import { formatDateTime } from '@/utils'
import api from './api'
import { h } from 'vue'
defineOptions({ name: 'RoleUser' })
const route = useRoute()
@@ -113,8 +111,8 @@ const columns = [
h(
NTag,
{ type: 'success', style: index > 0 ? 'margin-left: 8px;' : '' },
{ default: () => item.name },
),
{ default: () => item.name }
)
)
}
return '暂无角色'
@@ -124,14 +122,14 @@ const columns = [
title: '性别',
key: 'gender',
width: 80,
render: ({ gender }) => genders.find(item => gender === item.value)?.label ?? '',
render: ({ gender }) => genders.find((item) => gender === item.value)?.label ?? '',
},
{
title: '创建时间',
key: 'createDate',
width: 180,
render(row) {
return h('span', formatDateTime(row.createTime))
return h('span', formatDateTime(row['createTime']))
},
},
{
@@ -139,7 +137,7 @@ const columns = [
key: 'enable',
width: 100,
render: row =>
render: (row) =>
h(
NSwitch,
{
@@ -150,7 +148,7 @@ const columns = [
{
checked: () => '启用',
unchecked: () => '停用',
},
}
),
},
{
@@ -161,7 +159,7 @@ const columns = [
fixed: 'right',
hideInExcel: true,
render(row) {
return row.roles?.some(item => item.id === +route.params.roleId)
return row.roles?.some((item) => item.id === +route.params.roleId)
? h(
NButton,
{
@@ -173,7 +171,7 @@ const columns = [
{
default: () => '取消授权',
icon: () => h('i', { class: 'i-material-symbols:delete-outline text-14' }),
},
}
)
: h(
NButton,
@@ -186,7 +184,7 @@ const columns = [
{
default: () => '授权',
icon: () => h('i', { class: 'i-line-md:confirm-circle text-14' }),
},
}
)
},
},
@@ -199,10 +197,8 @@ function onChecked(rowKeys) {
function handleBatchAdd(ids = userIds.value) {
const roleId = route.params.roleId
if (!roleId)
return $message.error('角色异常,请重新选择角色')
if (!ids.length)
return $message.error('请先选择用户')
if (!roleId) return $message.error('角色异常,请重新选择角色')
if (!ids.length) return $message.error('请先选择用户')
$dialog.confirm({
content: `确认分配【${route.query.roleName}】?`,
async confirm() {
@@ -213,10 +209,8 @@ function handleBatchAdd(ids = userIds.value) {
}
function handleBatchRemove(ids = userIds.value) {
const roleId = route.params.roleId
if (!roleId)
return $message.error('角色异常,请重新选择角色')
if (!ids.length)
return $message.error('请先选择用户')
if (!roleId) return $message.error('角色异常,请重新选择角色')
if (!ids.length) return $message.error('请先选择用户')
$dialog.confirm({
content: `确认取消分配【${route.query.roleName}】?`,
async confirm() {

View File

@@ -9,10 +9,10 @@
import { request } from '@/utils'
export default {
create: data => request.post('/user', data),
create: (data) => request.post('/user', data),
read: (params = {}) => request.get('/user', { params }),
update: data => request.patch(`/user/${data.id}`, data),
delete: id => request.delete(`/user/${id}`),
update: (data) => request.patch(`/user/${data.id}`, data),
delete: (id) => request.delete(`/user/${id}`),
resetPwd: (id, data) => request.patch(`/user/password/reset/${id}`, data),
getAllRoles: () => request.get('/role?enable=1'),

Some files were not shown because too many files have changed in this diff Show More