1
0
mirror of https://github.com/zclzone/vue-naive-admin.git synced 2025-05-02 15:19:01 +08:00

Compare commits

..

No commits in common. "54c134c879c9c4791a5c86d1bd7fe78637e2cb67" and "7ed9a3540dba5bee79e884d504d2b4d7f75c6361" have entirely different histories.

79 changed files with 3040 additions and 5510 deletions

View File

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

4
.eslintignore Normal file
View File

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

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

44
.vscode/settings.json vendored
View File

@ -1,27 +1,25 @@
{ {
// Enable the ESlint flat config support "files.eol": "\n",
// (remove this if your ESLint extension above v3.0.5) "files.associations": {
"eslint.experimental.useFlatConfig": true, "*.env.*": "dotenv",
"*.svg": "html",
"prettier.enable": false, "*.css": "scss"
"editor.formatOnSave": false,
// Auto fix
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit",
"source.organizeImports": "never"
}, },
// Enable eslint for all supported languages "editor.formatOnSave": false,
"eslint.validate": [ "[html][css][less][scss][sass][yaml][yml][jsonc][json]": {
"javascript", "editor.formatOnSave": true,
"javascriptreact", "editor.defaultFormatter": "esbenp.prettier-vscode"
"typescript", },
"typescriptreact", "[markdown]": {
"vue", "editor.formatOnSave": true,
"html", "editor.defaultFormatter": "yzhang.markdown-all-in-one"
"markdown", },
"json", "editor.codeActionsOnSave": {
"jsonc" "source.fixAll.eslint": "explicit"
] },
"eslint.validate": ["javascript", "typescript", "javascriptreact", "typescriptreact", "vue"],
"eslint.options": {
"overrideConfigFile": "package.json"
}
} }

View File

