1
0
mirror of https://github.com/zclzone/vue-naive-admin.git synced 2025-12-28 12:10:20 +08:00

2 Commits

Author SHA1 Message Date
leip247
a977ec1f3a fix: #113 (#116)
* fix(slot): 优化部分slot写法,减少手动判断

* fix(slot): 优化部分slot写法,减少手动判断

* feat(table): 添加vxe-table,适配naiveui主题,并替换掉之前table,自带导出

* feat(table): 添加vxe-table,适配naiveui主题,并替换掉之前table,自带导出

* fix: 完善table总条数不显示问题
2025-03-19 09:15:41 +08:00
leip247
6d1b3a4159 feat(table): 添加vxe-table,适配naiveui主题,并替换掉之前table,自带导出 (#105)
* fix(slot): 优化部分slot写法,减少手动判断

* fix(slot): 优化部分slot写法,减少手动判断

* feat(table): 添加vxe-table,适配naiveui主题,并替换掉之前table,自带导出

* feat(table): 添加vxe-table,适配naiveui主题,并替换掉之前table,自带导出
2024-11-22 15:47:29 +08:00
52 changed files with 4243 additions and 3238 deletions

View File

@@ -6,7 +6,7 @@ VITE_PUBLIC_PATH = '/'
# Axios 基础路径 # Axios 基础路径
# VITE_AXIOS_BASE_URL = '/api' # 用于代理 # VITE_AXIOS_BASE_URL = '/api' # 用于代理
VITE_AXIOS_BASE_URL = 'https://m1.apifoxmock.com/m1/3776410-3408296-default' # apifox云端mock VITE_AXIOS_BASE_URL = 'https://mock.apipark.cn/m1/3776410-0-default' # apifox云端mock
# 代理配置-target # 代理配置-target
VITE_PROXY_TARGET = 'http://localhost:8085' VITE_PROXY_TARGET = 'http://localhost:8085'

View File

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

View File

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

10
.vscode/settings.json vendored
View File

@@ -19,5 +19,13 @@
"markdown", "markdown",
"json", "json",
"jsonc" "jsonc"
] ],
"common-intellisense.ui": [
"naiveUi2"
],
"workbench.colorCustomizations": {
"activityBar.background": "#233212",
"titleBar.activeBackground": "#314619",
"titleBar.activeForeground": "#F9FCF6"
}
} }

View File

