1
0
mirror of https://github.com/zclzone/vue-naive-admin.git synced 2026-01-23 08:00:22 +08:00
This commit is contained in:
zclzone
2023-12-07 21:55:23 +08:00
commit cfeb813b62
401 changed files with 11125 additions and 0 deletions

View File

@@ -0,0 +1,19 @@
/**********************************
* @Author: Ronnie Zhang
* @LastEditor: Ronnie Zhang
* @LastEditTime: 2023/12/05 21:28:47
* @Email: zclzone@outlook.com
* Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top
**********************************/
import axios from 'axios'
import { request } from '@/utils'
export default {
getMenuTree: () => request.get('/permission/menu/tree'),
getButtonAndApi: (parentId) => request.get(`/permission/button-and-api/${parentId}`),
getComponents: () => axios.get(`${import.meta.env.VITE_PUBLIC_PATH}components.json`),
addPermission: (data) => request.post('/permission', data),
savePermission: (id, data) => request.patch(`/permission/${id}`, data),
deletePermission: (id) => request.delete(`permission/${id}`),
}

View File

@@ -0,0 +1,116 @@
<!--------------------------------
- @Author: Ronnie Zhang
- @LastEditor: Ronnie Zhang
- @LastEditTime: 2023/12/05 21:28:59
- @Email: zclzone@outlook.com
- Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top
--------------------------------->
<template>
<div>
<n-space vertical :size="12">
<h3>菜单</h3>
<div class="flex">
<n-input v-model:value="pattern" placeholder="搜索" clearable />
<n-button class="ml-12" type="primary" @click="handleAdd()">
<i class="i-material-symbols:add mr-4 text-14" />
新增
</n-button>
</div>
<n-tree
:show-irrelevant-nodes="false"
:pattern="pattern"
:data="treeData"
:render-prefix="renderPrefix"
:render-suffix="renderSuffix"
:on-update:selected-keys="onSelect"
key-field="code"
label-field="name"
block-line
default-expand-all
/>
</n-space>
<ResAddOrEdit ref="modalRef" :menus="treeData" @refresh="(data) => emit('refresh', data)" />
</div>
</template>
<script setup>
import { withModifiers } from 'vue'
import ResAddOrEdit from './ResAddOrEdit.vue'
import { NButton } from 'naive-ui'
import api from '../api'
defineProps({
treeData: {
type: Array,
default: () => [],
},
currentMenu: {
type: Object,
default: () => null,
},
})
const emit = defineEmits(['refresh', 'update:currentMenu'])
const pattern = ref('')
const modalRef = ref(null)
async function handleAdd(data = {}) {
modalRef.value?.handleOpen({
action: 'add',
title: '新增菜单',
row: { type: 'MENU', ...data },
okText: '保存',
})
}
function onSelect(keys, option, { action, node }) {
emit('update:currentMenu', action === 'select' ? node : null)
}
function renderPrefix({ option }) {
return h('i', { class: `${option.icon}?mask text-16` })
}
function renderSuffix({ option }) {
return [
h(
NButton,
{
text: true,
type: 'primary',
title: '新增下级菜单',
size: 'tiny',
onClick: withModifiers(() => handleAdd({ parentId: option.id }), ['stop']),
},
{ default: () => '新增' }
),
h(
NButton,
{
text: true,
type: 'error',
size: 'tiny',
style: 'margin-left: 12px;',
onClick: withModifiers(() => handleDelete(option), ['stop']),
},
{ default: () => '删除' }
),
]
}
function handleDelete(item) {
$dialog.confirm({
content: `确认删除【${item.name}】?`,
async confirm() {
$message.loading('正在删除', { key: 'deleteMenu' })
await api.deletePermission(item.id)
$message.success('删除成功', { key: 'deleteMenu' })
emit('refresh')
},
})
}
</script>

View File

@@ -0,0 +1,32 @@
<!--------------------------------
- @Author: Ronnie Zhang
- @LastEditor: Ronnie Zhang
- @LastEditTime: 2023/12/05 21:29:05
- @Email: zclzone@outlook.com
- Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top
--------------------------------->
<template>
<span class="flex items-center">
<n-popover v-if="content" trigger="hover">
<template #trigger>
<i class="i-material-symbols:help mr-4" />
</template>
<span>{{ content }}</span>
</n-popover>
{{ label }}
</span>
</template>
<script setup>
defineProps({
label: {
type: String,
required: true,
},
content: {
type: String,
default: '',
},
})
</script>

