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

32 Commits

Author SHA1 Message Date
zclzone
fd9480e92f fix: logo 宽高异常 2024-03-12 09:38:08 +08:00
大脸怪
0520cd015a Merge pull request #73 from hanfengcan/2.x
feat: 增加必要的校验提示
2024-03-11 21:54:59 +08:00
hanfengcan
f5a26c32e9 U:增加必要的校验提示 2024-03-10 18:21:04 +08:00
zclzone
886ef9e11c feat: logo背景色根据主题色自动调整 2024-03-04 16:33:35 +08:00
zclzone
7d0a17b2b5 chore: update deps, close #I93HBV 2024-02-29 17:23:00 +08:00
zclzone
fa4967efc3 chore: 修改eslint设配置 2024-02-29 17:01:43 +08:00
zclzone
207150623e fix: 修复创建路由没有考虑 base 路径问题, close #70 2024-02-21 10:04:21 +08:00
zclzone
6981692c54 mod: appStore持久化存储改用sessionStorage 2024-02-21 09:06:04 +08:00
zclzone
b64e1c7595 chore: 移除 vite-plugin-vue-devtools 2024-02-11 17:00:25 +08:00
zclzone
efcbe3bea4 mod: 移除多余图标 2024-02-11 16:59:27 +08:00
zclzone
2c808c6d8b chore: 修改npm淘宝镜像地址 2024-02-01 11:19:32 +08:00
zclzone
447db11c52 chore: update eslintConfig 2024-02-01 10:24:52 +08:00
zclzone
a3d0e863cc fix: 修复本地代理apiFox云端mock接口不通问题 2024-01-30 22:51:50 +08:00
zclzone
a2827e4c0d Merge branch '2.x' of https://gitee.com/isme-admin/vue-naive-admin into 2.x 2024-01-25 15:54:05 +08:00
zclzone
f888c2fbfd fix: 移除跳转role-select页面逻辑 2024-01-25 15:53:35 +08:00
zclzone
1c37a38b92 chore: update deps 2024-01-21 18:06:25 +08:00
zclzone
f2eb40357d fix: submit导致reload问题 2024-01-21 18:01:05 +08:00
zclzone
58de3c1ad6 perf: 优化 crud query 2024-01-21 17:52:50 +08:00
zclzone
567e306a5c Merge branch '2.x' of https://github.com/zclzone/vue-naive-admin into 2.x 2024-01-21 17:26:00 +08:00
zclzone
065c6b50c6 fix: enter -> handleQuery 2024-01-21 16:53:43 +08:00
大脸怪
95a1ef654c !2 update src/views/pms/user/index.vue.
Merge pull request !2 from aiden/N/A
2024-01-21 08:51:51 +00:00
大脸怪
346da8772a Update README.md 2024-01-13 18:18:44 +08:00
zclzone
a6773cbfec feat: 增加MeModal拖拽功能 2024-01-13 17:43:48 +08:00
aiden
4c337d3aa8 update src/views/pms/user/index.vue.
修正用户管理回车不执行搜索的bug

Signed-off-by: aiden <792000767@qq.com>
2024-01-08 11:33:47 +00:00
zclzone
2c9604a829 fix: 解决vite5循环引用导致热更新失效问题 2024-01-06 18:47:40 +08:00
大脸怪
c4fd0459ab Update README.md 2024-01-04 09:12:51 +08:00
zclzone
aa92455dbb chore: update deps about vite & vue 2024-01-02 22:29:01 +08:00
zclzone
857381471e chore: update deps about unocss 2024-01-02 22:04:38 +08:00
zclzone
b780113f18 fix: message判空 2023-12-29 15:59:04 +08:00
zclzone
fe4bbded53 fix: 重置空字符 -> null 2023-12-28 17:09:12 +08:00
zclzone
d801cf28cc fix: 报错未及时清除loading message问题 2023-12-23 20:31:52 +08:00
大脸怪
334174d442 Update README.md 2023-12-21 14:26:12 +08:00
38 changed files with 2544 additions and 4484 deletions

2
.npmrc
View File

@@ -1,2 +1,2 @@
registry=https://registry.npm.taobao.org registry=https://registry.npmmirror.com
shamefully-hoist=true shamefully-hoist=true

View File

@@ -18,5 +18,8 @@
"editor.codeActionsOnSave": { "editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit" "source.fixAll.eslint": "explicit"
}, },
"eslint.validate": ["javascript", "typescript", "javascriptreact", "typescriptreact", "vue"] "eslint.validate": ["javascript", "typescript", "javascriptreact", "typescriptreact", "vue"],
"eslint.options": {
"overrideConfigFile": "package.json"
}
} }

View File