@@ -5,14 +5,8 @@
</p> </p>
<p align="center"> <p align="center">
<a href="./LICENSE"><img alt="MIT License" src="https://badgen.net/github/license/zclzone/vue-naive-admin"/></a> <a href="./LICENSE"><img alt="MIT License" src="https://badgen.net/github/license/zclzone/vue-naive-admin"/></a>
<a href="https://gitcode.com/zclzone/vue-naive-admin"><img alt="GitCode" src="https://gitcode.com/zclzone/vue-naive-admin/star/badge.svg
"/></a>
</p> </p>
---
<a href="https://hellogithub.com/repository/54f19ba1f9ae4238b3cbd111f3c428b0" target="_blank"><img src="https://abroad.hellogithub.com/v1/widgets/recommend.svg?rid=54f19ba1f9ae4238b3cbd111f3c428b0&claim_uid=jXGayRdJZScqMNr" alt="FeaturedHelloGitHub" style="width: 250px; height: 54px;" width="250" height="54" /></a>
## 简介 ## 简介
Vue Naive Admin 是一款极简风格的后台管理模板,包含前后端解决方案,前端使用 Vite + Vue3 + Pinia + Unocss后端使用 Nestjs + TypeOrm + MySql简单易用赏心悦目历经十几次重构和细节打磨诚意满满 Vue Naive Admin 是一款极简风格的后台管理模板,包含前后端解决方案,前端使用 Vite + Vue3 + Pinia + Unocss后端使用 Nestjs + TypeOrm + MySql简单易用赏心悦目历经十几次重构和细节打磨诚意满满
@@ -57,7 +51,6 @@ 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)
- 源码-gitcode: [isme-nest-serve | gitcode](https://gitcode.com/zclzone/isme-nest-serve)
## 文档 ## 文档

View File

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

View File

@@ -13,46 +13,49 @@
}, },
"dependencies": { "dependencies": {
"@arco-design/color": "^0.4.0", "@arco-design/color": "^0.4.0",
"@vueuse/core": "^14.1.0", "@vueuse/core": "^11.1.0",
"axios": "^1.13.2", "axios": "^1.7.7",
"dayjs": "^1.11.19", "dayjs": "^1.11.13",
"echarts": "^6.0.0", "echarts": "^5.5.1",
"lodash-es": "^4.17.22", "exceljs": "^4.4.0",
"naive-ui": "^2.43.2", "lodash-es": "^4.17.21",
"pinia": "^3.0.4", "naive-ui": "^2.40.1",
"pinia-plugin-persistedstate": "^4.7.1", "pinia": "^2.2.4",
"vue": "^3.5.26", "pinia-plugin-persistedstate": "^4.1.1",
"vue-echarts": "^8.0.1", "vue": "^3.5.12",
"vue-router": "^4.6.4", "vue-echarts": "^7.0.3",
"vue-router": "^4.4.5",
"vxe-pc-ui": "4.2.5",
"vxe-table": "4.8.13",
"vxe-table-plugin-export-xlsx": "^4.0.4",
"xlsx": "^0.18.5" "xlsx": "^0.18.5"
}, },
"devDependencies": { "devDependencies": {
"@antfu/eslint-config": "^6.7.3", "@antfu/eslint-config": "^3.8.0",
"@iconify/json": "^2.2.417", "@iconify/json": "^2.2.262",
"@unocss/eslint-config": "^66.5.10", "@unocss/eslint-config": "^0.63.6",
"@unocss/eslint-plugin": "^66.5.10", "@unocss/eslint-plugin": "^0.63.6",
"@unocss/preset-rem-to-px": "^66.5.10", "@unocss/preset-rem-to-px": "^0.63.6",
"@vitejs/plugin-vue": "^6.0.3", "@vitejs/plugin-vue": "^5.1.4",
"@vitejs/plugin-vue-jsx": "^5.1.2", "@vitejs/plugin-vue-jsx": "^4.0.1",
"eslint": "^9.39.2", "eslint": "^9.13.0",
"eslint-plugin-format": "^1.1.0", "eslint-plugin-format": "^0.1.2",
"esno": "^4.8.0", "esno": "^4.8.0",
"fs-extra": "^11.3.3", "fs-extra": "^11.2.0",
"glob": "^13.0.0", "glob": "^11.0.0",
"lint-staged": "^16.2.7", "lint-staged": "^15.2.10",
"rollup-plugin-visualizer": "^6.0.5", "rollup-plugin-visualizer": "^5.12.0",
"simple-git-hooks": "^2.13.1", "simple-git-hooks": "^2.11.1",
"taze": "^19.9.2", "taze": "^0.17.2",
"unocss": "^66.5.10", "unocss": "^0.63.6",
"unplugin-auto-import": "^20.3.0", "unplugin-auto-import": "^0.18.3",
"unplugin-vue-components": "^30.0.0", "unplugin-vue-components": "^0.27.4",
"vite": "^7.3.0", "vite": "^5.4.9",
"vite-plugin-router-warn": "^2.0.0", "vite-plugin-router-warn": "^1.0.0",
"vite-plugin-vue-devtools": "^8.0.5", "vite-plugin-vue-devtools": "^7.5.2"
"vue3-intro-step": "^1.0.5"
}, },
"simple-git-hooks": { "simple-git-hooks": {
"pre-commit": "npx lint-staged" "pre-commit": "pnpm lint-staged"
}, },
"lint-staged": { "lint-staged": {
"*": "eslint --fix" "*": "eslint --fix"

6548
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -29,9 +29,9 @@
</template> </template>
<script setup> <script setup>
import { darkTheme, dateZhCN, zhCN } from 'naive-ui'
import { LayoutSetting } from '@/components' import { LayoutSetting } from '@/components'
import { useAppStore, useTabStore } from '@/store' import { useAppStore, useTabStore } from '@/store'
import { darkTheme, dateZhCN, zhCN } from 'naive-ui'
import { layoutSettingVisible } from './settings' import { layoutSettingVisible } from './settings'
const layouts = new Map() const layouts = new Map()

View File

@@ -10,7 +10,7 @@
<div> <div>
<n-tooltip trigger="hover" placement="left"> <n-tooltip trigger="hover" placement="left">
<template #trigger> <template #trigger>
<div id="layout-setting" class="f-c-c rounded-4 bg-primary p-8" @click="modalRef.open()"> <div class="f-c-c rounded-4 bg-primary p-8" @click="modalRef.open()">
<i class="i-fe:settings cursor-pointer bg-white text-20" /> <i class="i-fe:settings cursor-pointer bg-white text-20" />
</div> </div>
</template> </template>

View File

@@ -0,0 +1,240 @@
<template>
<div
:style="isComputedHeight ? `height: calc(100% - ${autoHeight}px);` : `height: ${autoHeight}px`"
>
<vxe-grid
ref="gridRef"
:columns="vxeColumns"
:loading="loading"
:data="data"
height="auto"
:stripe="true"
:border="true"
size="mini"
:auto-resize="true"
:pager-config="pagerConfig"
:export-config="exportConfig"
:toolbar-config="vxeToolbarConfig"
:proxy-config="proxyConfig"
v-on="gridEvent"
>
<template #loading>
<n-spin :show="loading" />
</template>
<template #total>
<span class="font-size-14 line-height-28px"> {{ pagerConfig.total }} 条数据</span>
</template>
<template v-for="(c, k) in slots" :key="k" #[c.key]="{ row }">
<template v-if="Array.isArray(c.render(row))">
<component :is="r" v-for="(r, k1) in c.render(row)" :key="k1" :row="row" :col="c" />
</template>
<template v-else>
<component :is="c.render(row)" :row="row" :col="c" />
</template>
</template>
</vxe-grid>
</div>
</template>
<script setup>
import { computed } from 'vue'
defineOptions({ name: 'MeVxeGrid' })
const props = defineProps({
/**
* @remote 当isComputedHeight为true时为除table外的高度 false时为table高度
*/
autoHeight: {
type: Number,
default: 300,
},
/**
* @isPagination 是否为自动计算高度
*/
isComputedHeight: {
type: Boolean,
default: false,
},
loading: {
type: Boolean,
default: false,
},
/**
* @remote table的列
*/
columns: {
type: Array,
default: () => [],
},
data: {
type: Array,
default: () => [],
},
pagination: {
type: [Boolean, Object],
default: true,
},
exportConfig: {
type: Object,
default: () => ({}),
},
toolbarConfig: {
type: Object,
default: () => ({ }),
},
/**
* 是否显示复选框
*/
showCheck: {
type: Boolean,
default: false,
},
rowKey: {
type: String, // rowKey
default: 'id',
},
exportMethod: {
type: Function,
default: () => null,
},
remoteMethod: {
type: Function,
default: () => null,
},
})
const emits = defineEmits(['update:checked-row-keys', 'pageChanged'])
const isFirst = shallowRef(true)
const gridRef = ref(null)
watch(
() => props.data,
(n) => {
isFirst.value = false
gridRef.value.reloadData(n)
},
)
const vxeToolbarConfig = computed(() => {
return {
size: 'mini',
className: 'c-toolbar',
perfect: true,
refresh: true,
export: true,
zoom: true,
custom: true,
...props.toolbarConfig,
}
})
const pagerConfig = computed(() => {
if (props.pagination === false) {
return {
enabled: false,
}
}
return {
currentPage: props.pagination.pageNo,
pageSize: props.pagination.pageSize,
total: props.pagination.itemCount || 0,
background: true,
size: 'small',
layouts: [
'Home',
'PrevPage',
'JumpNumber',
'NextPage',
'End',
'Sizes',
'FullJump',
// 'Total',
],
slots: {
right: 'total',
},
}
})
/**
* 将naive 转为vxe-table可用的列
*/
const vxeColumns = computed(() => {
if (props.showCheck) {
return [{
type: 'checkbox',
resizable: false,
width: 45,
align: 'center',
fixed: 'left',
}, ...props.columns.map((item) => {
item.field = item.key
if (item.render) {
item.slots = { default: item.key }
}
return item
})]
}
return props.columns.map((item) => {
item.field = item.key
if (item.render) {
item.slots = { default: item.key }
}
return item
})
})
/**
* 将naive render函数转换为vxeTable的slots
*/
const slots = computed(() => {
return props.columns.filter(item => item.render)
})
const gridEvent = {
checkboxAll() {
const $grid = gridRef.value
if ($grid) {
emits(
'update:checked-row-keys',
$grid.getCheckboxRecords().map(item => item[props.rowKey]),
)
}
},
checkboxChange() {
const $grid = gridRef.value
if ($grid) {
emits(
'update:checked-row-keys',
$grid.getCheckboxRecords().map(item => item[props.rowKey]),
)
}
},
pageChange(e) {
emits('pageChanged', e)
},
}
const proxyConfig = {
props: {
result: 'data.pageData',
total: 'data.total',
},
ajax: {
query: async ({ page }) => {
if (isFirst.value === true) {
return Promise.reject('')
}
props.remoteMethod(page.currentPage, page.pageSize)
},
queryAll: async () => {
const res = await props.exportMethod()
return res
},
},
}
</script>
<style lang="scss" scoped>
</style>

View File

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

View File

@@ -1,6 +1,5 @@
<template> <template>
<i <i
id="toggleTheme"
class="mr-16 cursor-pointer" class="mr-16 cursor-pointer"
:class="isDark ? 'i-fe:moon' : 'i-fe:sun'" :class="isDark ? 'i-fe:moon' : 'i-fe:sun'"
@click="toggleDark" @click="toggleDark"
@@ -8,8 +7,8 @@
</template> </template>
<script setup> <script setup>
import { useDark, useToggle } from '@vueuse/core'
import { useAppStore } from '@/store' import { useAppStore } from '@/store'
import { useDark, useToggle } from '@vueuse/core'
const appStore = useAppStore() const appStore = useAppStore()
const isDark = useDark() const isDark = useDark()
@@ -39,7 +38,6 @@ async function toggleDark({ clientX, clientY }) {
duration: 500, duration: 500,
easing: 'ease-in', easing: 'ease-in',
pseudoElement: `::view-transition-${isDark.value ? 'old' : 'new'}(root)`, pseudoElement: `::view-transition-${isDark.value ? 'old' : 'new'}(root)`,
fill: 'both',
}, },
) )
} }