@ -8,7 +8,6 @@
</p> </p>
## 简介 ## 简介
Vue Naive Admin 是一款极简风格的后台管理模板,包含前后端解决方案,前端使用 Vite + Vue3 + Pinia + Unocss后端使用 Nestjs + TypeOrm + MySql简单易用赏心悦目历经十几次重构和细节打磨诚意满满 Vue Naive Admin 是一款极简风格的后台管理模板,包含前后端解决方案,前端使用 Vite + Vue3 + Pinia + Unocss后端使用 Nestjs + TypeOrm + MySql简单易用赏心悦目历经十几次重构和细节打磨诚意满满
## 设计理念 ## 设计理念
@ -52,8 +51,9 @@ 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)
## 文档
## 文档
- 项目文档: [docs | vue-naive-admin](https://docs.isme.top/web/#/624306705/188522224) - 项目文档: [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) - 接口文档: [apidoc | isme-nest-serve](https://apifox.com/apidoc/shared-ff4a4d32-c0d1-4caf-b0ee-6abc130f734a)
@ -81,3 +81,4 @@ Vue Naive Admin 提供一套后端代码,技术栈使用 Nestjs + TypeOrm + My
添加微信,拉你进群 添加微信,拉你进群
![](https://static.isme.top/image/coder_wx.jpg) ![](https://static.isme.top/image/coder_wx.jpg)

View File

@ -6,8 +6,8 @@
* Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top * Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top
**********************************/ **********************************/
import path from 'node:path'
import { globSync } from 'glob' import { globSync } from 'glob'
import path from 'path'
import dynamicIcons from '../src/assets/icons/dynamic-icons' import dynamicIcons from '../src/assets/icons/dynamic-icons'
/** /**
@ -35,5 +35,5 @@ export function getIcons() {
*/ */
export function getPagePathes() { export function getPagePathes() {
const files = globSync('src/views/**/*.vue') 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 { return {
name: 'isme:icons', name: 'isme:icons',
resolveId(id) { resolveId(id) {
if (id === PLUGIN_ICONS_ID) if (id === PLUGIN_ICONS_ID) return '\0' + PLUGIN_ICONS_ID
return `\0${PLUGIN_ICONS_ID}`
}, },
load(id) { load(id) {
if (id === `\0${PLUGIN_ICONS_ID}`) { if (id === '\0' + PLUGIN_ICONS_ID) {
return `export default ${JSON.stringify(getIcons())}` return `export default ${JSON.stringify(getIcons())}`
} }
}, },

View File

@ -13,11 +13,10 @@ export function pluginPagePathes() {
return { return {
name: 'isme:page-pathes', name: 'isme:page-pathes',
resolveId(id) { resolveId(id) {
if (id === PLUGIN_PAGE_PATHES_ID) if (id === PLUGIN_PAGE_PATHES_ID) return '\0' + PLUGIN_PAGE_PATHES_ID
return `\0${PLUGIN_PAGE_PATHES_ID}`
}, },
load(id) { load(id) {
if (id === `\0${PLUGIN_PAGE_PATHES_ID}`) { if (id === '\0' + PLUGIN_PAGE_PATHES_ID) {
return `export default ${JSON.stringify(getPagePathes())}` 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,4 +1,4 @@
<!doctype html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />

View File

@ -1,20 +1,18 @@
{ {
"name": "vue-naive-admin", "name": "vue-naive-admin",
"type": "module",
"version": "2.0.0",
"private": true, "private": true,
"version": "2.0.0",
"type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "vite build", "build": "vite build",
"preview": "vite preview", "preview": "vite preview",
"lint:fix": "eslint --fix", "lint:fix": "eslint --fix --ext .js,.vue ."
"postinstall": "npx simple-git-hooks",
"up": "taze major -I"
}, },
"dependencies": { "dependencies": {
"@arco-design/color": "^0.4.0", "@arco-design/color": "^0.4.0",
"@vueuse/core": "^10.10.0", "@vueuse/core": "^10.9.0",
"axios": "^1.7.2", "axios": "^1.6.8",
"dayjs": "^1.11.11", "dayjs": "^1.11.11",
"echarts": "^5.5.0", "echarts": "^5.5.0",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
@ -22,39 +20,39 @@
"pinia": "^2.1.7", "pinia": "^2.1.7",
"pinia-plugin-persistedstate": "^3.2.1", "pinia-plugin-persistedstate": "^3.2.1",
"vue": "^3.4.27", "vue": "^3.4.27",
"vue-echarts": "^6.7.3", "vue-echarts": "^6.7.2",
"vue-router": "^4.3.2", "vue-router": "^4.3.2",
"xlsx": "^0.18.5" "xlsx": "^0.18.5"
}, },
"devDependencies": { "devDependencies": {
"@antfu/eslint-config": "^2.20.0", "@iconify/json": "^2.2.210",
"@iconify/json": "^2.2.216", "@iconify/utils": "^2.1.23",
"@iconify/utils": "^2.1.24", "@unocss/eslint-config": "^0.58.9",
"@unocss/eslint-config": "^0.60.4", "@unocss/preset-rem-to-px": "^0.59.4",
"@unocss/eslint-plugin": "^0.60.4", "@vitejs/plugin-vue": "^5.0.4",
"@unocss/preset-rem-to-px": "^0.60.4", "@zclzone/eslint-config": "^0.0.5",
"@vitejs/plugin-vue": "^5.0.5",
"eslint": "^9.4.0",
"eslint-plugin-format": "^0.1.1",
"esno": "^4.7.0", "esno": "^4.7.0",
"fs-extra": "^11.2.0", "fs-extra": "^11.2.0",
"glob": "^10.4.1", "glob": "^10.3.15",
"lint-staged": "^15.2.5",
"rollup-plugin-visualizer": "^5.12.0", "rollup-plugin-visualizer": "^5.12.0",
"sass": "^1.77.4", "sass": "^1.77.1",
"simple-git-hooks": "^2.11.1", "unocss": "^0.59.4",
"taze": "^0.13.8",
"unocss": "^0.60.4",
"unplugin-auto-import": "^0.17.6", "unplugin-auto-import": "^0.17.6",
"unplugin-vue-components": "^0.27.0", "unplugin-vue-components": "^0.26.0",
"vite": "^5.2.12", "vite": "^5.2.11",
"vite-plugin-router-warn": "^1.0.0", "vite-plugin-router-warn": "^1.0.0",
"vite-plugin-vue-devtools": "^7.2.1" "vite-plugin-vue-devtools": "^7.2.0"
}, },
"simple-git-hooks": { "eslintConfig": {
"pre-commit": "pnpm lint-staged" "extends": [
}, "@zclzone",
"lint-staged": { "@unocss",
"*": "eslint --fix" ".eslint-global-variables.json"
],
"parserOptions": {
"ecmaFeatures": {
"jsx": true
}
}
} }
} }

7473
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -27,15 +27,14 @@
</template> </template>
<script setup> <script setup>
import { darkTheme, dateZhCN, zhCN } from 'naive-ui' import { zhCN, dateZhCN, darkTheme } from 'naive-ui'
import { LayoutSetting } from '@/components' import { LayoutSetting } from '@/components'
import { useAppStore, useTabStore } from '@/store' import { useAppStore, useTabStore } from '@/store'
const layouts = new Map() const layouts = new Map()
function getLayout(name) { function getLayout(name) {
// maplayoutlayout // maplayoutlayout
if (layouts.get(name)) if (layouts.get(name)) return layouts.get(name)
return layouts.get(name)
const layout = markRaw(defineAsyncComponent(() => import(`@/layouts/${name}/index.vue`))) const layout = markRaw(defineAsyncComponent(() => import(`@/layouts/${name}/index.vue`)))
layouts.set(name, layout) layouts.set(name, layout)
return layout return layout
@ -43,17 +42,15 @@ function getLayout(name) {
const route = useRoute() const route = useRoute()
const appStore = useAppStore() const appStore = useAppStore()
if (appStore.layout === 'default') if (appStore.layout === 'default') appStore.setLayout('')
appStore.setLayout('')
const Layout = computed(() => { const Layout = computed(() => {
if (!route.matched?.length) if (!route.matched?.length) return null
return null
return getLayout(route.meta?.layout || appStore.layout) return getLayout(route.meta?.layout || appStore.layout)
}) })
const tabStore = useTabStore() const tabStore = useTabStore()
const keepAliveNames = computed(() => { const keepAliveNames = computed(() => {
return tabStore.tabs.filter(item => item.keepAlive).map(item => item.name) return tabStore.tabs.filter((item) => item.keepAlive).map((item) => item.name)
}) })
watchEffect(() => { watchEffect(() => {

View File

@ -16,9 +16,9 @@ export default {
// 登出 // 登出
logout: () => request.post('/auth/logout'), 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'), 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

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

View File

@ -27,10 +27,8 @@
<span class="ml-4">返回</span> <span class="ml-4">返回</span>
</div> </div>
</template> </template>
<div class="mr-12 h-16 w-4 rounded-l-2 bg-primary" /> <div class="mr-12 h-16 w-4 rounded-l-2 bg-primary"></div>
<h2 class="font-normal"> <h2 class="font-normal">{{ title ?? route.meta?.title }}</h2>
{{ title ?? route.meta?.title }}
</h2>
<slot name="title-suffix" /> <slot name="title-suffix" />
</div> </div>
<slot name="action" /> <slot name="action" />

View File

@ -7,6 +7,6 @@
---------------------------------> --------------------------------->
<template> <template>
<div class="h-40 w-40 rounded-4 bg-primary p-1/100"> <div class="h-40 w-40 rounded-4 bg-primary p-1/100">
<img src="@/assets/images/isme.png" alt="Logo"> <img src="@/assets/images/isme.png" alt="Logo" />
</div> </div>
</template> </template>

View File

@ -16,7 +16,6 @@
<script setup> <script setup>
import { getPresetColors } from '@arco-design/color' import { getPresetColors } from '@arco-design/color'
import { useAppStore } from '@/store' import { useAppStore } from '@/store'
const appStore = useAppStore() const appStore = useAppStore()
const primaryColors = Object.entries(getPresetColors()).map(([, value]) => value.primary) const primaryColors = Object.entries(getPresetColors()).map(([, value]) => value.primary)

View File

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

View File

@ -25,7 +25,7 @@
</form> </form>
</AppCard> </AppCard>
<NDataTable <n-data-table
:remote="remote" :remote="remote"
:loading="loading" :loading="loading"
:scroll-x="scrollX" :scroll-x="scrollX"
@ -78,10 +78,10 @@ const props = defineProps({
}, },
/** /**
* ! 约定接口入参出参 * ! 约定接口入参出参
* 分页模式需约定分页接口入参 * * 分页模式需约定分页接口入参
* @pageSize 分页参数一页展示多少条默认10 * @pageSize 分页参数一页展示多少条默认10
* @pageNo 分页参数页码默认1 * @pageNo 分页参数页码默认1
* 需约定接口出参 * * 需约定接口出参
* @pageData 分页模式必须,非分页模式如果没有pageData则取上一层data * @pageData 分页模式必须,非分页模式如果没有pageData则取上一层data
* @total 分页模式必须非分页模式如果没有total则取上一层data.length * @total 分页模式必须非分页模式如果没有total则取上一层data.length
*/ */
@ -111,12 +111,10 @@ async function handleQuery() {
}) })
tableData.value = data?.pageData || data tableData.value = data?.pageData || data
pagination.itemCount = data.total ?? data.length pagination.itemCount = data.total ?? data.length
} } catch (error) {
catch (error) {
tableData.value = [] tableData.value = []
pagination.itemCount = 0 pagination.itemCount = 0
} } finally {
finally {
emit('onDataChange', tableData.value) emit('onDataChange', tableData.value)
loading.value = false loading.value = false
} }
@ -142,17 +140,16 @@ function onPageChange(currentPage) {
} }
} }
function onChecked(rowKeys) { function onChecked(rowKeys) {
if (props.columns.some(item => item.type === 'selection')) { if (props.columns.some((item) => item.type === 'selection')) {
emit('onChecked', rowKeys) emit('onChecked', rowKeys)
} }
} }
function handleExport(columns = props.columns, data = tableData.value) { function handleExport(columns = props.columns, data = tableData.value) {
if (!data?.length) if (!data?.length) return $message.warning('没有数据')
return $message.warning('没有数据') const columnsData = columns.filter((item) => !!item.title && !item.hideInExcel)
const columnsData = columns.filter(item => !!item.title && !item.hideInExcel) const thKeys = columnsData.map((item) => item.key)
const thKeys = columnsData.map(item => item.key) const thData = columnsData.map((item) => item.title)
const thData = columnsData.map(item => item.title) const trData = data.map((item) => thKeys.map((key) => item[key]))
const trData = data.map(item => thKeys.map(key => item[key]))
const sheet = utils.aoa_to_sheet([thData, ...trData]) const sheet = utils.aoa_to_sheet([thData, ...trData])
const workBook = utils.book_new() const workBook = utils.book_new()
utils.book_append_sheet(workBook, sheet, '数据报表') utils.book_append_sheet(workBook, sheet, '数据报表')

View File

@ -18,11 +18,9 @@
> >
<n-card :style="modalOptions.contentStyle" :closable="modalOptions.closable" @close="close()"> <n-card :style="modalOptions.contentStyle" :closable="modalOptions.closable" @close="close()">
<template #header> <template #header>
<header class="modal-header"> <header class="modal-header">{{ modalOptions.title }}</header>
{{ modalOptions.title }}
</header>
</template> </template>
<slot /> <slot></slot>
<!-- 底部按钮 --> <!-- 底部按钮 -->
<template #footer> <template #footer>
@ -48,7 +46,6 @@
<script setup> <script setup>
import { initDrag } from './utils' import { initDrag } from './utils'
const props = defineProps({ const props = defineProps({
width: { width: {
type: String, type: String,
@ -104,17 +101,6 @@ const show = ref(false)
// modalOptions // modalOptions
const modalOptions = ref({}) const modalOptions = ref({})
const okLoading = computed({
get() {
return !!modalOptions.value?.okLoading
},
set(v) {
if (modalOptions.value) {
modalOptions.value.okLoading = v
}
},
})
// //
async function open(options = {}) { async function open(options = {}) {
// propsoptionsmodalOptions // propsoptionsmodalOptions
@ -125,7 +111,7 @@ async function open(options = {}) {
await nextTick() await nextTick()
initDrag( initDrag(
Array.prototype.at.call(document.querySelectorAll('.modal-header'), -1), 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,8 +131,7 @@ async function handleOk(data) {
const res = await modalOptions.value.onOk(data) const res = await modalOptions.value.onOk(data)
// onOkfalse // onOkfalse
res !== false && close() res !== false && close()
} } catch (error) {
catch (error) {
okLoading.value = false okLoading.value = false
console.error(error) console.error(error)
} }
@ -164,8 +149,7 @@ async function handleCancel(data) {
// onCancelfalse // onCancelfalse
res !== false && close() res !== false && close()
} } catch (error) {
catch (error) {
okLoading.value = false okLoading.value = false
console.error(error) console.error(error)
} }
@ -175,10 +159,21 @@ async function onAfterLeave() {
await nextTick() await nextTick()
initDrag( initDrag(
Array.prototype.at.call(document.querySelectorAll('.modal-header'), -1), 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
}
},
})
// defineExposeopenclosehandleOkhandleCancel // defineExposeopenclosehandleOkhandleCancel
defineExpose({ defineExpose({
open, open,

View File

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

View File

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

View File

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

View File

@ -6,7 +6,7 @@
* Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top * Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top
**********************************/ **********************************/
export function useForm(initFormData = {}) { export const useForm = (initFormData = {}) => {
const formRef = ref(null) const formRef = ref(null)
const formModel = ref({ ...initFormData }) const formModel = ref({ ...initFormData })
const rules = { const rules = {

View File

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

View File

@ -11,7 +11,7 @@ import { router } from '@/router'
const permission = { const permission = {
mounted(el, binding) { mounted(el, binding) {
const currentRoute = unref(router.currentRoute) 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)) { if (!btns.includes(binding.value)) {
el.remove() el.remove()
} }

View File

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

View File

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

View File

@ -20,7 +20,6 @@
<script setup> <script setup>
import { useAppStore } from '@/store' import { useAppStore } from '@/store'
const title = import.meta.env.VITE_TITLE const title = import.meta.env.VITE_TITLE
const appStore = useAppStore() const appStore = useAppStore()

View File

@ -52,8 +52,7 @@ function handleMenuSelect(key, item) {
router.push(item.path) router.push(item.path)
}, },
}) })
} } else {
else {
router.push(item.path) router.push(item.path)
} }
} }

View File

@ -21,7 +21,7 @@
</template> </template>
<script setup> <script setup>
import { useAuthStore, usePermissionStore, useUserStore } from '@/store' import { useUserStore, useAuthStore, usePermissionStore } from '@/store'
import { RoleSelect } from '@/layouts/components' import { RoleSelect } from '@/layouts/components'
import api from '@/api' import api from '@/api'
@ -35,7 +35,7 @@ const options = reactive([
label: '个人资料', label: '个人资料',
key: 'profile', key: 'profile',
icon: () => h('i', { class: 'i-material-symbols:person-outline text-14' }), 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: '切换角色', label: '切换角色',
@ -71,8 +71,7 @@ function handleSelect(key) {
async confirm() { async confirm() {
try { try {
await api.logout() await api.logout()
} } catch (error) {
catch (error) {
console.error(error) console.error(error)
} }
authStore.logout() authStore.logout()

View File

@ -73,8 +73,8 @@ const options = computed(() => [
label: '关闭右侧', label: '关闭右侧',
key: 'close-right', key: 'close-right',
disabled: disabled:
tabStore.tabs.length <= 1 tabStore.tabs.length <= 1 ||
|| props.currentPath === tabStore.tabs[tabStore.tabs.length - 1].path, props.currentPath === tabStore.tabs[tabStore.tabs.length - 1].path,
icon: () => h('i', { class: 'i-mdi:arrow-expand-right text-14' }), icon: () => h('i', { class: 'i-mdi:arrow-expand-right text-14' }),
}, },
]) ])

View File

@ -49,7 +49,7 @@ const contextMenuOption = reactive({
currentPath: '', currentPath: '',
}) })
function handleItemClick(path) { const handleItemClick = (path) => {
tabStore.setActiveTab(path) tabStore.setActiveTab(path)
router.push(path) router.push(path)
} }

View File

@ -41,13 +41,13 @@
</template> </template>
<script setup> <script setup>
import { useDark, useFullscreen, useToggle } from '@vueuse/core' import { MenuCollapse, UserAvatar, BreadCrumb } from '@/layouts/components'
import { BreadCrumb, MenuCollapse, UserAvatar } from '@/layouts/components'
import { useAppStore } from '@/store' import { useAppStore } from '@/store'
import { useDark, useToggle, useFullscreen } from '@vueuse/core'
const appStore = useAppStore() const appStore = useAppStore()
const isDark = useDark() const isDark = useDark()
function toggleDark() { const toggleDark = () => {
appStore.toggleDark() appStore.toggleDark()
useToggle(isDark)() useToggle(isDark)()
} }

View File

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

View File

@ -43,13 +43,13 @@
</template> </template>
<script setup> <script setup>
import { useDark, useFullscreen, useToggle } from '@vueuse/core' import { UserAvatar, MenuCollapse, AppTab } from '@/layouts/components'
import { AppTab, MenuCollapse, UserAvatar } from '@/layouts/components' import { useDark, useToggle, useFullscreen } from '@vueuse/core'
import { useAppStore } from '@/store' import { useAppStore } from '@/store'
const appStore = useAppStore() const appStore = useAppStore()
const isDark = useDark() const isDark = useDark()
function toggleDark() { const toggleDark = () => {
appStore.toggleDark() appStore.toggleDark()
useToggle(isDark)() useToggle(isDark)()
} }

View File

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

View File

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

View File

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

View File

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

View File

@ -18,16 +18,13 @@ export function createPermissionGuard(router) {
/** 没有token */ /** 没有token */
if (!token) { if (!token) {
if (WHITE_LIST.includes(to.path)) if (WHITE_LIST.includes(to.path)) return true
return true
return { path: 'login', query: { ...to.query, redirect: to.path } } return { path: 'login', query: { ...to.query, redirect: to.path } }
} }
// 有token的情况 // 有token的情况
if (to.path === '/login') if (to.path === '/login') return { path: '/' }
return { path: '/' } if (WHITE_LIST.includes(to.path)) return true
if (WHITE_LIST.includes(to.path))
return true
const userStore = useUserStore() const userStore = useUserStore()
const permissionStore = usePermissionStore() const permissionStore = usePermissionStore()
@ -44,8 +41,7 @@ export function createPermissionGuard(router) {
} }
const routes = router.getRoutes() const routes = router.getRoutes()
if (routes.find(route => route.name === to.name)) if (routes.find((route) => route.name === to.name)) return true
return true
// 判断是无权限还是404 // 判断是无权限还是404
const { data: hasMenu } = await api.validateMenuPath(to.path) 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) { export function createTabGuard(router) {
router.afterEach((to) => { router.afterEach((to) => {
if (EXCLUDE_TAB.includes(to.path)) if (EXCLUDE_TAB.includes(to.path)) return
return
const tabStore = useTabStore() const tabStore = useTabStore()
const { name, fullPath: path } = to const { name, fullPath: path } = to
const title = to.meta?.title const title = to.meta?.title

View File

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

View File

@ -22,8 +22,7 @@ export async function getPermissions() {
try { try {
const res = await api.getRolePermissions() const res = await api.getRolePermissions()
asyncPermissions = res?.data || [] asyncPermissions = res?.data || []
} } catch (error) {
catch (error) {
console.error(error) console.error(error)
} }
return basePermissions.concat(asyncPermissions) return basePermissions.concat(asyncPermissions)

View File

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

View File

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

View File

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

View File

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

View File

@ -13,8 +13,7 @@ let isConfirming = false
export function resolveResError(code, message) { export function resolveResError(code, message) {
switch (code) { switch (code) {
case 401: case 401:
if (isConfirming) if (isConfirming) return
return
isConfirming = true isConfirming = true
$dialog.confirm({ $dialog.confirm({
title: '提示', title: '提示',
@ -32,8 +31,7 @@ export function resolveResError(code, message) {
return false return false
case 11007: case 11007:
case 11008: case 11008:
if (isConfirming) if (isConfirming) return
return
isConfirming = true isConfirming = true
$dialog.confirm({ $dialog.confirm({
title: '提示', title: '提示',

View File

@ -20,7 +20,7 @@ export function setupInterceptors(axiosInstance) {
const { accessToken } = useAuthStore() const { accessToken } = useAuthStore()
if (accessToken) { if (accessToken) {
// token: Bearer + xxx // token: Bearer + xxx
config.headers.Authorization = `Bearer ${accessToken}` config.headers.Authorization = 'Bearer ' + accessToken
} }
return config return config
@ -42,7 +42,7 @@ export function setupInterceptors(axiosInstance) {
// 根据code处理对应的操作并返回处理后的message // 根据code处理对应的操作并返回处理后的message
const message = resolveResError(code, data?.message ?? statusText) const message = resolveResError(code, data?.message ?? statusText)
// 需要错误提醒 //需要错误提醒
!config?.noNeedTip && message && 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 })
} }

View File

@ -99,27 +99,28 @@ export function isEmpty(val) {
} }
/** /**
* 类似mysql的IFNULL函数 * * 类似mysql的IFNULL函数
* * * 第一个参数为null/undefined/'' 则返回第二个参数作为备用值否则返回第一个参数
* @param {number | boolean | string} val * @param {Number|Boolean|String} val
* @param {number | boolean | string} def * @param {Number|Boolean|String} def
* @returns 第一个参数为null | undefined | '' 则返回第二个参数作为备用值否则返回第一个参数 * @returns
*/ */
export function ifNull(val, def = '') { export function ifNull(val, def = '') {
return isNullOrWhitespace(val) ? def : val return isNullOrWhitespace(val) ? def : val
} }
export function isUrl(path) { 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) return reg.test(path)
} }
/** /**
* @param {string} path * @param {string} path
* @returns {boolean} 是否是外部链接 * @returns {Boolean}
*/ */
export function isExternal(path) { export function isExternal(path) {
return /^https?:|mailto:|tel:/.test(path) return /^(https?:|mailto:|tel:)/.test(path)
} }
export const isServer = typeof window === 'undefined' export const isServer = typeof window === 'undefined'

View File

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

View File

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

View File

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

View File

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

View File

@ -8,9 +8,7 @@
<template> <template>
<CommonPage show-footer> <CommonPage show-footer>
<n-button type="primary" @click="openModal1"> <n-button type="primary" @click="openModal1">打开第一个弹个窗</n-button>
打开第一个弹个窗
</n-button>
<MeModal ref="$modal1"> <MeModal ref="$modal1">
<n-input v-model:value="text" /> <n-input v-model:value="text" />
</MeModal> </MeModal>

View File

@ -9,9 +9,7 @@
<template> <template>
<CommonPage show-footer> <CommonPage show-footer>
<template #title-suffix> <template #title-suffix>
<n-tag class="ml-12" type="primary"> <n-tag class="ml-12" type="primary">feather图标集 + isme自定义图标</n-tag>
feather图标集 + isme自定义图标
</n-tag>
</template> </template>
<ul class="flex flex-wrap justify-between"> <ul class="flex flex-wrap justify-between">
<li <li
@ -20,7 +18,7 @@
class="m-16 w-160 f-c-c flex-col cursor-pointer rounded-12 px-12 py-24 card-border auto-bg" 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;`)" @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 <span
class="mt-16 text-center text-14 text-gray-400 hover:color-primary" class="mt-16 text-center text-14 text-gray-400 hover:color-primary"
@click.stop="copy(item)" @click.stop="copy(item)"
@ -28,13 +26,13 @@
{{ item }} {{ item }}
</span> </span>
</li> </li>
<li class="mx-16 h-0 w-160" /> <li class="mx-16 h-0 w-160"></li>
<li class="mx-16 h-0 w-160" /> <li class="mx-16 h-0 w-160"></li>
<li class="mx-16 h-0 w-160" /> <li class="mx-16 h-0 w-160"></li>
<li class="mx-16 h-0 w-160" /> <li class="mx-16 h-0 w-160"></li>
<li class="mx-16 h-0 w-160" /> <li class="mx-16 h-0 w-160"></li>
<li class="mx-16 h-0 w-160" /> <li class="mx-16 h-0 w-160"></li>
<li class="mx-16 h-0 w-160" /> <li class="mx-16 h-0 w-160"></li>
</ul> </ul>
</CommonPage> </CommonPage>
</template> </template>

View File

@ -18,9 +18,7 @@
<n-upload-dragger> <n-upload-dragger>
<div class="h-150 f-c-c flex-col"> <div class="h-150 f-c-c flex-col">
<i class="i-mdi:upload mb-12 text-68 color-primary" /> <i class="i-mdi:upload mb-12 text-68 color-primary" />
<n-text class="text-14 color-gray"> <n-text class="text-14 color-gray">点击或者拖动文件到该区域来上传</n-text>
点击或者拖动文件到该区域来上传
</n-text>
</div> </div>
</n-upload-dragger> </n-upload-dragger>
</n-upload> </n-upload>
@ -33,9 +31,7 @@
<n-image width="200" :src="item.url" /> <n-image width="200" :src="item.url" />
</div> </div>
<n-space class="mt-16" justify="space-evenly"> <n-space class="mt-16" justify="space-evenly">
<n-button dashed type="primary" @click="copy(item.url)"> <n-button dashed type="primary" @click="copy(item.url)">url</n-button>
url
</n-button>
<n-button dashed type="primary" @click="copy(`![${item.fileName}](${item.url})`)"> <n-button dashed type="primary" @click="copy(`![${item.fileName}](${item.url})`)">
MD MD
</n-button> </n-button>
@ -57,7 +53,6 @@
<script setup> <script setup>
import { useClipboard } from '@vueuse/core' import { useClipboard } from '@vueuse/core'
defineOptions({ name: 'ImgUpload' }) defineOptions({ name: 'ImgUpload' })
const { copy, copied } = useClipboard() const { copy, copied } = useClipboard()

View File

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

View File

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

View File

@ -20,12 +20,8 @@
</div> </div>
</div> </div>
<p class="mt-28 text-14 opacity-60"> <p class="mt-28 text-14 opacity-60">一个人几乎可以在任何他怀有无限热忱的事情上成功</p>
一个人几乎可以在任何他怀有无限热忱的事情上成功 <p class="mt-12 text-right text-12 opacity-40"> 查尔斯·史考伯</p>
</p>
<p class="mt-12 text-right text-12 opacity-40">
查尔斯·史考伯
</p>
</n-card> </n-card>
<n-card class="ml-12 w-70%" title="✨ 欢迎使用 Vue Naive Admin 2.0"> <n-card class="ml-12 w-70%" title="✨ 欢迎使用 Vue Naive Admin 2.0">
<template #header-extra> <template #header-extra>
@ -160,13 +156,13 @@
</template> </template>
<script setup> <script setup>
import { useUserStore } from '@/store'
import * as echarts from 'echarts/core' import * as echarts from 'echarts/core'
import { GridComponent, LegendComponent, TooltipComponent } from 'echarts/components' import { TooltipComponent, GridComponent, LegendComponent } from 'echarts/components'
import { BarChart, LineChart, PieChart } from 'echarts/charts' import { BarChart, LineChart, PieChart } from 'echarts/charts'
import { UniversalTransition } from 'echarts/features' import { UniversalTransition } from 'echarts/features'
import { CanvasRenderer } from 'echarts/renderers' import { CanvasRenderer } from 'echarts/renderers'
import VChart from 'vue-echarts' import VChart from 'vue-echarts'
import { useUserStore } from '@/store'
const userStore = useUserStore() const userStore = useUserStore()

View File

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

View File

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

View File

@ -12,12 +12,12 @@
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" 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"> <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>
<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" class="mr-12 h-50"> <img src="@/assets/images/logo.png" class="mr-12 h-50" />
{{ title }} {{ title }}
</h2> </h2>
<n-input <n-input
@ -64,7 +64,7 @@
height="40" height="40"
class="ml-12 w-80 cursor-pointer" class="ml-12 w-80 cursor-pointer"
@click="initCaptcha" @click="initCaptcha"
> />
</div> </div>
<n-checkbox <n-checkbox
@ -101,9 +101,9 @@
</template> </template>
<script setup> <script setup>
import { throttle, lStorage } from '@/utils'
import { useStorage } from '@vueuse/core' import { useStorage } from '@vueuse/core'
import api from './api' import api from './api'
import { lStorage, throttle } from '@/utils'
import { useAuthStore } from '@/store' import { useAuthStore } from '@/store'
const authStore = useAuthStore() const authStore = useAuthStore()
@ -118,7 +118,7 @@ const loginInfo = ref({
const captchaUrl = ref('') const captchaUrl = ref('')
const initCaptcha = throttle(() => { const initCaptcha = throttle(() => {
captchaUrl.value = `${import.meta.env.VITE_AXIOS_BASE_URL}/auth/captcha?${Date.now()}` captchaUrl.value = import.meta.env.VITE_AXIOS_BASE_URL + '/auth/captcha?' + Date.now()
}, 500) }, 500)
const localLoginInfo = lStorage.get('loginInfo') const localLoginInfo = lStorage.get('loginInfo')
@ -138,23 +138,19 @@ const isRemember = useStorage('isRemember', true)
const loading = ref(false) const loading = ref(false)
async function handleLogin(isQuick) { async function handleLogin(isQuick) {
const { username, password, captcha } = loginInfo.value const { username, password, captcha } = loginInfo.value
if (!username || !password) if (!username || !password) return $message.warning('请输入用户名和密码')
return $message.warning('请输入用户名和密码') if (!isQuick && !captcha) return $message.warning('请输入验证码')
if (!isQuick && !captcha)
return $message.warning('请输入验证码')
try { try {
loading.value = true loading.value = true
$message.loading('正在验证,请稍后...', { key: 'login' }) $message.loading('正在验证,请稍后...', { key: 'login' })
const { data } = await api.login({ username, password: password.toString(), captcha, isQuick }) const { data } = await api.login({ username, password: password.toString(), captcha, isQuick })
if (isRemember.value) { if (isRemember.value) {
lStorage.set('loginInfo', { username, password }) lStorage.set('loginInfo', { username, password })
} } else {
else {
lStorage.remove('loginInfo') lStorage.remove('loginInfo')
} }
onLoginSuccess(data) onLoginSuccess(data)
} } catch (error) {
catch (error) {
// 10003 // 10003
if (error?.code === 10003) { if (error?.code === 10003) {
// //
@ -175,12 +171,10 @@ async function onLoginSuccess(data = {}) {
const path = route.query.redirect const path = route.query.redirect
delete route.query.redirect delete route.query.redirect
router.push({ path, query: route.query }) router.push({ path, query: route.query })
} } else {
else {
router.push('/') router.push('/')
} }
} } catch (error) {
catch (error) {
console.error(error) console.error(error)
$message.destroy('login') $message.destroy('login')
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -9,10 +9,10 @@
<template> <template>
<CommonPage> <CommonPage>
<template #action> <template #action>
<NButton v-permission="'AddUser'" type="primary" @click="handleAdd()"> <n-button v-permission="'AddUser'" type="primary" @click="handleAdd()">
<i class="i-material-symbols:add mr-4 text-18" /> <i class="i-material-symbols:add mr-4 text-18" />
创建新用户 创建新用户
</NButton> </n-button>
</template> </template>
<MeCrud <MeCrud
@ -92,14 +92,10 @@
/> />
</n-form-item> </n-form-item>
<n-form-item v-if="modalAction === 'add'" label="状态" path="enable"> <n-form-item v-if="modalAction === 'add'" label="状态" path="enable">
<NSwitch v-model:value="modalForm.enable"> <n-switch v-model:value="modalForm.enable">
<template #checked> <template #checked>启用</template>
启用 <template #unchecked>停用</template>
</template> </n-switch>
<template #unchecked>
停用
</template>
</NSwitch>
</n-form-item> </n-form-item>
</n-form> </n-form>
<n-alert v-if="modalAction === 'add'" type="warning" closable> <n-alert v-if="modalAction === 'add'" type="warning" closable>
@ -111,10 +107,10 @@
<script setup> <script setup>
import { NAvatar, NButton, NSwitch, NTag } from 'naive-ui' import { NAvatar, NButton, NSwitch, NTag } from 'naive-ui'
import api from './api'
import { formatDateTime } from '@/utils' import { formatDateTime } from '@/utils'
import { MeCrud, MeModal, MeQueryItem } from '@/components' import { MeCrud, MeQueryItem, MeModal } from '@/components'
import { useCrud } from '@/composables' import { useCrud } from '@/composables'
import api from './api'
defineOptions({ name: 'UserMgt' }) defineOptions({ name: 'UserMgt' })
@ -133,24 +129,6 @@ const genders = [
const roles = ref([]) const roles = ref([])
api.getAllRoles().then(({ data = [] }) => (roles.value = data)) api.getAllRoles().then(({ data = [] }) => (roles.value = data))
const {
modalRef,
modalFormRef,
modalForm,
modalAction,
handleAdd,
handleDelete,
handleOpen,
handleSave,
} = useCrud({
name: '用户',
initForm: { enable: true },
doCreate: api.create,
doDelete: api.delete,
doUpdate: api.update,
refresh: () => $table.value?.handleSearch(),
})
const columns = [ const columns = [
{ {
title: '头像', title: '头像',
@ -174,8 +152,8 @@ const columns = [
h( h(
NTag, NTag,
{ type: 'success', style: index > 0 ? 'margin-left: 8px;' : '' }, { type: 'success', style: index > 0 ? 'margin-left: 8px;' : '' },
{ default: () => item.name }, { default: () => item.name }
), )
) )
} }
return '暂无角色' return '暂无角色'
@ -185,7 +163,7 @@ const columns = [
title: '性别', title: '性别',
key: 'gender', key: 'gender',
width: 80, width: 80,
render: ({ gender }) => genders.find(item => gender === item.value)?.label ?? '', render: ({ gender }) => genders.find((item) => gender === item.value)?.label ?? '',
}, },
{ title: '邮箱', key: 'email', width: 150, ellipsis: { tooltip: true } }, { title: '邮箱', key: 'email', width: 150, ellipsis: { tooltip: true } },
{ {
@ -193,14 +171,14 @@ const columns = [
key: 'createDate', key: 'createDate',
width: 180, width: 180,
render(row) { render(row) {
return h('span', formatDateTime(row.createTime)) return h('span', formatDateTime(row['createTime']))
}, },
}, },
{ {
title: '状态', title: '状态',
key: 'enable', key: 'enable',
width: 120, width: 120,
render: row => render: (row) =>
h( h(
NSwitch, NSwitch,
{ {
@ -213,7 +191,7 @@ const columns = [
{ {
checked: () => '启用', checked: () => '启用',
unchecked: () => '停用', unchecked: () => '停用',
}, }
), ),
}, },
{ {
@ -236,7 +214,7 @@ const columns = [
{ {
default: () => '分配角色', default: () => '分配角色',
icon: () => h('i', { class: 'i-carbon:user-role text-14' }), icon: () => h('i', { class: 'i-carbon:user-role text-14' }),
}, }
), ),
h( h(
NButton, NButton,
@ -249,7 +227,7 @@ const columns = [
{ {
default: () => '重置密码', default: () => '重置密码',
icon: () => h('i', { class: 'i-radix-icons:reset text-14' }), icon: () => h('i', { class: 'i-radix-icons:reset text-14' }),
}, }
), ),
h( h(
@ -263,7 +241,7 @@ const columns = [
{ {
default: () => '删除', default: () => '删除',
icon: () => h('i', { class: 'i-material-symbols:delete-outline text-14' }), icon: () => h('i', { class: 'i-material-symbols:delete-outline text-14' }),
}, }
), ),
] ]
}, },
@ -277,14 +255,13 @@ async function handleEnable(row) {
row.enableLoading = false row.enableLoading = false
$message.success('操作成功') $message.success('操作成功')
$table.value?.handleSearch() $table.value?.handleSearch()
} } catch (error) {
catch (error) {
row.enableLoading = false row.enableLoading = false
} }
} }
function handleOpenRolesSet(row) { function handleOpenRolesSet(row) {
const roleIds = row.roles.map(item => item.id) const roleIds = row.roles.map((item) => item.id)
handleOpen({ handleOpen({
action: 'setRole', action: 'setRole',
title: '分配角色', title: '分配角色',
@ -293,14 +270,31 @@ function handleOpenRolesSet(row) {
}) })
} }
const {
modalRef,
modalFormRef,
modalForm,
modalAction,
handleAdd,
handleDelete,
handleOpen,
handleSave,
} = useCrud({
name: '用户',
initForm: { enable: true },
doCreate: api.create,
doDelete: api.delete,
doUpdate: api.update,
refresh: () => $table.value?.handleSearch(),
})
function onSave() { function onSave() {
if (modalAction.value === 'setRole') { if (modalAction.value === 'setRole') {
return handleSave({ return handleSave({
api: () => api.update(modalForm.value), api: () => api.update(modalForm.value),
cb: () => $message.success('分配成功'), cb: () => $message.success('分配成功'),
}) })
} } else if (modalAction.value === 'reset') {
else if (modalAction.value === 'reset') {
return handleSave({ return handleSave({
api: () => api.resetPwd(modalForm.value.id, modalForm.value), api: () => api.resetPwd(modalForm.value.id, modalForm.value),
cb: () => $message.success('密码重置成功'), cb: () => $message.success('密码重置成功'),

View File

@ -9,6 +9,6 @@
import { request } from '@/utils' import { request } from '@/utils'
export default { export default {
changePassword: data => request.post('/auth/password', data), changePassword: (data) => request.post('/auth/password', data),
updateProfile: data => request.patch(`/user/profile/${data.id}`, data), updateProfile: (data) => request.patch(`/user/profile/${data.id}`, data),
} }

View File

@ -21,9 +21,7 @@
</n-button> </n-button>
</div> </div>
<div class="mt-16 flex items-center"> <div class="mt-16 flex items-center">
<n-button type="primary" ghost @click="avatarModalRef.open()"> <n-button type="primary" ghost @click="avatarModalRef.open()">更改头像</n-button>
更改头像
</n-button>
<span class="ml-12 opacity-60"> <span class="ml-12 opacity-60">
修改头像只支持在线链接不提供上传图片功能如有需要可自行对接 修改头像只支持在线链接不提供上传图片功能如有需要可自行对接
</span> </span>
@ -46,18 +44,12 @@
:column="1" :column="1"
bordered bordered
> >
<n-descriptions-item label="昵称"> <n-descriptions-item label="昵称">{{ userStore.nickName }}</n-descriptions-item>
{{ userStore.nickName }}
</n-descriptions-item>
<n-descriptions-item label="性别"> <n-descriptions-item label="性别">
{{ genders.find((item) => item.value === userStore.userInfo?.gender)?.label ?? '未知' }} {{ genders.find((item) => item.value === userStore.userInfo?.gender)?.label ?? '未知' }}
</n-descriptions-item> </n-descriptions-item>
<n-descriptions-item label="地址"> <n-descriptions-item label="地址">{{ userStore.userInfo?.address }}</n-descriptions-item>
{{ userStore.userInfo?.address }} <n-descriptions-item label="邮箱">{{ userStore.userInfo?.email }}</n-descriptions-item>
</n-descriptions-item>
<n-descriptions-item label="邮箱">
{{ userStore.userInfo?.email }}
</n-descriptions-item>
</n-descriptions> </n-descriptions>
</n-card> </n-card>
@ -105,11 +97,11 @@
</template> </template>
<script setup> <script setup>
import api from './api'
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 { getUserInfo } from '@/store/helper'
import api from './api'
const userStore = useUserStore() const userStore = useUserStore()
const required = { const required = {

View File

@ -6,7 +6,7 @@
* Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top * Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top
**********************************/ **********************************/
import { defineConfig, presetAttributify, presetIcons, presetUno } from 'unocss' import { defineConfig, presetUno, presetIcons, presetAttributify } from 'unocss'
import presetRemToPx from '@unocss/preset-rem-to-px' import presetRemToPx from '@unocss/preset-rem-to-px'
import { FileSystemIconLoader } from '@iconify/utils/lib/loader/node-loaders' import { FileSystemIconLoader } from '@iconify/utils/lib/loader/node-loaders'
import { getIcons } from './build' import { getIcons } from './build'
@ -29,7 +29,7 @@ export default defineConfig({
}), }),
presetRemToPx({ baseFontSize: 4 }), presetRemToPx({ baseFontSize: 4 }),
], ],
safelist: icons.map(icon => `${icon} ${icon}?mask`.split(' ')).flat(), safelist: icons.map((icon) => `${icon} ${icon}?mask`.split(' ')).flat(),
shortcuts: [ shortcuts: [
['wh-full', 'w-full h-full'], ['wh-full', 'w-full h-full'],
['f-c-c', 'flex justify-center items-center'], ['f-c-c', 'flex justify-center items-center'],

View File

@ -6,7 +6,7 @@
* Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top * Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top
**********************************/ **********************************/
import path from 'node:path' import path from 'path'
import { defineConfig, loadEnv } from 'vite' import { defineConfig, loadEnv } from 'vite'
import Vue from '@vitejs/plugin-vue' import Vue from '@vitejs/plugin-vue'
import VueDevTools from 'vite-plugin-vue-devtools' import VueDevTools from 'vite-plugin-vue-devtools'
@ -15,7 +15,7 @@ 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 removeNoMatch from 'vite-plugin-router-warn' import removeNoMatch from 'vite-plugin-router-warn'
import { pluginIcons, pluginPagePathes } from './build/plugin-isme' import { pluginPagePathes, pluginIcons } from './build/plugin-isme'
export default defineConfig(({ mode }) => { export default defineConfig(({ mode }) => {
const viteEnv = loadEnv(mode, process.cwd()) const viteEnv = loadEnv(mode, process.cwd())
@ -56,7 +56,7 @@ export default defineConfig(({ mode }) => {
'/api': { '/api': {
target: VITE_PROXY_TARGET, target: VITE_PROXY_TARGET,
changeOrigin: true, changeOrigin: true,
rewrite: path => path.replace(/^\/api/, ''), rewrite: (path) => path.replace(new RegExp('^/api'), ''),
secure: false, secure: false,
configure: (proxy, options) => { configure: (proxy, options) => {
// 配置此项可在响应头中看到请求的真实地址 // 配置此项可在响应头中看到请求的真实地址