@@ -12,7 +12,7 @@ Vue Naive Admin 是一款极简风格的后台管理模板,包含前后端解
## 设计理念 ## 设计理念
Vue Naive Admin 2022年2月开始开源从 1.0 到现在的 2.0,一直秉持着`简单即正义`的理念,旨在帮助中小企业、在校大学生及个人开发者快速上手开发后台管理项目,为了降低使用者的学习成本,没有使用看似主流的 TypeScript前端这也使得 Vue Naive Admin 成为了市面上少有的 `使用 JavaScript 的 Vue3 后台管理模板`,而且还算优秀,得到了大量朋友的认可和喜爱,截至 2023-11-17 github `1.1k+` stargitee `260+` star Vue Naive Admin 2022年2月开始开源从 1.0 到现在的 2.0,一直秉持着`简单即正义`的理念,旨在帮助中小企业、在校大学生及个人开发者快速上手开发后台管理项目,为了降低使用者的学习成本,没有使用看似主流的 TypeScript前端这也使得 Vue Naive Admin 成为了市面上少有的 `使用 JavaScript 的 Vue3 后台管理模板`,而且还算优秀,得到了大量朋友的认可和喜爱。
## 特性 ## 特性
@@ -52,6 +52,7 @@ Vue Naive Admin 提供一套后端代码,技术栈使用 Nestjs + TypeOrm + My
- 源码-github: [isme-nest-serve | github](https://github.com/zclzone/isme-nest-serve) - 源码-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) - 源码-gitee: [isme-nest-serve | gitee](https://gitee.com/isme-admin/isme-nest-serve)
- 接口文档: [apidoc | isme-nest-serve](https://apifox.com/apidoc/shared-ff4a4d32-c0d1-4caf-b0ee-6abc130f734a)
## 版权说明 ## 版权说明
@@ -64,4 +65,5 @@ Vue Naive Admin 提供一套后端代码,技术栈使用 Nestjs + TypeOrm + My
## 其他已对接本项目的后端项目 ## 其他已对接本项目的后端项目
- [isme-java-serve](https://github.com/DHBin/isme-java-serve): 一个轻量级的Java后端服务基于SpringBoot、MybatisPlus、SaToken、MapStruct等实现已对接 Vue Naive Admin 2.0。 - [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。

View File

@@ -1,4 +1,4 @@
<!doctype html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
@@ -11,7 +11,7 @@
<div id="app"> <div id="app">
<!-- 白屏时的loading效果 --> <!-- 白屏时的loading效果 -->
<div class="loading-container"> <div class="loading-container">
<img src="/resource/logo.png" alt="logo" height="128" /> <img src="/resource/logo.png" alt="logo" />
<div class="loading-spin__container"> <div class="loading-spin__container">
<div class="loading-spin"> <div class="loading-spin">
<div class="left-0 top-0 loading-spin-item"></div> <div class="left-0 top-0 loading-spin-item"></div>

View File

@@ -10,37 +10,36 @@
"lint:fix": "eslint --fix --ext .js,.vue ." "lint:fix": "eslint --fix --ext .js,.vue ."
}, },
"dependencies": { "dependencies": {
"@iconify/json": "^2.2.129", "@vueuse/core": "^10.9.0",
"@iconify/utils": "^2.1.11", "axios": "^1.6.7",
"@unocss/eslint-config": "^0.58.0",
"@unocss/preset-rem-to-px": "^0.58.0",
"@vueuse/core": "^10.5.0",
"axios": "^1.5.1",
"dayjs": "^1.11.10", "dayjs": "^1.11.10",
"echarts": "^5.4.3", "echarts": "^5.5.0",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"naive-ui": "^2.35.0", "naive-ui": "^2.38.1",
"pinia": "^2.1.7", "pinia": "^2.1.7",
"pinia-plugin-persistedstate": "^3.2.0", "pinia-plugin-persistedstate": "^3.2.1",
"sass": "^1.69.3", "vue": "^3.4.21",
"unocss": "^0.58.0", "vue-echarts": "^6.6.9",
"vue": "^3.3.11", "vue-router": "^4.3.0",
"vue-echarts": "^6.6.1",
"vue-router": "^4.2.5",
"xlsx": "^0.18.5" "xlsx": "^0.18.5"
}, },
"devDependencies": { "devDependencies": {
"@vitejs/plugin-vue": "^4.5.2", "@iconify/json": "^2.2.188",
"@iconify/utils": "^2.1.22",
"@unocss/eslint-config": "^0.58.5",
"@unocss/preset-rem-to-px": "^0.58.5",
"@vitejs/plugin-vue": "^5.0.4",
"@zclzone/eslint-config": "^0.0.5", "@zclzone/eslint-config": "^0.0.5",
"esno": "^0.17.0", "esno": "^4.0.0",
"fs-extra": "^11.1.1", "fs-extra": "^11.2.0",
"glob": "^10.3.10", "glob": "^10.3.10",
"rollup-plugin-visualizer": "^5.9.2", "rollup-plugin-visualizer": "^5.12.0",
"unplugin-auto-import": "^0.16.6", "sass": "^1.71.1",
"unplugin-vue-components": "^0.25.2", "unocss": "^0.58.5",
"vite": "^5.0.7", "unplugin-auto-import": "^0.17.5",
"vite-plugin-simple-html": "^0.1.1", "unplugin-vue-components": "^0.26.0",
"vite-plugin-vue-devtools": "1.0.0-rc.7" "vite": "^5.1.4",
"vite-plugin-simple-html": "^0.1.2"
}, },
"eslintConfig": { "eslintConfig": {
"extends": [ "extends": [
@@ -48,13 +47,10 @@
"@unocss", "@unocss",
".eslint-global-variables.json" ".eslint-global-variables.json"
], ],
"rules": { "parserOptions": {
"no-unused-vars": [ "ecmaFeatures": {
"error", "jsx": true
{ }
"varsIgnorePattern": "^_"
}
]
} }
} }
} }