View File

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

View File

@@ -7,59 +7,101 @@
---------------------------------> --------------------------------->
<template> <template>
<div class="h-full flex flex-col overflow-hidden"> <div :class="computedHeight === true ? 'h-100%' : ''">
<AppCard v-if="$slots.default" bordered bg="#fafafc dark:black" class="mb-30 min-h-60 rounded-4"> <div id="search_main">
<form class="flex justify-between p-16" @submit.prevent="handleSearch()"> <AppCard v-if="$slots.default" bordered bg="#fafafc dark:black" class="mb-30 min-h-60 rounded-4">
<n-scrollbar x-scrollable> <form class="flex justify-between p-16" @submit.prevent="handleSearch()">
<n-space :wrap="!expand || isExpanded" :size="[32, 16]" class="p-10"> <n-scrollbar x-scrollable>
<slot /> <n-space :wrap="!expand || isExpanded" :size="[32, 16]" class="p-10">
</n-space> <slot />
</n-scrollbar> </n-space>
<div class="flex-shrink-0 p-10"> </n-scrollbar>
<n-button ghost type="primary" @click="handleReset"> <div class="flex-shrink-0 p-10">
<i class="i-fe:rotate-ccw mr-4" /> <n-button ghost type="primary" @click="handleReset">
重置 <i class="i-fe:rotate-ccw mr-4" />
</n-button> 重置
<n-button attr-type="submit" class="ml-20" type="primary">
<i class="i-fe:search mr-4" />
搜索
</n-button>
<template v-if="expand">
<n-button v-if="!isExpanded" type="primary" text @click="toggleExpand">
<i class="i-fe:chevrons-down ml-4" />
展开
</n-button> </n-button>
<n-button v-else text type="primary" @click="toggleExpand"> <n-button attr-type="submit" class="ml-20" type="primary">
<i class="i-fe:chevrons-up ml-4" /> <i class="i-fe:search mr-4" />
收起 搜索
</n-button> </n-button>
</template>
</div>
</form>
</AppCard>
<NDataTable <template v-if="expand">
<n-button v-if="!isExpanded" type="primary" text @click="toggleExpand">
<i class="i-fe:chevrons-down ml-4" />
展开
</n-button>
<n-button v-else text type="primary" @click="toggleExpand">
<i class="i-fe:chevrons-up ml-4" />
收起
</n-button>
</template>
</div>
</form>
</AppCard>
</div>
<MeVxeGrid
:remote="remote" :remote="remote"
:loading="loading" :loading="loading"
:scroll-x="scrollX" :scroll-x="scrollX"
:columns="columns" :columns="columns"
:export-config="{ filename: exportName, modes: ['all', 'current'] }"
:toolbar-config="toolbarConfig"
:data="tableData" :data="tableData"
:row-key="(row) => row[rowKey]" :auto-height="computedHeight === true ? autoHeight : computedHeight"
:row-key="rowKey"
:is-computed-height="computedHeight === true"
:show-check="showCheck"
:pagination="isPagination ? pagination : false" :pagination="isPagination ? pagination : false"
flex-height :remote-method="remoteMethod"
class="flex-1" :export-method="exportMethod"
@update:checked-row-keys="onChecked" @update:checked-row-keys="onChecked"
@update:page="onPageChange" @update:page="onPageChange"
/> />
<!-- <NDataTable
:remote="remote"
:loading="loading"
:scroll-x="scrollX"
:columns="columns"
:data="tableData"
:row-key="(row) => row[rowKey]"
:pagination="isPagination ? pagination : false"
@update:checked-row-keys="onChecked"
@update:page="onPageChange"
/> -->
</div> </div>
</template> </template>
<script setup> <script setup>
import { NDataTable } from 'naive-ui'
import { utils, writeFile } from 'xlsx' import { utils, writeFile } from 'xlsx'
const props = defineProps({ const props = defineProps({
/**
* 是否自动计算高度适用只有search和table
*/
computedHeight: {
type: [Boolean, Number],
default: () => true,
},
showCheck: {
type: Boolean,
default: () => false,
},
toolbarConfig: {
type: Object,
default: () => ({
export: true,
zoom: true,
}),
},
/**
* @remote 导出数据表格名称
*/
exportName: {
type: String,
default: '导出数据',
},
/** /**
* @remote true: 后端分页 false 前端分页 * @remote true: 后端分页 false 前端分页
*/ */
@@ -109,8 +151,18 @@ const props = defineProps({
/** 是否支持展开 */ /** 是否支持展开 */
expand: Boolean, expand: Boolean,
}) })
const emit = defineEmits(['update:queryItems', 'onChecked', 'onDataChange']) const emit = defineEmits(['update:queryItems', 'onChecked', 'onDataChange'])
const autoHeight = ref(0)
window.onresize = () => {
autoHeight.value = document.getElementById('search_main').clientHeight + 24
}
onMounted(() => {
autoHeight.value = document.getElementById('search_main').clientHeight + 24
})
const loading = ref(false) const loading = ref(false)
const initQuery = { ...props.queryItems } const initQuery = { ...props.queryItems }
const tableData = ref([]) const tableData = ref([])
@@ -129,6 +181,22 @@ function toggleExpand() {
isExpanded.value = !isExpanded.value isExpanded.value = !isExpanded.value
} }
async function exportMethod() {
const { data } = await props.getData({
pageNo: 1,
pageSize: 2000,
...props.queryItems,
})
return data
}
function remoteMethod(page, pageSize) {
pagination.page = page
pagination.pageSize = pageSize
handleQuery()
}
async function handleQuery() { async function handleQuery() {
try { try {
loading.value = true loading.value = true
@@ -147,6 +215,7 @@ async function handleQuery() {
// 如果当前页数据为空且总条数不为0则返回上一页数据 // 如果当前页数据为空且总条数不为0则返回上一页数据
onPageChange(pagination.page - 1) onPageChange(pagination.page - 1)
} }
return data
} }
catch (error) { catch (error) {
console.error(error) console.error(error)
@@ -184,9 +253,7 @@ function onPageChange(currentPage) {
} }
} }
function onChecked(rowKeys) { function onChecked(rowKeys) {
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)

View File

@@ -6,7 +6,6 @@
* Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top * Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top
**********************************/ **********************************/
import { cloneDeep } from 'lodash-es'
import { useForm, useModal } from '.' import { useForm, useModal } from '.'
const ACTIONS = { const ACTIONS = {
@@ -22,7 +21,7 @@ export function useCrud({ name, initForm = {}, doCreate, doDelete, doUpdate, ref
/** 新增 */ /** 新增 */
function handleAdd(row = {}, title) { function handleAdd(row = {}, title) {
handleOpen({ action: 'add', title, row: Object.assign({}, cloneDeep(initForm), cloneDeep(row)) }) handleOpen({ action: 'add', title, row: { ...initForm, ...row } })
} }
/** 修改 */ /** 修改 */

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,5 @@
<template> <template>
<i <i
id="fullscreen"
class="mr-16 cursor-pointer" class="mr-16 cursor-pointer"
:class="isFullscreen ? 'i-fe:minimize' : 'i-fe:maximize'" :class="isFullscreen ? 'i-fe:minimize' : 'i-fe:maximize'"
@click="toggle" @click="toggle"

View File

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

View File

@@ -54,8 +54,6 @@ function handleMenuSelect(key, item) {
}) })
} }
else { else {
if (!item.path)
return
router.push(item.path) router.push(item.path)
} }
} }