View File

@@ -0,0 +1,201 @@
<!--------------------------------
- @Author: Ronnie Zhang
- @LastEditor: Ronnie Zhang
- @LastEditTime: 2023/12/05 21:29:12
- @Email: zclzone@outlook.com
- Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top
--------------------------------->
<template>
<MeModal ref="modalRef">
<n-form
ref="modalFormRef"
label-placement="left"
require-mark-placement="left"
:label-width="100"
:model="modalForm"
>
<n-grid :cols="24" :x-gap="24">
<n-form-item-gi :span="12" label="所属菜单" path="parentId">
<n-tree-select
v-model:value="modalForm.parentId"
:options="menuOptions"
label-field="name"
key-field="id"
placeholder="根菜单"
clearable
/>
</n-form-item-gi>
<n-form-item-gi :span="12" path="name" :rule="required">
<template #label>
<QuestionLabel label="名称" content="标题" />
</template>
<n-input v-model:value="modalForm.name" />
</n-form-item-gi>
<n-form-item-gi :span="12" path="code" :rule="required">
<template #label>
<QuestionLabel label="编码" content="如果是菜单则对应前端路由的name使用大驼峰" />
</template>
<n-input v-model:value="modalForm.code" />
</n-form-item-gi>
<n-form-item-gi :span="12" path="path">
<template #label>
<QuestionLabel label="路由地址" content="父级菜单可不填" />
</template>
<n-input v-model:value="modalForm.path" />
</n-form-item-gi>
<n-form-item-gi :span="12" path="icon">
<template #label>
<QuestionLabel
label="菜单图标"
content="如material-symbols:help图标库地址: https://icones.js.org/collection/all"
/>
</template>
<n-select v-model:value="modalForm.icon" :options="iconOptions" clearable filterable />
</n-form-item-gi>
<n-form-item-gi :span="12" path="layout">
<template #label>
<QuestionLabel
label="layout"
content="对应layouts文件夹下的目录名, 为空则默认为 default"
/>
</template>
<n-select v-model:value="modalForm.layout" :options="layoutOptions" clearable />
</n-form-item-gi>
<n-form-item-gi :span="24" path="component">
<template #label>
<QuestionLabel
label="组件路径"
content="前端组件的路径,以 /src 开头,父级菜单可不填"
/>
</template>
<n-select
v-model:value="modalForm.component"
:options="componentOptions"
clearable
filterable
tag
/>
</n-form-item-gi>
<n-form-item-gi :span="12" path="show">
<template #label>
<QuestionLabel label="显示状态" content="控制是否在菜单栏显示,不影响路由注册" />
</template>
<n-switch v-model:value="modalForm.show">
<template #checked>显示</template>
<template #unchecked>隐藏</template>
</n-switch>
</n-form-item-gi>
<n-form-item-gi :span="12" path="enable">
<template #label>
<QuestionLabel
label="状态"
content="如果是菜单,禁用后将不添加到路由表,无法进入此页面"
/>
</template>
<n-switch v-model:value="modalForm.enable">
<template #checked>启用</template>
<template #unchecked>禁用</template>
</n-switch>
</n-form-item-gi>
<n-form-item-gi :span="12" path="enable">
<template #label>
<QuestionLabel
label="KeepAlive"
content="设置keepAlive需将组件的name设置成当前菜单的code"
/>
</template>
<n-switch v-model:value="modalForm.keepAlive">
<template #checked></template>
<template #unchecked></template>
</n-switch>
</n-form-item-gi>
<n-form-item-gi
:span="12"
label="排序"
path="order"
:rule="{
type: 'number',
required: true,
message: '此为必填项',
trigger: ['blur', 'change'],
}"
>
<n-input-number v-model:value="modalForm.order" />
</n-form-item-gi>
</n-grid>
</n-form>
</MeModal>
</template>
<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 icons from 'isme:icons'
const props = defineProps({
menus: {
type: Array,
required: true,
},
})
const emit = defineEmits(['refresh'])
const menuOptions = computed(() => {
return [{ name: '根菜单', id: '', children: props.menus || [] }]
})
const componentOptions = pagePathes.map((path) => ({ label: path, value: path }))
const iconOptions = icons.map((item) => ({
label: () =>
h('span', { class: 'flex items-center' }, [h('i', { class: item + ' text-18 mr-8' }), item]),
value: item,
}))
const layoutOptions = [
{ label: '默认-default', value: 'default' },
{ label: '空白-empty', value: 'empty' },
]
const required = {
required: true,
message: '此为必填项',
trigger: ['blur', 'change'],
}
const defaultForm = { enable: true, show: true }
const [modalFormRef, modalForm, validation] = useForm(defaultForm)
const [modalRef, okLoading] = useModal()
const modalAction = ref('')
function handleOpen(options = {}) {
const { action, row = {}, ...rest } = options
modalAction.value = action
modalForm.value = { ...row }
modalRef.value.open({ ...rest, onOk: onSave })
}
async function onSave() {
if (!(await validation())) return
okLoading.value = true
try {
if (!modalForm.value.parentId) modalForm.value.parentId = null
if (modalAction.value === 'add') {
await api.addPermission(modalForm.value)
} else if (modalAction.value === 'edit') {
await api.savePermission(modalForm.value.id, modalForm.value)
}
okLoading.value = false
$message.success('保存成功')
emit('refresh', modalForm.value)
} catch (error) {
okLoading.value = false
console.error(error)
}
}
defineExpose({
handleOpen,
})
</script>