6396
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,21 +0,0 @@
<svg
t="1702480351321"
class="icon"
viewBox="0 0 1024 1024"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
p-id="11122"
width="200"
height="200"
>
<path
d="M509.9008 519.8336m-450.816 0a450.816 450.816 0 1 0 901.632 0 450.816 450.816 0 1 0-901.632 0Z"
fill="#C65EDB"
p-id="11123"
></path>
<path
d="M798.1568 512.512l-113.3056-78.3872a47.4112 47.4112 0 0 1-20.4288-39.0656l0.3584-137.7792c0.1024-39.2704-44.9024-61.5936-76.0832-37.7856l-109.568 83.5072a47.2832 47.2832 0 0 1-43.4688 7.3216l-130.9184-42.9056c-37.3248-12.2368-72.448 23.6544-59.4432 60.7232l45.568 129.9968A47.3088 47.3088 0 0 1 284.416 501.76l-81.2544 111.2576c-23.1424 31.6928 0.1024 76.2368 39.3728 75.3152l137.728-3.1744a47.3344 47.3344 0 0 1 39.4752 19.6096l80.6912 111.6672c22.9888 31.8464 72.4992 23.4496 83.7632-14.1824l37.9392-126.6176 126.5664 118.272a27.648 27.648 0 0 0 17.7664 7.4752c7.8848 0.3584 15.872-2.6112 21.7088-8.8064a27.91936 27.91936 0 0 0-1.3312-39.4752l-124.8768-116.6848 123.8016-39.8848c37.376-11.9808 44.6976-61.6448 12.3904-84.0192z m-389.6832-6.5024l-42.8032 77.824a27.86816 27.86816 0 0 1-37.9392 11.008 27.93984 27.93984 0 0 1-11.008-37.9392l40.0384-72.7552-19.0464-63.6928c-4.4032-14.7968 3.9936-30.3616 18.7392-34.7648 14.7456-4.4032 30.3616 3.9936 34.7648 18.7392l20.6848 69.2224c3.2256 10.752 1.9968 22.528-3.4304 32.3584z"
fill="#FFFFFF"
p-id="11124"
></path>
</svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

BIN
src/assets/images/isme.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -0,0 +1,12 @@
<!--------------------------------
- @Author: Ronnie Zhang
- @LastEditor: Ronnie Zhang
- @LastEditTime: 2024/03/04 16:09:47
- @Email: zclzone@outlook.com
- Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top
--------------------------------->
<template>
<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