View File

@@ -8,7 +8,7 @@
<template> <template>
<n-dropdown :options="options" @select="handleSelect"> <n-dropdown :options="options" @select="handleSelect">
<div id="user-dropdown" class="flex cursor-pointer items-center"> <div class="flex cursor-pointer items-center">
<n-avatar round :size="36" :src="userStore.avatar" /> <n-avatar round :size="36" :src="userStore.avatar" />
<div v-if="userStore.userInfo" class="ml-12 flex-col flex-shrink-0 items-center"> <div v-if="userStore.userInfo" class="ml-12 flex-col flex-shrink-0 items-center">
<span class="text-14">{{ userStore.nickName ?? userStore.username }}</span> <span class="text-14">{{ userStore.nickName ?? userStore.username }}</span>

View File

@@ -1,4 +1,3 @@
export { default as BeginnerGuide } from './BeginnerGuide.vue'
export { default as BreadCrumb } from './BreadCrumb.vue' export { default as BreadCrumb } from './BreadCrumb.vue'
export { default as Fullscreen } from './Fullscreen.vue' export { default as Fullscreen } from './Fullscreen.vue'
export { default as MenuCollapse } from './MenuCollapse.vue' export { default as MenuCollapse } from './MenuCollapse.vue'

View File

@@ -7,7 +7,7 @@
---------------------------------> --------------------------------->
<template> <template>
<div id="top-tab"> <div>
<n-tabs <n-tabs
:value="tabStore.activeTab" :value="tabStore.activeTab"
:closable="tabStore.tabs.length > 1" :closable="tabStore.tabs.length > 1"