View File

@@ -0,0 +1,103 @@
<!--------------------------------
- @Author: Ronnie Zhang
- @LastEditor: Ronnie Zhang
- @LastEditTime: 2023/12/05 21:28:53
- @Email: zclzone@outlook.com
- Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top
--------------------------------->
<template>
<CommonPage>
<div class="flex">
<n-spin size="small" :show="treeLoading">
<MenuTree
v-model:currentMenu="currentMenu"
class="w-320 shrink-0"
:tree-data="treeData"
@refresh="initData"
/>
</n-spin>
<div class="ml-40 w-0 flex-1">
<template v-if="currentMenu">
<div class="flex justify-between">
<h3 class="mb-12">{{ currentMenu.name }}</h3>
<n-button
:disabled="!currentMenu"
size="small"
type="primary"
@click="handleEdit(currentMenu)"
>
<i class="i-material-symbols:edit-outline mr-4 text-14" />
编辑
</n-button>
</div>
<n-descriptions label-placement="left" bordered :column="2">
<n-descriptions-item label="编码">{{ currentMenu.code }}</n-descriptions-item>
<n-descriptions-item label="名称">{{ currentMenu.name }}</n-descriptions-item>
<n-descriptions-item label="路由地址">
{{ currentMenu.path ?? '--' }}
</n-descriptions-item>
<n-descriptions-item label="组件路径">
{{ currentMenu.component ?? '--' }}
</n-descriptions-item>
<n-descriptions-item label="菜单图标">
<span v-if="currentMenu.icon" class="flex items-center">
<i :class="`${currentMenu.icon}?mask text-22 mr-8`" />
<span class="opacity-50">{{ currentMenu.icon }}</span>
</span>
<span v-else></span>
</n-descriptions-item>
<n-descriptions-item label="layout">
{{ currentMenu.layout ?? 'default' }}
</n-descriptions-item>
<n-descriptions-item label="是否显示">
{{ currentMenu.show ? '是' : '否' }}
</n-descriptions-item>
<n-descriptions-item label="是否启用">
{{ currentMenu.enable ? '是' : '否' }}
</n-descriptions-item>
<n-descriptions-item label="KeepAlive">
{{ currentMenu.keepAlive ? '是' : '否' }}
</n-descriptions-item>
<n-descriptions-item label="排序">
{{ currentMenu.order ?? '--' }}
</n-descriptions-item>
</n-descriptions>
</template>
<n-empty v-else class="h-450 f-c-c" size="large" description="请选择菜单查看详情" />
</div>
</div>
<ResAddOrEdit ref="modalRef" :menus="treeData" @refresh="initData" />
</CommonPage>
</template>
<script setup>
import MenuTree from './components/MenuTree.vue'
import ResAddOrEdit from './components/ResAddOrEdit.vue'
import api from './api'
const treeData = ref([])
const treeLoading = ref(false)
async function initData(data) {
treeLoading.value = true
const res = await api.getMenuTree()
treeData.value = res?.data || []
treeLoading.value = false
if (data) currentMenu.value = data
}
initData()
const currentMenu = ref(null)
const modalRef = ref(null)
function handleEdit(item = {}) {
modalRef.value?.handleOpen({
action: 'edit',
title: '编辑菜单 - ' + item.name,
row: item,
okText: '保存',
})
}
</script>