@@ -7,25 +7,22 @@
---------------------------------> --------------------------------->
<template> <template>
<AppCard <AppCard v-if="$slots.default" bordered bg="#fafafc dark:black" class="mb-30 min-h-60 rounded-4">
v-if="$slots.default" <form class="flex justify-between p-16" @submit.prevent="handleSearch()">
bordered <n-space wrap :size="[32, 16]">
bg="#fafafc dark:black" <slot />
class="mb-30 min-h-60 flex justify-between rounded-4 p-16" </n-space>
> <div class="flex-shrink-0">
<n-space wrap :size="[32, 16]"> <n-button ghost type="primary" @click="handleReset">
<slot /> <i class="i-fe:rotate-ccw mr-4" />
</n-space> 重置
<div class="flex-shrink-0"> </n-button>
<n-button ghost type="primary" @click="handleReset"> <n-button attr-type="submit" class="ml-20" type="primary">
<i class="i-fe:rotate-ccw mr-4" /> <i class="i-fe:search mr-4" />
重置 搜索
</n-button> </n-button>
<n-button class="ml-20" type="primary" @click="handleSearch"> </div>
<i class="i-fe:search mr-4" /> </form>
搜索
</n-button>
</div>
</AppCard> </AppCard>
<n-data-table <n-data-table
@@ -129,7 +126,7 @@ function handleSearch() {
async function handleReset() { async function handleReset() {
const queryItems = { ...props.queryItems } const queryItems = { ...props.queryItems }
for (const key in queryItems) { for (const key in queryItems) {
queryItems[key] = '' queryItems[key] = null
} }
emit('update:queryItems', { ...queryItems, ...initQuery }) emit('update:queryItems', { ...queryItems, ...initQuery })
await nextTick() await nextTick()

View File

@@ -1,7 +1,7 @@
<!-------------------------------- <!--------------------------------
- @Author: Ronnie Zhang - @Author: Ronnie Zhang
- @LastEditor: Ronnie Zhang - @LastEditor: Ronnie Zhang
- @LastEditTime: 2023/12/16 18:50:02 - @LastEditTime: 2024/01/13 17:41:38
- @Email: zclzone@outlook.com - @Email: zclzone@outlook.com
- Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top - Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top
---------------------------------> --------------------------------->
@@ -9,17 +9,17 @@
<template> <template>
<n-modal <n-modal
v-model:show="show" v-model:show="show"
class="modal-box"
:style="{ width: modalOptions.width, ...modalOptions.modalStyle }" :style="{ width: modalOptions.width, ...modalOptions.modalStyle }"
:preset="undefined" :preset="undefined"
size="huge" size="huge"
:bordered="false" :bordered="false"
@after-leave="onAfterLeave"
> >
<n-card <n-card :style="modalOptions.contentStyle" :closable="modalOptions.closable" @close="close()">
:title="modalOptions.title" <template #header>
:style="modalOptions.contentStyle" <header class="modal-header">{{ modalOptions.title }}</header>
:closable="modalOptions.closable" </template>
@close="close()"
>
<slot></slot> <slot></slot>
<!-- 底部按钮 --> <!-- 底部按钮 -->
@@ -45,6 +45,7 @@
</template> </template>
<script setup> <script setup>
import { initDrag } from './utils'
const props = defineProps({ const props = defineProps({
width: { width: {
type: String, type: String,
@@ -101,12 +102,17 @@ const show = ref(false)
const modalOptions = ref({}) const modalOptions = ref({})
// 打开模态框 // 打开模态框
function open(options = {}) { async function open(options = {}) {
// 将props和options合并赋值给modalOptions // 将props和options合并赋值给modalOptions
modalOptions.value = { ...props, ...options } modalOptions.value = { ...props, ...options }
// 将show的值设置为true // 将show的值设置为true
show.value = true show.value = true
await nextTick()
initDrag(
Array.prototype.at.call(document.querySelectorAll('.modal-header'), -1),
Array.prototype.at.call(document.querySelectorAll('.modal-box'), -1)
)
} }
// 定义一个close函数用于关闭模态框 // 定义一个close函数用于关闭模态框
@@ -149,6 +155,14 @@ async function handleCancel(data) {
} }
} }
async function onAfterLeave() {
await nextTick()
initDrag(
Array.prototype.at.call(document.querySelectorAll('.modal-header'), -1),
Array.prototype.at.call(document.querySelectorAll('.modal-box'), -1)
)
}
const okLoading = computed({ const okLoading = computed({
get() { get() {
return !!modalOptions.value?.okLoading return !!modalOptions.value?.okLoading

View File

@@ -0,0 +1,94 @@
/**********************************
* @Author: Ronnie Zhang
* @LastEditor: Ronnie Zhang
* @LastEditTime: 2024/01/13 17:41:26
* @Email: zclzone@outlook.com
* Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top
**********************************/
// 获取元素的CSS样式
function getCss(element, key) {
return element.currentStyle
? element.currentStyle[key]
: window.getComputedStyle(element, null)[key]
}
const params = {
left: 0,
top: 0,
currentX: 0,
currentY: 0,
flag: false,
}
// 初始化拖拽
export function initDrag(bar, box) {
if (!bar || !box) return
const screenWidth = document.body.clientWidth // 页面宽度
const screenHeight = document.documentElement.clientHeight // 页面可见区域高度
const dragDomWidth = box.offsetWidth // 盒子宽度
const dragDomHeight = box.offsetHeight // 盒子高度
const minDomLeft = box.offsetLeft // 盒子相对于父元素的左偏移量
const minDomTop = box.offsetTop // 盒子相对于父元素的上偏移量
const maxDragDomLeft = screenWidth - minDomLeft - dragDomWidth // 盒子在水平方向上可拖拽的最大距离
const maxDragDomTop = screenHeight - minDomTop - dragDomHeight // 盒子在垂直方向上可拖拽的最大距离
if (getCss(box, 'left') !== 'auto') {
params.left = getCss(box, 'left')
}
if (getCss(box, 'top') !== 'auto') {
params.top = getCss(box, 'top')
}
// 设置触发拖动元素的鼠标样式为移动图标
bar.style.cursor = 'move'
// 鼠标按下事件处理函数
bar.onmousedown = function (e) {
params.flag = true // 设置拖拽标志为true
e.preventDefault() // 阻止默认事件
params.currentX = e.clientX // 鼠标当前位置的X坐标
params.currentY = e.clientY // 鼠标当前位置的Y坐标
}
document.onmouseup = function () {
params.flag = false // 设置拖拽标志为false
if (getCss(box, 'left') !== 'auto') {
params.left = getCss(box, 'left')
}
if (getCss(box, 'top') !== 'auto') {
params.top = getCss(box, 'top')
}
}
document.onmousemove = function (e) {
e.preventDefault() // 阻止默认事件
// 如果拖拽标志为true
if (params.flag) {
const nowX = e.clientX // 鼠标当前位置的X坐标
const nowY = e.clientY // 鼠标当前位置的Y坐标
const disX = nowX - params.currentX // 鼠标移动的X距离
const disY = nowY - params.currentY // 鼠标移动的Y距离
let left = parseInt(params.left) + disX // 盒子元素的新left值
let top = parseInt(params.top) + disY // 盒子元素的新top值
// 拖出屏幕边缘
if (-left > minDomLeft) {
left = -minDomLeft
} else if (left > maxDragDomLeft) {
left = maxDragDomLeft
}
if (-top > minDomTop) {
top = -minDomTop
} else if (top > maxDragDomTop) {
top = maxDragDomTop
}
box.style.left = left + 'px'
box.style.top = top + 'px'
}
}
}

View File

@@ -62,7 +62,8 @@ function open(options) {
async function setCurrentRole() { async function setCurrentRole() {
try { try {
okLoading.value = true okLoading.value = true
await userStore.switchCurrentRole(roleCode.value) const { data } = await api.switchCurrentRole(roleCode.value)
await authStore.switchCurrentRole(data)
okLoading.value = false okLoading.value = false
$message.success('切换成功') $message.success('切换成功')
modalRef.value?.handleOk() modalRef.value?.handleOk()

View File

@@ -8,7 +8,8 @@
<template> <template>
<router-link class="h-60 f-c-c" to="/"> <router-link class="h-60 f-c-c" to="/">
<img src="@/assets/images/logo.png" class="h-40" /> <!-- <img src="@/assets/images/logo.png" class="h-40" /> -->
<TheLogo class="rounded-8!" />
<h2 <h2
v-show="!appStore.collapsed" v-show="!appStore.collapsed"
class="ml-10 max-w-140 flex-shrink-0 text-16 color-primary font-bold" class="ml-10 max-w-140 flex-shrink-0 text-16 color-primary font-bold"

View File

@@ -0,0 +1,40 @@
export const basicRoutes = [
{
name: 'Login',
path: '/login',
component: () => import('@/views/login/index.vue'),
meta: {
title: '登录页',
layout: 'empty',
},
},
{
name: 'Home',
path: '/',
component: () => import('@/views/home/index.vue'),
meta: {
title: '首页',
},
},
{
name: '404',
path: '/404',
component: () => import('@/views/error-page/404.vue'),
meta: {
title: '页面飞走了',
layout: 'empty',
},
},
{
name: '403',
path: '/403',
component: () => import('@/views/error-page/403.vue'),
meta: {
title: '没有权限',
layout: 'empty',
},
},
]

View File

@@ -9,7 +9,7 @@
import { useAuthStore } from '@/store' import { useAuthStore } from '@/store'
import api from '@/api' import api from '@/api'
const WHITE_LIST = ['/login', '/404', '/role-select'] const WHITE_LIST = ['/login', '/404']
export function createPermissionGuard(router) { export function createPermissionGuard(router) {
router.beforeEach(async (to) => { router.beforeEach(async (to) => {
const authStore = useAuthStore() const authStore = useAuthStore()

View File

@@ -8,7 +8,7 @@
import { useTabStore } from '@/store' import { useTabStore } from '@/store'
export const EXCLUDE_TAB = ['/404', '/403', '/login', '/role-select'] export const EXCLUDE_TAB = ['/404', '/403', '/login']
export function createTabGuard(router) { export function createTabGuard(router) {
router.afterEach((to) => { router.afterEach((to) => {

View File

@@ -9,51 +9,14 @@
import { createRouter, createWebHistory, createWebHashHistory } from 'vue-router' import { createRouter, createWebHistory, createWebHashHistory } from 'vue-router'
import { setupRouterGuards } from './guards' import { setupRouterGuards } from './guards'
import { useAuthStore, usePermissionStore, useUserStore } from '@/store' import { useAuthStore, usePermissionStore, useUserStore } from '@/store'
import { getPermissions, getUserInfo } from '@/store/helper'
export const basicRoutes = [ import { basicRoutes } from './basic-routes'
{
name: 'Login',
path: '/login',
component: () => import('@/views/login/index.vue'),
meta: {
title: '登录页',
layout: 'empty',
},
},
{
name: 'Home',
path: '/',
component: () => import('@/views/home/index.vue'),
meta: {
title: '首页',
},
},
{
name: '404',
path: '/404',
component: () => import('@/views/error-page/404.vue'),
meta: {
title: '页面飞走了',
layout: 'empty',
},
},
{
name: '403',
path: '/403',
component: () => import('@/views/error-page/403.vue'),
meta: {
title: '没有权限',
layout: 'empty',
},
},
]
export const router = createRouter({ export const router = createRouter({
history: history:
import.meta.env.VITE_USE_HASH === 'true' ? createWebHashHistory('/') : createWebHistory('/'), import.meta.env.VITE_USE_HASH === 'true'
? createWebHashHistory(import.meta.env.VITE_PUBLIC_PATH || '/')
: createWebHistory(import.meta.env.VITE_PUBLIC_PATH || '/'),
routes: basicRoutes, routes: basicRoutes,
scrollBehavior: () => ({ left: 0, top: 0 }), scrollBehavior: () => ({ left: 0, top: 0 }),
}) })
@@ -74,32 +37,21 @@ export async function initUserAndPermissions() {
const authStore = useAuthStore() const authStore = useAuthStore()
if (!authStore.accessToken) { if (!authStore.accessToken) {
authStore.toLogin() const route = unref(router.currentRoute)
if (route.path !== '/login') {
router.replace({
path: '/login',
query: route.query,
})
}
return return
} }
await Promise.all([userStore.getUserInfo(), permissionStore.initPermissions()]) const [user, permissions] = await Promise.all([getUserInfo(), getPermissions()])
userStore.setUser(user)
permissionStore.setPermissions(permissions)
const routeComponents = import.meta.glob('@/views/**/*.vue')
permissionStore.accessRoutes.forEach((route) => { permissionStore.accessRoutes.forEach((route) => {
route.component = routeComponents[route.component] || undefined
!router.hasRoute(route.name) && router.addRoute(route) !router.hasRoute(route.name) && router.addRoute(route)
}) })
} }
export async function resetRouter() {
const basicRouteNames = getRouteNames(basicRoutes)
router.getRoutes().forEach((route) => {
const name = route.name
if (!basicRouteNames.includes(name)) {
router.removeRoute(name)
}
})
}
export function getRouteNames(routes) {
const names = []
for (const route of routes) {
names.push(route.name)
if (route.children?.length) {
names.push(...getRouteNames(route.children))
}
}
return names
}

29
src/store/helper.js Normal file
View File

@@ -0,0 +1,29 @@
import { basePermissions } from '@/settings'
import api from '@/api'
export async function getUserInfo() {
const res = await api.getUser()
const { id, username, profile, roles, currentRole } = res.data || {}
return {
id,
username,
avatar: profile?.avatar,
nickName: profile?.nickName,
gender: profile?.gender,
address: profile?.address,
email: profile?.email,
roles,
currentRole,
}
}
export async function getPermissions() {
let asyncPermissions = []
try {
const res = await api.getRolePermissions()
asyncPermissions = res?.data || []
} catch (error) {
console.error(error)
}
return basePermissions.concat(asyncPermissions)
}

View File

@@ -33,6 +33,6 @@ export const useAppStore = defineStore('app', {
}, },
persist: { persist: {
paths: ['collapsed', 'naiveThemeOverrides'], paths: ['collapsed', 'naiveThemeOverrides'],
storage: localStorage, storage: sessionStorage,
}, },
}) })

View File

@@ -7,8 +7,7 @@
**********************************/ **********************************/
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import { useUserStore, usePermissionStore, useTabStore } from '@/store' import { useUserStore, usePermissionStore, useTabStore, useRouterStore } from '@/store'
import { resetRouter, router } from '@/router'
export const useAuthStore = defineStore('auth', { export const useAuthStore = defineStore('auth', {
state: () => ({ state: () => ({
@@ -22,24 +21,30 @@ export const useAuthStore = defineStore('auth', {
this.$reset() this.$reset()
}, },
toLogin() { toLogin() {
const currentRoute = unref(router.currentRoute) const { router, route } = useRouterStore()
router.replace({ router.replace({
path: '/login', path: '/login',
query: currentRoute.query, query: route.query,
}) })
}, },
async switchCurrentRole(data) {
this.resetLoginState()
await nextTick()
this.setToken(data)
},
resetLoginState() { resetLoginState() {
const { resetUser } = useUserStore() const { resetUser } = useUserStore()
const { resetPermission } = usePermissionStore() const { resetRouter } = useRouterStore()
const { resetPermission, accessRoutes } = usePermissionStore()
const { resetTabs } = useTabStore() const { resetTabs } = useTabStore()
// 重置路由
resetRouter(accessRoutes)
// 重置用户 // 重置用户
resetUser() resetUser()
// 重置权限 // 重置权限
resetPermission() resetPermission()
// 重置Tabs // 重置Tabs
resetTabs() resetTabs()
// 重置路由
resetRouter()
// 重置token // 重置token
this.resetToken() this.resetToken()
}, },

View File

@@ -3,3 +3,4 @@ export * from './auth'
export * from './permission' export * from './permission'
export * from './tab' export * from './tab'
export * from './user' export * from './user'
export * from './router'

View File

@@ -7,27 +7,16 @@
**********************************/ **********************************/
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import { isExternal } from '@/utils'
import { basePermissions } from '@/settings'
import api from '@/api'
const routeComponents = import.meta.glob('@/views/**/*.vue')
export const usePermissionStore = defineStore('permission', { export const usePermissionStore = defineStore('permission', {
state: () => ({ state: () => ({
menus: [],
accessRoutes: [], accessRoutes: [],
asyncPermissions: [], permissions: [],
menus: [],
}), }),
getters: {
permissions() {
return basePermissions.concat(this.asyncPermissions)
},
},
actions: { actions: {
async initPermissions() { setPermissions(permissions) {
const { data } = (await api.getRolePermissions()) || [] this.permissions = permissions
this.asyncPermissions = data
this.menus = this.permissions this.menus = this.permissions
.filter((item) => item.type === 'MENU') .filter((item) => item.type === 'MENU')
.map((item) => this.getMenuItem(item)) .map((item) => this.getMenuItem(item))
@@ -36,7 +25,7 @@ export const usePermissionStore = defineStore('permission', {
}, },
getMenuItem(item, parent) { getMenuItem(item, parent) {
const route = this.generateRoute(item, item.show ? null : parent?.key) const route = this.generateRoute(item, item.show ? null : parent?.key)
if (item.enable && route.path && !isExternal(route.path)) this.accessRoutes.push(route) if (item.enable && route.path && !route.path.startsWith('http')) this.accessRoutes.push(route)
if (!item.show) return null if (!item.show) return null
const menuItem = { const menuItem = {
label: route.meta.title, label: route.meta.title,
@@ -60,7 +49,7 @@ export const usePermissionStore = defineStore('permission', {
name: item.code, name: item.code,
path: item.path, path: item.path,
redirect: item.redirect, redirect: item.redirect,
component: routeComponents[item.component] || undefined, component: item.component,
meta: { meta: {
icon: item.icon, icon: item.icon,
title: item.name, title: item.name,

View File

@@ -0,0 +1,26 @@
/**********************************
* @Author: Ronnie Zhang
* @LastEditor: Ronnie Zhang
* @LastEditTime: 2024/01/06 17:18:40
* @Email: zclzone@outlook.com
* Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top
**********************************/
import { defineStore } from 'pinia'
export const useRouterStore = defineStore('router', () => {
const router = useRouter()
const route = useRoute()
function resetRouter(accessRoutes) {
accessRoutes.forEach((item) => {
router.hasRoute(item.name) && router.removeRoute(item.name)
})
}
return {
router,
route,
resetRouter,
}
})

View File

@@ -7,7 +7,7 @@
**********************************/ **********************************/
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import { router } from '@/router' import { useRouterStore } from './router'
export const useTabStore = defineStore('tab', { export const useTabStore = defineStore('tab', {
state: () => ({ state: () => ({
@@ -55,13 +55,13 @@ export const useTabStore = defineStore('tab', {
async removeTab(path) { 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) { if (path === this.activeTab) {
router.push(this.tabs[this.tabs.length - 1].path) useRouterStore().router?.push(this.tabs[this.tabs.length - 1].path)
} }
}, },
removeOther(curPath = this.activeTab) { 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) { if (curPath !== this.activeTab) {
router.push(this.tabs[this.tabs.length - 1].path) useRouterStore().router?.push(this.tabs[this.tabs.length - 1].path)
} }
}, },
removeLeft(curPath) { removeLeft(curPath) {
@@ -69,7 +69,7 @@ export const useTabStore = defineStore('tab', {
const filterTabs = this.tabs.filter((item, index) => index >= curIndex) const filterTabs = this.tabs.filter((item, index) => index >= curIndex)
this.setTabs(filterTabs) this.setTabs(filterTabs)
if (!filterTabs.find((item) => item.path === this.activeTab)) { if (!filterTabs.find((item) => item.path === this.activeTab)) {
router.push(filterTabs[filterTabs.length - 1].path) useRouterStore().router?.push(filterTabs[filterTabs.length - 1].path)
} }
}, },
removeRight(curPath) { removeRight(curPath) {
@@ -77,7 +77,7 @@ export const useTabStore = defineStore('tab', {
const filterTabs = this.tabs.filter((item, index) => index <= curIndex) const filterTabs = this.tabs.filter((item, index) => index <= curIndex)
this.setTabs(filterTabs) this.setTabs(filterTabs)
if (!filterTabs.find((item) => item.path === this.activeTab.value)) { if (!filterTabs.find((item) => item.path === this.activeTab.value)) {
router.push(filterTabs[filterTabs.length - 1].path) useRouterStore().router?.push(filterTabs[filterTabs.length - 1].path)
} }
}, },
resetTabs() { resetTabs() {

View File

@@ -7,8 +7,6 @@
**********************************/ **********************************/
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import api from '@/api'
import { useAuthStore } from '@/store'
export const useUserStore = defineStore('user', { export const useUserStore = defineStore('user', {
state: () => ({ state: () => ({
@@ -35,32 +33,8 @@ export const useUserStore = defineStore('user', {
}, },
}, },
actions: { actions: {
async getUserInfo() { setUser(user) {
try { this.userInfo = user
const res = await api.getUser()
const { id, username, profile, roles, currentRole } = res.data || {}
this.userInfo = {
id,
username,
avatar: profile?.avatar,
nickName: profile?.nickName,
gender: profile?.gender,
address: profile?.address,
email: profile?.email,
roles,
currentRole,
}
return Promise.resolve(res.data)
} catch (error) {
return Promise.reject(error)
}
},
async switchCurrentRole(roleCode) {
const { data } = await api.switchCurrentRole(roleCode)
const authStore = useAuthStore()
authStore.resetLoginState()
await nextTick()
authStore.setToken(data)
}, },
resetUser() { resetUser() {
this.$reset() this.$reset()

View File

@@ -41,3 +41,9 @@ textarea {
border: none; border: none;
resize: none; resize: none;
} }
img,
video {
max-width: 100%;
height: auto;
}

View File

@@ -43,7 +43,7 @@ export function setupInterceptors(axiosInstance) {
const message = resolveResError(code, data?.message ?? statusText) const message = resolveResError(code, data?.message ?? statusText)
//需要错误提醒 //需要错误提醒
!config.noNeedTip && window.$message?.error(message) !config?.noNeedTip && message && window.$message?.error(message)
return Promise.reject({ code, message, error: data ?? response }) return Promise.reject({ code, message, error: data ?? response })
} }
return Promise.resolve(data ?? response) return Promise.resolve(data ?? response)

View File

@@ -9,7 +9,7 @@
import * as NaiveUI from 'naive-ui' import * as NaiveUI from 'naive-ui'
import { isNullOrUndef } from '@/utils' import { isNullOrUndef } from '@/utils'
import { useAppStore } from '@/store/modules/app' import { useAppStore } from '@/store'
export function setupMessage(NMessage) { export function setupMessage(NMessage) {
class Message { class Message {

View File

@@ -1,7 +1,7 @@
<!-------------------------------- <!--------------------------------
- @Author: Ronnie Zhang - @Author: Ronnie Zhang
- @LastEditor: Ronnie Zhang - @LastEditor: Ronnie Zhang
- @LastEditTime: 2023/12/16 18:51:56 - @LastEditTime: 2024/01/13 17:41:47
- @Email: zclzone@outlook.com - @Email: zclzone@outlook.com
- Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top - Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top
---------------------------------> --------------------------------->
@@ -55,7 +55,7 @@ function openModal2() {
$modal2.value?.open({ $modal2.value?.open({
cancelText: '关闭当前', cancelText: '关闭当前',
okText: '关闭所有弹窗', okText: '关闭所有弹窗',
modalStyle: { width: '320px', padding: '12px', top: '100px' }, width: '400px',
async onOk() { async onOk() {
okLoading2.value = true okLoading2.value = true
$message.loading('正在关闭...', { key: 'modal2' }) $message.loading('正在关闭...', { key: 'modal2' })

View File

@@ -17,7 +17,7 @@
<div class="w-320 flex-col px-20 py-32"> <div class="w-320 flex-col px-20 py-32">
<h2 class="f-c-c text-24 text-#6a6a6a font-normal"> <h2 class="f-c-c text-24 text-#6a6a6a font-normal">
<img src="@/assets/images/logo.png" height="50" class="mr-12" /> <img src="@/assets/images/logo.png" class="mr-12 h-50" />
{{ title }} {{ title }}
</h2> </h2>
<n-input <n-input
@@ -104,42 +104,30 @@
import { throttle, lStorage } from '@/utils' import { throttle, lStorage } from '@/utils'
import { useStorage } from '@vueuse/core' import { useStorage } from '@vueuse/core'
import api from './api' import api from './api'
import { useUserStore, useAuthStore } from '@/store' import { useAuthStore } from '@/store'
import { initUserAndPermissions } from '@/router' import { initUserAndPermissions } from '@/router'
const userStore = useUserStore()
const authStore = useAuthStore() const authStore = useAuthStore()
const router = useRouter() const router = useRouter()
const route = useRoute() const route = useRoute()
const title = import.meta.env.VITE_TITLE const title = import.meta.env.VITE_TITLE
const isLogined = computed(() => {
return authStore.accessToken && userStore.roles
})
const loginInfo = ref({ const loginInfo = ref({
username: '', username: '',
password: '', password: '',
}) })
function initLoginInfo() {
const localLoginInfo = lStorage.get('loginInfo')
if (localLoginInfo) {
loginInfo.value.username = localLoginInfo.username || ''
loginInfo.value.password = localLoginInfo.password || ''
}
}
const captchaUrl = ref('') const captchaUrl = ref('')
const initCaptcha = throttle(() => { const initCaptcha = throttle(() => {
captchaUrl.value = '/api/auth/captcha?' + Date.now() captchaUrl.value = '/api/auth/captcha?' + Date.now()
}, 500) }, 500)
if (isLogined.value) { const localLoginInfo = lStorage.get('loginInfo')
router.push({ path: '/role-select', query: route.query }) if (localLoginInfo) {
} else { loginInfo.value.username = localLoginInfo.username || ''
initLoginInfo() loginInfo.value.password = localLoginInfo.password || ''
initCaptcha()
} }
initCaptcha()
function quickLogin() { function quickLogin() {
loginInfo.value.username = 'admin' loginInfo.value.username = 'admin'

View File

@@ -106,10 +106,14 @@ function handleDelete(item) {
$dialog.confirm({ $dialog.confirm({
content: `确认删除【${item.name}】?`, content: `确认删除【${item.name}】?`,
async confirm() { async confirm() {
$message.loading('正在删除', { key: 'deleteMenu' }) try {
await api.deletePermission(item.id) $message.loading('正在删除', { key: 'deleteMenu' })
$message.success('删除成功', { key: 'deleteMenu' }) await api.deletePermission(item.id)
emit('refresh') $message.success('删除成功', { key: 'deleteMenu' })
emit('refresh')
} catch (error) {
$message.destroy('deleteMenu')
}
}, },
}) })
} }

View File

@@ -38,7 +38,21 @@
</template> </template>
<n-input v-model:value="modalForm.code" /> <n-input v-model:value="modalForm.code" />
</n-form-item-gi> </n-form-item-gi>
<n-form-item-gi :span="12" path="path"> <n-form-item-gi
:span="12"
path="path"
:rule="{
trigger: ['blur', 'change'],
type: 'string',
message: '必须是/、http、https开头',
validator(rule, value) {
if (value) {
return /\/|http|https/.test(value)
}
return true
},
}"
>
<template #label> <template #label>
<QuestionLabel label="路由地址" content="父级菜单可不填" /> <QuestionLabel label="路由地址" content="父级菜单可不填" />
</template> </template>

View File

@@ -23,13 +23,7 @@
:get-data="api.read" :get-data="api.read"
> >
<MeQueryItem label="角色名" :label-width="50"> <MeQueryItem label="角色名" :label-width="50">
<n-input <n-input v-model:value="queryItems.name" type="text" placeholder="请输入角色名" clearable />
v-model:value="queryItems.name"
type="text"
placeholder="请输入角色名"
clearable
@keydown.enter="() => $table?.handleSearch()"
/>
</MeQueryItem> </MeQueryItem>
<MeQueryItem label="状态" :label-width="50"> <MeQueryItem label="状态" :label-width="50">
<n-select <n-select

View File

@@ -43,7 +43,6 @@
type="text" type="text"
placeholder="请输入用户名" placeholder="请输入用户名"
clearable clearable
@keydown.enter="() => $table?.handleSearch"
/> />
</MeQueryItem> </MeQueryItem>

View File

@@ -28,7 +28,6 @@
type="text" type="text"
placeholder="请输入用户名" placeholder="请输入用户名"
clearable clearable
@keydown.enter="() => $table?.handleSearch"
/> />
</MeQueryItem> </MeQueryItem>

View File

@@ -100,9 +100,10 @@
import { MeModal } from '@/components' import { MeModal } from '@/components'
import { useForm, useModal } from '@/composables' import { useForm, useModal } from '@/composables'
import { useUserStore } from '@/store' import { useUserStore } from '@/store'
import { getUserInfo } from '@/store/helper'
import api from './api' import api from './api'
const userStore = useUserStore()
const userStore = useUserStore()
const required = { const required = {
required: true, required: true,
message: '此为必填项', message: '此为必填项',
@@ -116,7 +117,7 @@ async function handlePwdSave() {
await pwdValidation() await pwdValidation()
await api.changePassword(pwdForm.value) await api.changePassword(pwdForm.value)
$message.success('密码修改成功') $message.success('密码修改成功')
userStore.getUserInfo() refreshUserInfo()
} }
const newAvatar = ref(userStore.avatar) const newAvatar = ref(userStore.avatar)
@@ -128,7 +129,7 @@ async function handleAvatarSave() {
} }
await api.updateProfile({ id: userStore.userId, avatar: newAvatar.value }) await api.updateProfile({ id: userStore.userId, avatar: newAvatar.value })
$message.success('头像修改成功') $message.success('头像修改成功')
userStore.getUserInfo() refreshUserInfo()
} }
const genders = [ const genders = [
@@ -148,6 +149,11 @@ async function handleProfileSave() {
await profileValidation() await profileValidation()
await api.updateProfile(profileForm.value) await api.updateProfile(profileForm.value)
$message.success('资料修改成功') $message.success('资料修改成功')
userStore.getUserInfo() refreshUserInfo()
}
async function refreshUserInfo() {
const user = await getUserInfo()
userStore.setUser(user)
} }
</script> </script>

View File

@@ -14,7 +14,6 @@ import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite' import Components from 'unplugin-vue-components/vite'
import { NaiveUiResolver } from 'unplugin-vue-components/resolvers' import { NaiveUiResolver } from 'unplugin-vue-components/resolvers'
import simpleHtmlPlugin from 'vite-plugin-simple-html' import simpleHtmlPlugin from 'vite-plugin-simple-html'
import VueDevTools from 'vite-plugin-vue-devtools'
import { pluginPagePathes, pluginIcons } from './build/plugin-isme' import { pluginPagePathes, pluginIcons } from './build/plugin-isme'
export default defineConfig(({ command, mode }) => { export default defineConfig(({ command, mode }) => {
@@ -25,7 +24,6 @@ export default defineConfig(({ command, mode }) => {
return { return {
base: VITE_PUBLIC_PATH || '/', base: VITE_PUBLIC_PATH || '/',
plugins: [ plugins: [
VueDevTools(),
Vue(), Vue(),
Unocss(), Unocss(),
AutoImport({ AutoImport({
@@ -64,6 +62,7 @@ export default defineConfig(({ command, mode }) => {
target: VITE_PROXY_TARGET, target: VITE_PROXY_TARGET,
changeOrigin: true, changeOrigin: true,
rewrite: (path) => path.replace(new RegExp('^/api'), ''), rewrite: (path) => path.replace(new RegExp('^/api'), ''),
secure: false,
configure: (proxy, options) => { configure: (proxy, options) => {
// 配置此项可在响应头中看到请求的真实地址 // 配置此项可在响应头中看到请求的真实地址
proxy.on('proxyRes', (proxyRes, req) => { proxy.on('proxyRes', (proxyRes, req) => {