View File

@@ -13,8 +13,6 @@
<BreadCrumb /> <BreadCrumb />
<div class="ml-auto flex flex-shrink-0 items-center px-12 text-18"> <div class="ml-auto flex flex-shrink-0 items-center px-12 text-18">
<BeginnerGuide />
<ToggleTheme /> <ToggleTheme />
<Fullscreen /> <Fullscreen />
@@ -37,7 +35,7 @@
<script setup> <script setup>
import { ToggleTheme } from '@/components' import { ToggleTheme } from '@/components'
import { BeginnerGuide, BreadCrumb, Fullscreen, MenuCollapse, UserAvatar } from '@/layouts/components' import { BreadCrumb, Fullscreen, MenuCollapse, UserAvatar } from '@/layouts/components'
function handleLinkClick(link) { function handleLinkClick(link) {
window.open(link) window.open(link)

View File

@@ -15,8 +15,6 @@
<span class="mx-6 opacity-20">|</span> <span class="mx-6 opacity-20">|</span>
<div class="flex flex-shrink-0 items-center px-12 text-18"> <div class="flex flex-shrink-0 items-center px-12 text-18">
<BeginnerGuide />
<ToggleTheme /> <ToggleTheme />
<Fullscreen /> <Fullscreen />
@@ -39,7 +37,7 @@
<script setup> <script setup>
import { ToggleTheme } from '@/components' import { ToggleTheme } from '@/components'
import { AppTab, BeginnerGuide, Fullscreen, MenuCollapse, UserAvatar } from '@/layouts/components' import { AppTab, Fullscreen, MenuCollapse, UserAvatar } from '@/layouts/components'
function handleLinkClick(link) { function handleLinkClick(link) {
window.open(link) window.open(link)

View File

@@ -8,19 +8,35 @@
* Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top * Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top
**********************************/ **********************************/
import ExcelJS from 'exceljs'
import { createApp } from 'vue' import { createApp } from 'vue'
import App from './App.vue' import App from './App.vue'
import { setupDirectives } from './directives'
import { setupDirectives } from './directives'
/**
* 引入vxe-table 和 vxe-pc-ui excel导出
*/
import VxeUI from 'vxe-pc-ui'
import VXETable from 'vxe-table'
import VXETablePluginExportXLSX from 'vxe-table-plugin-export-xlsx'
import { setupRouter } from './router' import { setupRouter } from './router'
import { setupStore } from './store' import { setupStore } from './store'
import { setupNaiveDiscreteApi } from './utils' import { setupNaiveDiscreteApi } from './utils'
import 'vxe-pc-ui/lib/style.css'
import 'vxe-table/lib/style.css'
import '@/styles/reset.css' import '@/styles/reset.css'
import '@/styles/global.css' import '@/styles/global.css'
import 'uno.css' import 'uno.css'
async function bootstrap() { async function bootstrap() {
const app = createApp(App) const app = createApp(App)
VXETable.use(VXETablePluginExportXLSX, {
ExcelJS,
})
app.use(VxeUI).use(VXETable)
setupStore(app) setupStore(app)
setupDirectives(app) setupDirectives(app)
await setupRouter(app) await setupRouter(app)

View File

@@ -1,4 +1,3 @@
import { cloneDeep } from 'lodash-es'
import api from '@/api' import api from '@/api'
import { basePermissions } from '@/settings' import { basePermissions } from '@/settings'
@@ -27,5 +26,5 @@ export async function getPermissions() {
catch (error) { catch (error) {
console.error(error) console.error(error)
} }
return cloneDeep(basePermissions).concat(asyncPermissions) return basePermissions.concat(asyncPermissions)
} }

View File

@@ -6,10 +6,11 @@
* Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top * Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top
**********************************/ **********************************/
import { defaultLayout, defaultPrimaryColor, naiveThemeOverrides } from '@/settings'
import { generate, getRgbStr } from '@arco-design/color' import { generate, getRgbStr } from '@arco-design/color'
import { useDark } from '@vueuse/core' import { useDark } from '@vueuse/core'
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import { defaultLayout, defaultPrimaryColor, naiveThemeOverrides } from '@/settings' import { VxeUI } from 'vxe-pc-ui'
export const useAppStore = defineStore('app', { export const useAppStore = defineStore('app', {
state: () => ({ state: () => ({
@@ -40,6 +41,18 @@ export const useAppStore = defineStore('app', {
list: true, list: true,
dark: isDark, dark: isDark,
}) })
VxeUI.setTheme(isDark ? 'dark' : 'light')
if (isDark) {
document.body.style.setProperty('--vxe-ui-font-color', '#fff')
}
else {
document.body.style.setProperty('--vxe-ui-font-color', '#000')
}
document.body.style.setProperty('--vxe-ui-font-primary-color', color)
document.body.style.setProperty('--vxe-ui-font-primary-lighten-color', color)
document.body.style.setProperty('--vxe-ui-font-primary-darken-color', color)
document.body.style.setProperty('--primary-color', getRgbStr(colors[5])) document.body.style.setProperty('--primary-color', getRgbStr(colors[5]))
this.naiveThemeOverrides.common = Object.assign(this.naiveThemeOverrides.common || {}, { this.naiveThemeOverrides.common = Object.assign(this.naiveThemeOverrides.common || {}, {
primaryColor: colors[5], primaryColor: colors[5],
@@ -50,7 +63,7 @@ export const useAppStore = defineStore('app', {
}, },
}, },
persist: { persist: {
pick: ['collapsed', 'layout', 'primaryColor', 'naiveThemeOverrides'], paths: ['collapsed', 'layout', 'primaryColor', 'naiveThemeOverrides'],
storage: sessionStorage, storage: sessionStorage,
}, },
}) })

View File

@@ -6,8 +6,8 @@
* Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top * Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top
**********************************/ **********************************/
import { defineStore } from 'pinia'
import { usePermissionStore, useRouterStore, useTabStore, useUserStore } from '@/store' import { usePermissionStore, useRouterStore, useTabStore, useUserStore } from '@/store'
import { defineStore } from 'pinia'
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: () => ({
@@ -29,6 +29,8 @@ export const usePermissionStore = defineStore('permission', {
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
const menuItem = { const menuItem = {
label: route.meta.title, label: route.meta.title,
key: route.name, key: route.name,
@@ -46,8 +48,6 @@ export const usePermissionStore = defineStore('permission', {
if (!menuItem.children.length) if (!menuItem.children.length)
delete menuItem.children delete menuItem.children
} }
if (!item.show)
return null
return menuItem return menuItem
}, },
generateRoute(item, parentKey) { generateRoute(item, parentKey) {

View File

@@ -88,7 +88,7 @@ export const useTabStore = defineStore('tab', {
}, },
}, },
persist: { persist: {
pick: ['tabs'], paths: ['tabs'],
storage: sessionStorage, storage: sessionStorage,
}, },
}) })

View File

@@ -34,8 +34,8 @@ html,
-webkit-text-size-adjust: 100%; /* 2 */ -webkit-text-size-adjust: 100%; /* 2 */
-moz-tab-size: 4; /* 3 */ -moz-tab-size: 4; /* 3 */
tab-size: 4; /* 3 */ tab-size: 4; /* 3 */
font-family: font-family: ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol',
ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'; /* 4 */ 'Noto Color Emoji'; /* 4 */
font-feature-settings: normal; /* 5 */ font-feature-settings: normal; /* 5 */
font-variation-settings: normal; /* 6 */ font-variation-settings: normal; /* 6 */
-webkit-tap-highlight-color: transparent; /* 7 */ -webkit-tap-highlight-color: transparent; /* 7 */
@@ -114,8 +114,7 @@ code,
kbd, kbd,
samp, samp,
pre { pre {
font-family: font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace; /* 1 */
ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace; /* 1 */
font-feature-settings: normal; /* 2 */ font-feature-settings: normal; /* 2 */
font-variation-settings: normal; /* 3 */ font-variation-settings: normal; /* 3 */
font-size: 1em; /* 4 */ font-size: 1em; /* 4 */

View File

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

View File

@@ -7,9 +7,9 @@
* Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top * Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top
**********************************/ **********************************/
import * as NaiveUI from 'naive-ui'
import { useAppStore } from '@/store' import { useAppStore } from '@/store'
import { isNullOrUndef } from '@/utils' import { isNullOrUndef } from '@/utils'
import * as NaiveUI from 'naive-ui'
export function setupMessage(NMessage) { export function setupMessage(NMessage) {
class Message { class Message {

View File

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

View File

@@ -98,7 +98,7 @@
<script setup> <script setup>
import { sleep } from '@/utils' import { sleep } from '@/utils'
function handleDelete() { const handleDelete = function () {
$dialog.confirm({ $dialog.confirm({
content: '确认删除?', content: '确认删除?',
confirm() { confirm() {

View File

@@ -26,7 +26,7 @@
<li <li
v-for="item in icons" v-for="item in icons"
:key="item" :key="item"
class="w-160 f-c-c flex-col cursor-pointer card-border rounded-12 auto-bg px-12 py-24" class="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" />

View File

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

View File

@@ -62,7 +62,11 @@ defineOptions({ name: 'ImgUpload' })
const { copy, copied } = useClipboard() const { copy, copied } = useClipboard()
const imgList = reactive([]) const imgList = reactive([
{ url: 'https://img.isme.top/isme/67208863145ef.jpg' },
{ url: 'https://img.isme.top/isme/67208ab2a9de0.jpg' },
{ url: 'https://img.isme.top/isme/67208ab4c6596.jpg' },
])
watch(copied, (val) => { watch(copied, (val) => {
if (val) if (val)

View File

@@ -30,9 +30,10 @@
<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>
<a <a
class="text-highlight text-14 text-primary hover:underline hover:opacity-80" class="text-14 text-primary text-highlight hover:underline hover:opacity-80"
href="https://isme.top" href="https://isme.top"
target="_blank" target="_blank"
@click.prevent="message?.info('官网正在火速开发中...')"
> >
isme.top isme.top
</a> </a>
@@ -68,7 +69,7 @@
<div class="mt-12 flex"> <div class="mt-12 flex">
<n-card class="w-50%" title="💯 特性" segmented> <n-card class="w-50%" title="💯 特性" segmented>
<template #header-extra> <template #header-extra>
<span class="text-highlight opacity-90">👏 历经十几次重构和细节打磨</span> <span class="opacity-90 text-highlight">👏 历经十几次重构和细节打磨</span>
</template> </template>
<ul class="opacity-90"> <ul class="opacity-90">
@@ -159,13 +160,13 @@
</template> </template>
<script setup> <script setup>
import { useUserStore } from '@/store'
import { BarChart, LineChart, PieChart } from 'echarts/charts' import { BarChart, LineChart, PieChart } from 'echarts/charts'
import { GridComponent, LegendComponent, TooltipComponent } from 'echarts/components' import { GridComponent, LegendComponent, TooltipComponent } from 'echarts/components'
import * as echarts from 'echarts/core' import * as echarts from 'echarts/core'
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()
@@ -250,7 +251,7 @@ const skillOption = {
}, },
series: [ series: [
{ {
bottom: '12%', top: '12%',
type: 'pie', type: 'pie',
radius: ['35%', '90%'], radius: ['35%', '90%'],
avoidLabelOverlap: true, avoidLabelOverlap: true,
@@ -283,4 +284,6 @@ const skillOption = {
}, },
], ],
} }
const message = $message
</script> </script>

View File

@@ -9,7 +9,7 @@
<template> <template>
<div class="wh-full flex-col bg-[url(@/assets/images/login_bg.webp)] bg-cover"> <div class="wh-full flex-col bg-[url(@/assets/images/login_bg.webp)] bg-cover">
<div <div
class="m-auto max-w-700 min-w-345 f-c-c rounded-8 auto-bg bg-opacity-20 bg-cover p-12 card-shadow" class="m-auto max-w-700 min-w-345 f-c-c rounded-8 bg-opacity-20 bg-cover p-12 card-shadow auto-bg"
> >
<div class="hidden w-380 px-20 py-35 md:block"> <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">
@@ -101,9 +101,9 @@
</template> </template>
<script setup> <script setup>
import { useStorage } from '@vueuse/core'
import { useAuthStore } from '@/store' import { useAuthStore } from '@/store'
import { lStorage, throttle } from '@/utils' import { lStorage, throttle } from '@/utils'
import { useStorage } from '@vueuse/core'
import api from './api' import api from './api'
const authStore = useAuthStore() const authStore = useAuthStore()

View File

@@ -6,8 +6,8 @@
* Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top * Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top
**********************************/ **********************************/
import axios from 'axios'
import { request } from '@/utils' import { request } from '@/utils'
import axios from 'axios'
export default { export default {
getMenuTree: () => request.get('/permission/menu/tree'), getMenuTree: () => request.get('/permission/menu/tree'),

View File

@@ -159,10 +159,10 @@
</template> </template>
<script setup> <script setup>
import icons from 'isme:icons'
import pagePathes from 'isme:page-pathes'
import { MeModal } from '@/components' import { MeModal } from '@/components'
import { useForm, useModal } from '@/composables' import { useForm, useModal } from '@/composables'
import icons from 'isme:icons'
import pagePathes from 'isme:page-pathes'
import api from '../api' import api from '../api'
import QuestionLabel from './QuestionLabel.vue' import QuestionLabel from './QuestionLabel.vue'

View File

@@ -81,6 +81,7 @@
:columns="btnsColumns" :columns="btnsColumns"
:scroll-x="-1" :scroll-x="-1"
:get-data="api.getButtons" :get-data="api.getButtons"
:computed-height="300"
:query-items="{ parentId: currentMenu.id }" :query-items="{ parentId: currentMenu.id }"
/> />
</template> </template>
@@ -92,8 +93,8 @@
</template> </template>
<script setup> <script setup>
import { NButton, NSwitch } from 'naive-ui'
import { MeCrud } from '@/components' import { MeCrud } from '@/components'
import { NButton, NSwitch } from 'naive-ui'
import api from './api' import api from './api'
import MenuTree from './components/MenuTree.vue' import MenuTree from './components/MenuTree.vue'
import ResAddOrEdit from './components/ResAddOrEdit.vue' import ResAddOrEdit from './components/ResAddOrEdit.vue'

View File

@@ -75,7 +75,7 @@
:checked-keys="modalForm.permissionIds" :checked-keys="modalForm.permissionIds"
:on-update:checked-keys="(keys) => (modalForm.permissionIds = keys)" :on-update:checked-keys="(keys) => (modalForm.permissionIds = keys)"
checkable check-on-click default-expand-all default-expand-all checkable check-on-click
class="cus-scroll max-h-200 w-full" class="cus-scroll max-h-200 w-full"
/> />
</n-form-item> </n-form-item>
@@ -95,9 +95,9 @@
</template> </template>
<script setup> <script setup>
import { NButton, NSwitch } from 'naive-ui'
import { MeCrud, MeModal, MeQueryItem } from '@/components' import { MeCrud, MeModal, MeQueryItem } from '@/components'
import { useCrud } from '@/composables' import { useCrud } from '@/composables'
import { NButton, NSwitch } from 'naive-ui'
import api from './api' import api from './api'
defineOptions({ name: 'RoleMgt' }) defineOptions({ name: 'RoleMgt' })

View File

@@ -37,6 +37,7 @@
:scroll-x="1200" :scroll-x="1200"
:columns="columns" :columns="columns"
:get-data="api.getAllUsers" :get-data="api.getAllUsers"
:show-check="true"
@on-checked="onChecked" @on-checked="onChecked"
> >
<MeQueryItem label="用户名" :label-width="50"> <MeQueryItem label="用户名" :label-width="50">
@@ -67,10 +68,10 @@
</template> </template>
<script setup> <script setup>
import { NAvatar, NButton, NSwitch, NTag } from 'naive-ui'
import { h } from 'vue'
import { MeCrud, MeQueryItem } from '@/components' import { MeCrud, MeQueryItem } from '@/components'
import { formatDateTime } from '@/utils' import { formatDateTime } from '@/utils'
import { NAvatar, NButton, NSwitch, NTag } from 'naive-ui'
import { h } from 'vue'
import api from './api' import api from './api'
defineOptions({ name: 'RoleUser' }) defineOptions({ name: 'RoleUser' })
@@ -90,7 +91,7 @@ const genders = [
] ]
const columns = [ const columns = [
{ type: 'selection', fixed: 'left' }, // { type: 'selection', fixed: 'left' },
{ {
title: '头像', title: '头像',
key: 'avatar', key: 'avatar',
@@ -101,11 +102,10 @@ const columns = [
src: avatar, src: avatar,
}), }),
}, },
{ title: '用户名', key: 'username', width: 150, ellipsis: { tooltip: true } }, { title: '用户名', key: 'username', ellipsis: { tooltip: true } },
{ {
title: '角色', title: '角色',
key: 'roles', key: 'roles',
width: 200,
ellipsis: { tooltip: true }, ellipsis: { tooltip: true },
render: ({ roles }) => { render: ({ roles }) => {
if (roles?.length) { if (roles?.length) {
@@ -156,38 +156,38 @@ const columns = [
{ {
title: '操作', title: '操作',
key: 'actions', key: 'actions',
width: 100, width: 120,
align: 'right', align: 'right',
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' }),
}, },
) )
}, },
}, },
] ]

View File

@@ -77,7 +77,7 @@
trigger: ['input', 'blur'], trigger: ['input', 'blur'],
}" }"
> >
<n-input v-model:value="modalForm.password" type="password" show-password-on="mousedown" /> <n-input v-model:value="modalForm.password" />
</n-form-item> </n-form-item>
<n-form-item v-if="['add', 'setRole'].includes(modalAction)" label="角色" path="roleIds"> <n-form-item v-if="['add', 'setRole'].includes(modalAction)" label="角色" path="roleIds">
@@ -110,11 +110,10 @@
</template> </template>
<script setup> <script setup>
import { NAvatar, NButton, NSwitch, NTag } from 'naive-ui'
import { MeCrud, MeModal, MeQueryItem } from '@/components' import { MeCrud, MeModal, MeQueryItem } from '@/components'
import { useCrud } from '@/composables' import { useCrud } from '@/composables'
import { withPermission } from '@/directives'
import { formatDateTime } from '@/utils' import { formatDateTime } from '@/utils'
import { NAvatar, NButton, NSwitch, NTag } from 'naive-ui'
import api from './api' import api from './api'
defineOptions({ name: 'UserMgt' }) defineOptions({ name: 'UserMgt' })
@@ -163,11 +162,10 @@ const columns = [
src: avatar, src: avatar,
}), }),
}, },
{ title: '用户名', key: 'username', width: 150, ellipsis: { tooltip: true } }, { title: '用户名', key: 'username', ellipsis: { tooltip: true } },
{ {
title: '角色', title: '角色',
key: 'roles', key: 'roles',
width: 200,
ellipsis: { tooltip: true }, ellipsis: { tooltip: true },
render: ({ roles }) => { render: ({ roles }) => {
if (roles?.length) { if (roles?.length) {
@@ -220,29 +218,17 @@ const columns = [
{ {
title: '操作', title: '操作',
key: 'actions', key: 'actions',
width: 420, width: 320,
align: 'right', align: 'right',
fixed: 'right', fixed: 'right',
hideInExcel: true, hideInExcel: true,
render(row) { render(row) {
return [ return [
withPermission(
h(NButton, {
size: 'small',
type: 'primary',
secondary: true,
}, {
default: () => '超管专属',
icon: () => h('i', { class: 'i-carbon:user-role text-14' }),
}),
'SuperAdmin',
),
h( h(
NButton, NButton,
{ {
size: 'small', size: 'small',
type: 'primary', type: 'primary',
class: 'ml-12px',
secondary: true, secondary: true,
onClick: () => handleOpenRolesSet(row), onClick: () => handleOpenRolesSet(row),
}, },

View File

@@ -73,10 +73,10 @@
require-mark-placement="left" require-mark-placement="left"
> >
<n-form-item label="原密码" path="oldPassword" :rule="required"> <n-form-item label="原密码" path="oldPassword" :rule="required">
<n-input v-model:value="pwdForm.oldPassword" type="password" placeholder="请输入原密码" show-password-on="mousedown" /> <n-input v-model:value="pwdForm.oldPassword" type="password" placeholder="请输入原密码" />
</n-form-item> </n-form-item>
<n-form-item label="新密码" path="newPassword" :rule="required"> <n-form-item label="新密码" path="newPassword" :rule="required">
<n-input v-model:value="pwdForm.newPassword" type="password" placeholder="请输入新密码" show-password-on="mousedown" /> <n-input v-model:value="pwdForm.newPassword" type="password" placeholder="请输入新密码" />
</n-form-item> </n-form-item>
</n-form> </n-form>
</MeModal> </MeModal>

View File

@@ -8,13 +8,13 @@
import { FileSystemIconLoader } from '@iconify/utils/lib/loader/node-loaders' import { FileSystemIconLoader } from '@iconify/utils/lib/loader/node-loaders'
import presetRemToPx from '@unocss/preset-rem-to-px' import presetRemToPx from '@unocss/preset-rem-to-px'
import { defineConfig, presetAttributify, presetIcons, presetWind3 } from 'unocss' import { defineConfig, presetAttributify, presetIcons, presetUno } from 'unocss'
import { getIcons } from './build/index.js' import { getIcons } from './build/index.js'
const icons = getIcons() const icons = getIcons()
export default defineConfig({ export default defineConfig({
presets: [ presets: [
presetWind3(), presetUno(),
presetAttributify(), presetAttributify(),
presetIcons({ presetIcons({
warn: true, warn: true,

View File

@@ -69,9 +69,6 @@ export default defineConfig(({ mode }) => {
}, },
}, },
}, },
optimizeDeps: {
include: ['vue3-intro-step'],
},
build: { build: {
chunkSizeWarningLimit: 1024, // chunk 大小警告的限制单位kb chunkSizeWarningLimit: 1024, // chunk 大小警告的限制单位kb
}, },