21
src/views/pms/role/api.js Normal file
View File

@@ -0,0 +1,21 @@
/**********************************
* @Author: Ronnie Zhang
* @LastEditor: Ronnie Zhang
* @LastEditTime: 2023/12/05 21:29:27
* @Email: zclzone@outlook.com
* Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top
**********************************/
import { request } from '@/utils'
export default {
create: (data) => request.post('/role', data),
read: (params = {}) => request.get('/role/page', { params }),
update: (data) => request.patch(`/role/${data.id}`, data),
delete: (id) => request.delete(`/role/${id}`),
getAllPermissionTree: () => request.get('/permission/tree'),
getAllUsers: (params = {}) => request.get('/user', { params }),
addRoleUsers: (roleId, data) => request.patch(`/role/users/add/${roleId}`, data),
removeRoleUsers: (roleId, data) => request.patch(`/role/users/remove/${roleId}`, data),
}

View File

@@ -0,0 +1,85 @@
<!--------------------------------
- @Author: Ronnie Zhang
- @LastEditor: Ronnie Zhang
- @LastEditTime: 2023/12/05 21:29:32
- @Email: zclzone@outlook.com
- Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top
--------------------------------->
<template>
<n-tree
:key-field="keyField"
:label-field="labelField"
:selectable="false"
block-line
default-expand-all
checkable
check-on-click
cascade
:data="treeData"
:checked-keys="checkedKeys"
:on-update:checked-keys="(keys) => (checkedKeys = keys)"
:on-update:indeterminate-keys="(keys) => (halfCheckedKeys = keys)"
/>
</template>
<script setup>
const props = defineProps({
treeData: {
type: Array,
default: () => [],
},
value: {
type: Array,
default: () => [],
},
labelField: {
type: String,
default: 'label',
},
keyField: {
type: String,
default: 'value',
},
})
const emit = defineEmits(['update:value'])
const halfCheckedKeys = ref([])
const checkedKeys = ref([])
watch([halfCheckedKeys, checkedKeys], ([v1, v2]) => {
emit('update:value', new Array(...new Set([...v1, ...v2])))
})
onMounted(() => {
halfCheckedKeys.value = getHalfCheckedValues(props.value, props.treeData)
checkedKeys.value = props.value.filter((item) => !halfCheckedKeys.value.includes(item))
})
// 获取半选状态的值
function getHalfCheckedValues(selectedValues, treeData, halfCheckedValues = []) {
function isHalfChecked(node) {
// 如果存在子节点没有选中或者子节点是半选状态
return node.children.some(
(item) =>
!selectedValues.includes(item[props.keyField]) ||
halfCheckedValues.includes(item[props.keyField])
)
}
function hasGrandson(node) {
return node.children.some((item) => !!item.children?.length)
}
for (const item of treeData) {
if (!item.children?.length) continue
if (hasGrandson(item)) {
// 根据孙节点判断子节点是否是半选
getHalfCheckedValues(selectedValues, item.children, halfCheckedValues)
isHalfChecked(item) && halfCheckedValues.push(item[props.keyField])
} else {
isHalfChecked(item) && halfCheckedValues.push(item[props.keyField])
}
}
return halfCheckedValues
}
</script>

View File

@@ -0,0 +1,243 @@
<!--------------------------------
- @Author: Ronnie Zhang
- @LastEditor: Ronnie Zhang
- @LastEditTime: 2023/12/05 21:29:38
- @Email: zclzone@outlook.com
- Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top
--------------------------------->
<template>
<CommonPage>
<template #action>
<n-button type="primary" @click="handleAdd()">
<i class="i-material-symbols:add mr-4 text-18" />
新增角色
</n-button>
</template>
<MeCrud
ref="$table"
v-model:query-items="queryItems"
:scroll-x="1200"
:columns="columns"
:get-data="api.read"
>
<MeQueryItem label="角色名" :label-width="50">
<n-input
v-model:value="queryItems.name"
type="text"
placeholder="请输入角色名"
clearable
@keydown.enter="$table?.handleSearch"
/>
</MeQueryItem>
<MeQueryItem label="状态" :label-width="50">
<n-select
v-model:value="queryItems.enable"
clearable
:options="[
{ label: '启用', value: 1 },
{ label: '停用', value: 0 },
]"
/>
</MeQueryItem>
</MeCrud>
<MeModal ref="modalRef" width="520px">
<n-form
ref="modalFormRef"
label-placement="left"
label-align="left"
:label-width="80"
:model="modalForm"
>
<n-form-item
label="角色名"
path="name"
:rule="{
required: true,
message: '请输入角色名',
trigger: ['input', 'blur'],
}"
>
<n-input v-model:value="modalForm.name" />
</n-form-item>
<n-form-item
label="角色编码"
path="code"
:rule="{
required: true,
message: '请输入角色编码',
trigger: ['input', 'blur'],
}"
>
<n-input v-model:value="modalForm.code" :disabled="modalAction !== 'add'" />
</n-form-item>
<n-form-item label="权限" path="permissionIds">
<CascadeTree
v-model:value="modalForm.permissionIds"
:tree-data="permissionTree"
label-field="name"
key-field="id"
class="cus-scroll max-h-200 w-full"
/>
</n-form-item>
<n-form-item label="状态" path="enable">
<n-switch v-model:value="modalForm.enable">
<template #checked>启用</template>
<template #unchecked>停用</template>
</n-switch>
</n-form-item>
</n-form>
</MeModal>
</CommonPage>
</template>
<script setup>
import { NButton, NSwitch } from 'naive-ui'
import { MeCrud, MeQueryItem, MeModal } from '@/components'
import { useCrud } from '@/composables'
import api from './api'
import CascadeTree from './components/CascadeTree.vue'
defineOptions({ name: 'RoleMgt' })
const router = useRouter()
const $table = ref(null)
/** QueryBar筛选参数可选 */
const queryItems = ref({})
onMounted(() => {
$table.value?.handleSearch()
})
const columns = [
{ title: '角色名', key: 'name' },
{ title: '角色编码', key: 'code' },
{
title: '状态',
key: 'enable',
render: (row) =>
h(
NSwitch,
{
size: 'small',
rubberBand: false,
value: row.enable,
loading: !!row.enableLoading,
disabled: row.code === 'SUPER_ADMIN',
onUpdateValue: () => handleEnable(row),
},
{
checked: () => '启用',
unchecked: () => '停用',
}
),
},
{
title: '操作',
key: 'actions',
width: 320,
align: 'right',
fixed: 'right',
render(row) {
return [
h(
NButton,
{
size: 'small',
type: 'primary',
secondary: true,
onClick: () =>
router.push({ path: `/pms/role/user/${row.id}`, query: { roleName: row.name } }),
},
{
default: () => '分配用户',
icon: () => h('i', { class: 'i-fe:user-plus text-14' }),
}
),
h(
NButton,
{
size: 'small',
type: 'primary',
style: 'margin-left: 12px;',
disabled: row.code === 'SUPER_ADMIN',
onClick: () => handleEdit(row),
},
{
default: () => '编辑',
icon: () => h('i', { class: 'i-material-symbols:edit-outline text-14' }),
}
),
h(
NButton,
{
size: 'small',
type: 'error',
style: 'margin-left: 12px;',
disabled: row.code === 'SUPER_ADMIN',
onClick: () => handleDelete(row.id),
},
{
default: () => '删除',
icon: () => h('i', { class: 'i-material-symbols:delete-outline text-14' }),
}
),
]
},
},
]
async function handleEnable(row) {
row.enableLoading = true
try {
await api.update({ id: row.id, enable: !row.enable })
row.enableLoading = false
$message.success('操作成功')
$table.value?.handleSearch()
} catch (error) {
row.enableLoading = false
}
}
const {
modalRef,
modalFormRef,
modalAction,
modalForm,
okLoading,
handleAdd,
handleDelete,
handleOpen,
} = useCrud({
name: '角色',
doCreate: api.create,
doDelete: api.delete,
doUpdate: api.update,
initForm: { enable: true },
refresh: () => $table.value?.handleSearch(),
})
function handleEdit(row) {
handleOpen({ action: 'edit', title: '修改角色', row, onSave })
}
async function onSave() {
await modalFormRef.value?.validate()
okLoading.value = true
try {
await api.update(modalForm.value)
okLoading.value = false
$message.success('保存成功')
$table.value?.handleSearch()
} catch (error) {
okLoading.value = false
return false
}
}
const permissionTree = ref([])
api.getAllPermissionTree().then(({ data = [] }) => (permissionTree.value = data))
</script>

View File

@@ -0,0 +1,223 @@
<!--------------------------------
- @Author: Ronnie Zhang
- @LastEditor: Ronnie Zhang
- @LastEditTime: 2023/12/05 21:29:43
- @Email: zclzone@outlook.com
- Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top
--------------------------------->
<template>
<CommonPage back>
<template #title-suffix>
<n-tag class="ml-12" type="warning">{{ route.query.roleName }}</n-tag>
</template>
<template #action>
<div class="flex items-center">
<n-button :disabled="!userIds.length" type="error" @click="handleBatchRemove()">
<i v-if="userIds.length" class="i-material-symbols:delete-outline mr-4 text-18" />
批量取消授权
</n-button>
<n-button
class="ml-12"
:disabled="!userIds.length"
type="primary"
@click="handleBatchAdd()"
>
<i v-if="userIds.length" class="i-line-md:confirm-circle mr-4 text-18" />
批量授权
</n-button>
</div>
</template>
<MeCrud
ref="$table"
v-model:query-items="queryItems"
:scroll-x="1200"
:columns="columns"
:get-data="api.getAllUsers"
@on-checked="onChecked"
>
<MeQueryItem label="用户名" :label-width="50">
<n-input
v-model:value="queryItems.username"
type="text"
placeholder="请输入用户名"
clearable
@keydown.enter="$table?.handleSearch"
/>
</MeQueryItem>
<MeQueryItem label="性别" :label-width="50">
<n-select v-model:value="queryItems.gender" clearable :options="genders" />
</MeQueryItem>
<MeQueryItem label="状态" :label-width="50">
<n-select
v-model:value="queryItems.enable"
clearable
:options="[
{ label: '启用', value: 1 },
{ label: '停用', value: 0 },
]"
/>
</MeQueryItem>
</MeCrud>
</CommonPage>
</template>
<script setup>
import { NAvatar, NButton, NSwitch, NTag } from 'naive-ui'
import { MeCrud, MeQueryItem } from '@/components'
import { formatDateTime } from '@/utils'
import api from './api'
import { h } from 'vue'
defineOptions({ name: 'RoleUser' })
const route = useRoute()
const $table = ref(null)
/** QueryBar筛选参数可选 */
const queryItems = ref({})
onMounted(() => {
$table.value?.handleSearch()
})
const genders = [
{ label: '男', value: 1 },
{ label: '女', value: 2 },
]
const columns = [
{ type: 'selection', fixed: 'left' },
{
title: '头像',
key: 'avatar',
width: 80,
render: ({ avatar }) =>
h(NAvatar, {
size: 'medium',
src: avatar,
}),
},
{ title: '用户名', key: 'username', width: 150, ellipsis: { tooltip: true } },
{
title: '角色',
key: 'roles',
width: 200,
ellipsis: { tooltip: true },
render: ({ roles }) => {
if (roles?.length) {
return roles.map((item, index) =>
h(
NTag,
{ type: 'success', style: index > 0 ? 'margin-left: 8px;' : '' },
{ default: () => item.name }
)
)
}
return '暂无角色'
},
},
{
title: '性别',
key: 'gender',
width: 80,
render: ({ gender }) => genders.find((item) => gender === item.value)?.label ?? '',
},
{
title: '创建时间',
key: 'createDate',
width: 180,
render(row) {
return h('span', formatDateTime(row['createTime']))
},
},
{
title: '状态',
key: 'enable',
width: 100,
render: (row) =>
h(
NSwitch,
{
size: 'small',
rubberBand: false,
value: row.enable,
},
{
checked: () => '启用',
unchecked: () => '停用',
}
),
},
{
title: '操作',
key: 'actions',
width: 100,
align: 'right',
fixed: 'right',
hideInExcel: true,
render(row) {
return row.roles?.some((item) => item.id === +route.params.roleId)
? h(
NButton,
{
size: 'small',
type: 'error',
secondary: true,
onClick: () => handleBatchRemove([row.id]),
},
{
default: () => '取消授权',
icon: () => h('i', { class: 'i-material-symbols:delete-outline text-14' }),
}
)
: h(
NButton,
{
size: 'small',
type: 'primary',
secondary: true,
onClick: () => handleBatchAdd([row.id]),
},
{
default: () => '授权',
icon: () => h('i', { class: 'i-line-md:confirm-circle text-14' }),
}
)
},
},
]
const userIds = ref([])
function onChecked(rowKeys) {
userIds.value = rowKeys || []
}
function handleBatchAdd(ids = userIds.value) {
const roleId = route.params.roleId
if (!roleId) return $message.error('角色异常,请重新选择角色')
if (!ids.length) return $message.error('请先选择用户')
$dialog.confirm({
content: `确认分配【${route.query.roleName}】?`,
async confirm() {
await api.addRoleUsers(roleId, { userIds: ids })
$table.value?.handleSearch()
},
})
}
function handleBatchRemove(ids = userIds.value) {
const roleId = route.params.roleId
if (!roleId) return $message.error('角色异常,请重新选择角色')
if (!ids.length) return $message.error('请先选择用户')
$dialog.confirm({
content: `确认取消分配【${route.query.roleName}】?`,
async confirm() {
await api.removeRoleUsers(roleId, { userIds: ids })
$table.value?.handleSearch()
},
})
}
</script>

19
src/views/pms/user/api.js Normal file
View File

@@ -0,0 +1,19 @@
/**********************************
* @Author: Ronnie Zhang
* @LastEditor: Ronnie Zhang
* @LastEditTime: 2023/12/05 21:29:51
* @Email: zclzone@outlook.com
* Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top
**********************************/
import { request } from '@/utils'
export default {
create: (data) => request.post('/user', data),
read: (params = {}) => request.get('/user', { params }),
update: (data) => request.patch(`/user/${data.id}`, data),
delete: (id) => request.delete(`/user/${id}`),
resetPwd: (id, data) => request.patch(`/user/password/reset/${id}`, data),
getAllRoles: () => request.get('/role?enable=1'),
}

View File

@@ -0,0 +1,306 @@
<!--------------------------------
- @Author: Ronnie Zhang
- @LastEditor: Ronnie Zhang
- @LastEditTime: 2023/12/05 21:29:56
- @Email: zclzone@outlook.com
- Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top
--------------------------------->
<template>
<CommonPage>
<template #action>
<n-button v-permission="'AddUser'" type="primary" @click="handleAdd()">
<i class="i-material-symbols:add mr-4 text-18" />
创建新用户
</n-button>
</template>
<MeCrud
ref="$table"
v-model:query-items="queryItems"
:scroll-x="1200"
:columns="columns"
:get-data="api.read"
>
<MeQueryItem label="用户名" :label-width="50">
<n-input
v-model:value="queryItems.username"
type="text"
placeholder="请输入用户名"
clearable
@keydown.enter="$table?.handleSearch"
/>
</MeQueryItem>
<MeQueryItem label="性别" :label-width="50">
<n-select v-model:value="queryItems.gender" clearable :options="genders" />
</MeQueryItem>
<MeQueryItem label="状态" :label-width="50">
<n-select
v-model:value="queryItems.enable"
clearable
:options="[
{ label: '启用', value: 1 },
{ label: '停用', value: 0 },
]"
/>
</MeQueryItem>
</MeCrud>
<MeModal ref="modalRef" width="520px">
<n-form
ref="modalFormRef"
label-placement="left"
label-align="left"
:label-width="80"
:model="modalForm"
:disabled="modalAction === 'view'"
>
<n-form-item
label="用户名"
path="username"
:rule="{
required: true,
message: '请输入用户名',
trigger: ['input', 'blur'],
}"
>
<n-input v-model:value="modalForm.username" :disabled="modalAction !== 'add'" />
</n-form-item>
<n-form-item
v-if="['add', 'reset'].includes(modalAction)"
:label="modalAction === 'reset' ? '重置密码' : '初始密码'"
path="password"
:rule="{
required: true,
message: '请输入密码',
trigger: ['input', 'blur'],
}"
>
<n-input v-model:value="modalForm.password" />
</n-form-item>
<n-form-item v-if="['add', 'setRole'].includes(modalAction)" label="角色" path="roleIds">
<n-select
v-model:value="modalForm.roleIds"
:options="roles"
label-field="name"
value-field="id"
clearable
filterable
multiple
/>
</n-form-item>
<n-form-item v-if="modalAction === 'add'" label="状态" path="enable">
<n-switch v-model:value="modalForm.enable">
<template #checked>启用</template>
<template #unchecked>停用</template>
</n-switch>
</n-form-item>
</n-form>
<n-alert v-if="modalAction === 'add'" type="warning" closable>
详细信息需由用户本人补充修改
</n-alert>
</MeModal>
</CommonPage>
</template>
<script setup>
import { NAvatar, NButton, NSwitch, NTag } from 'naive-ui'
import { formatDateTime } from '@/utils'
import { MeCrud, MeQueryItem, MeModal } from '@/components'
import { useCrud } from '@/composables'
import api from './api'
defineOptions({ name: 'UserMgt' })
const $table = ref(null)
/** QueryBar筛选参数可选 */
const queryItems = ref({})
onMounted(() => {
$table.value?.handleSearch()
})
const genders = [
{ label: '男', value: 1 },
{ label: '女', value: 2 },
]
const roles = ref([])
api.getAllRoles().then(({ data = [] }) => (roles.value = data))
const columns = [
{
title: '头像',
key: 'avatar',
width: 80,
render: ({ avatar }) =>
h(NAvatar, {
size: 'medium',
src: avatar,
}),
},
{ title: '用户名', key: 'username', width: 150, ellipsis: { tooltip: true } },
{
title: '角色',
key: 'roles',
width: 200,
ellipsis: { tooltip: true },
render: ({ roles }) => {
if (roles?.length) {
return roles.map((item, index) =>
h(
NTag,
{ type: 'success', style: index > 0 ? 'margin-left: 8px;' : '' },
{ default: () => item.name }
)
)
}
return '暂无角色'
},
},
{
title: '性别',
key: 'gender',
width: 80,
render: ({ gender }) => genders.find((item) => gender === item.value)?.label ?? '',
},
{ title: '邮箱', key: 'email', width: 150, ellipsis: { tooltip: true } },
{
title: '创建时间',
key: 'createDate',
width: 180,
render(row) {
return h('span', formatDateTime(row['createTime']))
},
},
{
title: '状态',
key: 'enable',
width: 120,
render: (row) =>
h(
NSwitch,
{
size: 'small',
rubberBand: false,
value: row.enable,
loading: !!row.enableLoading,
onUpdateValue: () => handleEnable(row),
},
{
checked: () => '启用',
unchecked: () => '停用',
}
),
},
{
title: '操作',
key: 'actions',
width: 320,
align: 'right',
fixed: 'right',
hideInExcel: true,
render(row) {
return [
h(
NButton,
{
size: 'small',
type: 'primary',
secondary: true,
onClick: () => handleOpenRolesSet(row),
},
{
default: () => '分配角色',
icon: () => h('i', { class: 'i-carbon:user-role text-14' }),
}
),
h(
NButton,
{
size: 'small',
type: 'primary',
style: 'margin-left: 12px;',
onClick: () => handleOpen({ action: 'reset', title: '重置密码', row, onSave }),
},
{
default: () => '重置密码',
icon: () => h('i', { class: 'i-radix-icons:reset text-14' }),
}
),
h(
NButton,
{
size: 'small',
type: 'error',
style: 'margin-left: 12px;',
onClick: () => handleDelete(row.id),
},
{
default: () => '删除',
icon: () => h('i', { class: 'i-material-symbols:delete-outline text-14' }),
}
),
]
},
},
]
async function handleEnable(row) {
row.enableLoading = true
try {
await api.update({ id: row.id, enable: !row.enable })
row.enableLoading = false
$message.success('操作成功')
$table.value?.handleSearch()
} catch (error) {
row.enableLoading = false
}
}
function handleOpenRolesSet(row) {
const roleIds = row.roles.map((item) => item.id)
handleOpen({
action: 'setRole',
title: '分配角色',
row: { id: row.id, username: row.username, roleIds },
onSave,
})
}
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() {
if (modalAction.value === 'setRole') {
return handleSave({
api: () => api.update(modalForm.value),
cb: () => $message.success('分配成功'),
})
} else if (modalAction.value === 'reset') {
return handleSave({
api: () => api.resetPwd(modalForm.value.id, modalForm.value),
cb: () => $message.success('密码重置成功'),
})
}
handleSave()
}
</script>