权限模块功能基本改写完成

This commit is contained in:
2026-01-22 22:28:40 +08:00
parent 72a6a6a709
commit 0c2ebc8501
13 changed files with 2124 additions and 1313 deletions

View File

@@ -338,4 +338,11 @@ export default {
}, },
}, },
}, },
upload: {
url: `system/file/upload`,
name: '文件上传',
post: async function (params = {}) {
return await request.post(this.url, params)
},
},
} }

View File

@@ -74,9 +74,91 @@ body {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
background-color: #ffffff; background-color: #ffffff;
.search-box {
padding: 10px; .tool-bar {
background-color: #f5f5f5; padding: 12px 16px;
border-radius: 10px; background-color: #fff;
border-bottom: 1px solid #f0f0f0;
display: flex;
justify-content: space-between;
align-items: center;
gap: 16px;
.left-panel {
flex: 1;
display: flex;
align-items: center;
gap: 12px;
overflow-x: auto;
:deep(.ant-form) {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 8px;
flex: 1;
}
:deep(.ant-form-item) {
margin-bottom: 0;
}
:deep(.ant-form-item-label) {
min-width: 70px;
}
}
.right-panel {
display: flex;
align-items: center;
gap: 8px;
flex-shrink: 0;
}
// 按钮组样式
.button-group {
display: flex;
gap: 8px;
}
// 搜索输入框样式
:deep(.ant-input),
:deep(.ant-select-selector) {
border-radius: 4px;
}
// 按钮样式优化
:deep(.ant-btn) {
border-radius: 4px;
transition: all 0.3s ease;
&:hover {
transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
}
&:active {
transform: translateY(0);
}
}
// 主按钮特殊样式
:deep(.ant-btn-primary) {
background: linear-gradient(135deg, #1890ff 0%, #096dd9 100%);
border: none;
&:hover {
background: linear-gradient(135deg, #40a9ff 0%, #1890ff 100%);
}
}
// 危险按钮样式
:deep(.ant-btn-dangerous) {
&:hover {
background: #ff4d4f;
border-color: #ff4d4f;
color: #fff;
}
}
} }
} }

View File

@@ -1,9 +1,9 @@
import { upload } from "@/api/system"; import systemApi from "@/api/system";
//上传配置 //上传配置
export default { export default {
apiObj: upload, //上传请求API对象 apiObj: systemApi.upload.post, //上传请求API对象
filename: "file", //form请求时文件的key filename: "file", //form请求时文件的key
successCode: 1, //请求完成代码 successCode: 1, //请求完成代码
maxSize: 10, //最大文件大小 默认10MB maxSize: 10, //最大文件大小 默认10MB
@@ -15,6 +15,6 @@ export default {
msg: res.message //分析描述字段结构 msg: res.message //分析描述字段结构
} }
}, },
apiObjFile: upload, //附件上传请求API对象 apiObjFile: systemApi.upload.post, //附件上传请求API对象
maxSizeFile: 10 //最大文件大小 默认10MB maxSizeFile: 10 //最大文件大小 默认10MB
} }

View File

@@ -1,155 +1,283 @@
<template> <template>
<el-container> <div class="pages department-page">
<el-header> <div class="tool-bar">
<div class="left-panel"> <div class="left-panel">
<el-button type="primary" icon="el-icon-plus" @click="add"></el-button> <a-form layout="inline" :model="searchForm">
<el-button type="danger" plain icon="el-icon-delete" :disabled="selection.length==0" @click="batch_del"></el-button> <a-form-item>
<a-input v-model:value="searchForm.keyword" placeholder="请输入部门名称" allow-clear style="width: 200px" />
</a-form-item>
<a-form-item>
<a-space>
<a-button type="primary" @click="handleSearch">
<template #icon><search-outlined /></template>
</a-button>
<a-button @click="handleReset">
<template #icon><redo-outlined /></template>
</a-button>
</a-space>
</a-form-item>
</a-form>
</div> </div>
<div class="right-panel"> <div class="right-panel">
<div class="right-panel-search"> <a-button type="primary" @click="handleAdd">
<el-input v-model="search.keyword" placeholder="部门名称" clearable></el-input> <template #icon><plus-outlined /></template>
<el-button type="primary" icon="el-icon-search" @click="upsearch"></el-button> </a-button>
<a-button danger :disabled="selection.length === 0" @click="handleBatchDelete">
<template #icon><delete-outlined /></template>
</a-button>
</div> </div>
</div> </div>
</el-header> <div class="table-content">
<el-main class="nopadding"> <scTable ref="tableRef" :columns="columns" :data-source="tableData" :loading="loading" :pagination="false"
<scTable ref="table" :apiObj="apiObj" row-key="id" :params="search" @selection-change="selectionChange" hidePagination> :row-key="rowKey" :row-selection="rowSelection" @refresh="loadData" @select="handleSelectChange"
<el-table-column type="selection" width="50"></el-table-column> @selectAll="handleSelectAll">
<el-table-column label="#" type="index" width="50"></el-table-column> <template #action="{ record }">
<el-table-column label="部门名称" prop="title"></el-table-column> <a-space>
<el-table-column label="部门标识" prop="name" width="150"></el-table-column> <a-button type="link" size="small" @click="handleView(record)">查看</a-button>
<el-table-column label="排序" prop="sort" width="150"></el-table-column> <a-button type="link" size="small" @click="handleEdit(record)">编辑</a-button>
<el-table-column label="操作" fixed="right" align="right" width="220"> <a-popconfirm title="确定删除该部门吗?" @confirm="handleDelete(record)">
<template #default="scope"> <a-button type="link" size="small" danger>删除</a-button>
<el-button-group> </a-popconfirm>
<el-button type="success" @click="table_show(scope.row, scope.$index)">查看</el-button> </a-space>
<el-divider direction="vertical"></el-divider>
<el-button type="primary" @click="table_edit(scope.row, scope.$index)">编辑</el-button>
<el-divider direction="vertical"></el-divider>
<el-popconfirm title="确定删除吗?" @confirm="table_del(scope.row, scope.$index)">
<template #reference>
<el-button type="danger">删除</el-button>
</template> </template>
</el-popconfirm>
</el-button-group>
</template>
</el-table-column>
</scTable> </scTable>
</el-main> </div>
</el-container> </div>
<save-dialog v-if="dialog.save" ref="saveDialog" @success="handleSaveSuccess" @closed="dialog.save=false"></save-dialog>
<!-- 新增/编辑部门弹窗 -->
<save-dialog v-if="dialog.save" ref="saveDialogRef" @success="handleSaveSuccess" @closed="dialog.save = false" />
</template> </template>
<script> <script setup>
import saveDialog from './save' import { ref, reactive, onMounted, computed } from 'vue'
import { message, Modal } from 'ant-design-vue'
import scTable from '@/components/scTable/index.vue'
import saveDialog from './save.vue'
import authApi from '@/api/auth'
export default { defineOptions({
name: 'auth.department', name: 'authDepartment'
components: { })
saveDialog
}, // 表格引用
data() { const tableRef = ref(null)
return {
dialog: { // 对话框状态
save: false, const dialog = reactive({
permission: false save: false
}, })
apiObj: this.$API.auth.department.list,
selection: [], // 弹窗引用
search: { const saveDialogRef = ref(null)
// 选中的行
const selection = ref([])
// 搜索表单
const searchForm = reactive({
keyword: ''
})
// 表格数据
const tableData = ref([])
const loading = ref(false)
// 行key
const rowKey = 'id'
// 行选择配置
const rowSelection = computed(() => ({
selectedRowKeys: selection.value.map(item => item.id),
onChange: (selectedRowKeys, selectedRows) => {
selection.value = selectedRows
}
}))
// 表格列配置
const columns = [
{ title: '#', dataIndex: '_index', key: '_index', width: 60, align: 'center' },
{ title: '部门名称', dataIndex: 'title', key: 'title', width: 300 },
{ title: '部门标识', dataIndex: 'name', key: 'name', width: 200 },
{ title: '排序', dataIndex: 'sort', key: 'sort', width: 100, align: 'center' },
{ title: '操作', dataIndex: 'action', key: 'action', width: 220, align: 'center', slot: 'action', fixed: 'right' }
]
// 加载部门列表数据
const loadData = async () => {
loading.value = true
try {
const params = {
is_tree: 1, is_tree: 1,
keyword: null ...searchForm
} }
} const res = await authApi.department.list.get(params)
}, if (res.code === 1) {
methods: { tableData.value = res.data || []
//添加
add(){
this.dialog.save = true
this.$nextTick(() => {
this.$refs.saveDialog.open()
})
},
//编辑
table_edit(row){
this.dialog.save = true
this.$nextTick(() => {
this.$refs.saveDialog.open('edit').setData(row)
})
},
//查看
table_show(row){
this.dialog.save = true
this.$nextTick(() => {
this.$refs.saveDialog.open('show').setData(row)
})
},
//删除
async table_del(row){
var reqData = {id: row.id}
var res = await this.$API.auth.department.delete.post(reqData);
if(res.code == 1){
this.$refs.table.refresh()
this.$message.success("删除成功")
} else { } else {
this.$alert(res.message, "提示", {type: 'error'}) message.error(res.message || '加载数据失败')
}
} catch (error) {
console.error('加载部门列表失败:', error)
message.error('加载数据失败')
} finally {
loading.value = false
} }
},
//批量删除
async batch_del(){
this.$confirm(`确定删除选中的 ${this.selection.length} 项吗?如果删除项中含有子集将会被一并删除`, '提示', {
type: 'warning'
}).then(async () => {
const loading = this.$loading();
let ids = this.selection.map(item => item.id)
var reqData = {ids: ids}
var res = await this.$API.auth.department.delete.post(reqData);
if(res.code == 1){
this.$refs.table.refresh()
this.$message.success("删除成功")
loading.close();
}else{
this.$message.error(res.message)
} }
}).catch(() => {
// 选择变化处理
const handleSelectChange = (record, selected, selectedRows) => {
if (selected) {
selection.value.push(record)
} else {
const index = selection.value.findIndex(item => item.id === record.id)
if (index > -1) {
selection.value.splice(index, 1)
}
}
}
// 全选/取消全选处理
const handleSelectAll = (selected, selectedRows, changeRows) => {
if (selected) {
changeRows.forEach(record => {
if (!selection.value.find(item => item.id === record.id)) {
selection.value.push(record)
}
}) })
}, } else {
//表格选择后回调事件 changeRows.forEach(record => {
selectionChange(selection){ const index = selection.value.findIndex(item => item.id === record.id)
this.selection = selection; if (index > -1) {
}, selection.value.splice(index, 1)
}
})
}
}
// 搜索 // 搜索
upsearch(){ const handleSearch = () => {
this.$refs.table.upData(this.search) loadData()
}, }
//根据ID获取树结构
filterTree(id){ // 重置
var target = null; const handleReset = () => {
searchForm.keyword = ''
selection.value = []
loadData()
}
// 新增部门
const handleAdd = () => {
dialog.save = true
setTimeout(() => {
saveDialogRef.value?.open('add')
}, 0)
}
// 查看部门
const handleView = (record) => {
dialog.save = true
setTimeout(() => {
saveDialogRef.value?.open('show').setData(record)
}, 0)
}
// 编辑部门
const handleEdit = (record) => {
dialog.save = true
setTimeout(() => {
saveDialogRef.value?.open('edit').setData(record)
}, 0)
}
// 删除部门
const handleDelete = async (record) => {
try {
const res = await authApi.department.delete.post({ id: record.id })
if (res.code === 1) {
message.success('删除成功')
loadData()
} else {
message.error(res.message || '删除失败')
}
} catch (error) {
console.error('删除部门失败:', error)
message.error('删除失败')
}
}
// 批量删除
const handleBatchDelete = () => {
if (selection.value.length === 0) {
message.warning('请选择要删除的部门')
return
}
Modal.confirm({
title: '确认删除',
content: `确定删除选中的 ${selection.value.length} 个部门吗?如果删除项中含有子集将会被一并删除`,
okText: '确定',
cancelText: '取消',
okType: 'danger',
onOk: async () => {
try {
const ids = selection.value.map(item => item.id)
const res = await authApi.department.delete.post({ ids })
if (res.code === 1) {
message.success('删除成功')
selection.value = []
loadData()
} else {
message.error(res.message || '删除失败')
}
} catch (error) {
console.error('批量删除部门失败:', error)
message.error('删除失败')
}
}
})
}
// 根据ID获取树节点
const filterTree = (id) => {
let target = null
function filter(tree) { function filter(tree) {
tree.forEach(item => { for (const item of tree) {
if(item.id == id){ if (item.id === id) {
target = item target = item
return
} }
if (item.children) { if (item.children) {
filter(item.children) filter(item.children)
} }
})
} }
filter(this.$refs.table.tableData) }
filter(tableData.value)
return target return target
},
//本地更新数据
handleSaveSuccess(data, mode){
if(mode=='add'){
this.$refs.table.refresh()
}else if(mode=='edit'){
this.$refs.table.refresh()
}
}
} }
// 保存成功回调
const handleSaveSuccess = (data, mode) => {
loadData()
} }
// 初始化
onMounted(() => {
loadData()
})
</script> </script>
<style scoped lang="scss">
.department-page {
display: flex;
flex-direction: column;
height: 100%;
padding: 0;
.table-content {
flex: 1;
overflow: hidden;
background: #f5f5f5;
}
}
</style>

View File

@@ -1,103 +1,156 @@
<template> <template>
<el-dialog :title="titleMap[mode]" v-model="visible" :width="500" destroy-on-close :close-on-click-modal="false" @closed="$emit('closed')"> <a-modal :title="titleMap[mode]" :open="visible" :width="500" :destroy-on-close="true" :footer="null"
<el-form :model="form" :rules="rules" :disabled="mode=='show'" ref="dialogForm" label-width="100px" label-position="left"> @cancel="handleCancel">
<el-form-item label="上级部门" prop="parent_id"> <a-form :model="form" :rules="rules" :disabled="mode === 'show'" ref="dialogForm" :label-col="{ span: 5 }"
<el-cascader v-model="form.parent_id" :options="groups" :props="groupsProps" :show-all-levels="false" clearable style="width: 100%;"></el-cascader> :wrapper-col="{ span: 18 }">
</el-form-item> <a-form-item label="上级部门" name="parent_id">
<el-form-item label="部门名称" prop="title"> <a-tree-select v-model:value="form.parent_id" :tree-data="departments"
<el-input v-model="form.title" clearable></el-input> :field-names="departmentFieldNames" :tree-default-expand-all="false" placeholder="请选择上级部门"
</el-form-item> allow-clear tree-node-filter-prop="title"
<el-form-item label="部门别名" prop="name"> :dropdown-style="{ maxHeight: '400px', overflow: 'auto' }" />
<el-input v-model="form.name" clearable></el-input> </a-form-item>
</el-form-item> <a-form-item label="部门名称" name="title">
<el-form-item label="排序" prop="sort"> <a-input v-model:value="form.title" placeholder="请输入部门名称" allow-clear></a-input>
<el-input-number v-model="form.sort" controls-position="right" :min="1" style="width: 100%;"></el-input-number> </a-form-item>
</el-form-item> <a-form-item label="部门别名" name="name">
</el-form> <a-input v-model:value="form.name" placeholder="请输入部门别名" allow-clear></a-input>
</a-form-item>
<a-form-item label="排序" name="sort">
<a-input-number v-model:value="form.sort" :min="1" :step="1" style="width: 100%" placeholder="请输入排序" />
</a-form-item>
</a-form>
<template #footer> <template #footer>
<el-button @click="visible=false" > </el-button> <a-button @click="handleCancel"> </a-button>
<el-button v-if="mode!='show'" type="primary" :loading="isSaveing" @click="submit()"> </el-button> <a-button v-if="mode !== 'show'" type="primary" :loading="isSaveing" @click="submit"> </a-button>
</template> </template>
</el-dialog> </a-modal>
</template> </template>
<script> <script setup>
export default { import { ref, reactive } from 'vue'
emits: ['success', 'closed'], import { message } from 'ant-design-vue'
data() { import authApi from '@/api/auth'
return {
mode: "add", const emit = defineEmits(['success', 'closed'])
titleMap: {
add: '新增', const mode = ref('add')
edit: '编辑', const titleMap = {
show: '查看' add: '新增部门',
}, edit: '编辑部门',
visible: false, show: '查看部门'
isSaveing: false, }
const visible = ref(false)
const isSaving = ref(false)
// 表单数据 // 表单数据
form: {id:"",title: "",name: "",sort: 1,parent_id: ""}, const form = reactive({
//验证规则 id: '',
rules: { title: '',
sort: [{required: true, message: '请输入排序', trigger: 'change'}], name: '',
title: [{required: true, message: '请输入角色名称'}], sort: 1,
name: [{required: true, message: '请输入角色别名'}] parent_id: null
},
//所需数据选项
groups: [],
groupsProps: {
value: "id",
label: "title",
emitPath: false,
checkStrictly: true
}
}
},
mounted() {
this.getGroup()
},
methods: {
//显示
open(mode='add'){
this.mode = mode;
this.visible = true;
return this
},
//加载树数据
async getGroup(){
var res = await this.$API.auth.department.list.get({is_tree: 1});
this.groups = res.data;
},
//表单提交方法
submit(){
this.$refs.dialogForm.validate(async (valid) => {
if (valid) {
this.isSaveing = true;
this.form.parent_id = this.form.parent_id ? this.form.parent_id : 0;
var res = {}
if(this.mode == 'add'){
res = await this.$API.auth.department.add.post(this.form);
}else{
res = await this.$API.auth.department.edit.post(this.form);
}
this.isSaveing = false;
if(res.code == 1){
this.$emit('success', this.form, this.mode)
this.visible = false;
this.$message.success("操作成功")
}else{
this.$message.error(res.message)
}
}
}) })
},
// 表单引用
const dialogForm = ref()
// 验证规则
const rules = {
title: [{ required: true, message: '请输入部门名称', trigger: 'blur' }],
name: [{ required: true, message: '请输入部门别名', trigger: 'blur' }],
sort: [
{ required: true, message: '请输入排序', trigger: 'change' },
{ type: 'number', message: '排序必须为数字', trigger: 'change' }
]
}
// 部门数据
const departments = ref([])
const departmentFieldNames = {
title: 'title',
key: 'id',
children: 'children'
}
// 显示对话框
const open = (openMode = 'add') => {
mode.value = openMode
visible.value = true
return {
setData,
open,
close
}
}
// 关闭对话框
const close = () => {
visible.value = false
}
// 处理取消
const handleCancel = () => {
emit('closed')
visible.value = false
}
// 表单提交方法
const submit = async () => {
try {
await dialogForm.value.validate()
isSaving.value = true
let res = {}
form.parent_id = form.parent_id || 0
if (mode.value === 'add') {
res = await authApi.department.add.post(form)
} else {
res = await authApi.department.edit.post(form)
}
isSaving.value = false
if (res.code === 1) {
emit('success', form, mode.value)
visible.value = false
message.success('操作成功')
} else {
message.error(res.message)
}
} catch (error) {
console.error('表单验证失败', error)
isSaving.value = false
}
}
// 加载部门树数据
const loadDepartments = async () => {
try {
const res = await authApi.department.list.get({ is_tree: 1 })
departments.value = res.data || []
} catch (error) {
console.error('加载部门树失败:', error)
message.error('加载部门树失败')
}
}
// 表单注入数据 // 表单注入数据
setData(data){ const setData = (data) => {
this.form.id = data.id form.id = data.id
this.form.title = data.title form.title = data.title
this.form.name = data.name form.name = data.name
this.form.sort = data.sort form.sort = data.sort
this.form.parent_id = data.parent_id form.parent_id = data.parent_id || null
}
}
} }
// 组件挂载时加载数据
loadDepartments()
// 暴露方法给父组件
defineExpose({
open,
setData,
close
})
</script> </script>
<style></style>

View File

@@ -1,170 +1,330 @@
<template> <template>
<el-container> <div class="pages permission-page">
<el-aside width="300px" v-loading="menuloading"> <div class="left-box">
<el-container> <div class="header">
<el-header> <a-input v-model:value="menuFilterText" placeholder="搜索菜单..." allow-clear @change="handleMenuSearch">
<el-input placeholder="输入关键字进行过滤" v-model="menuFilterText" clearable></el-input> <template #prefix>
</el-header> <search-outlined style="color: rgba(0, 0, 0, 0.45)" />
<el-main class="nopadding"> </template>
<el-tree ref="menu" class="menu" node-key="id" :data="menuList" :props="menuProps" draggable highlight-current :expand-on-click-node="false" check-strictly show-checkbox :filter-node-method="menuFilterNode" @node-click="menuClick" @node-drop="nodeDrop"> </a-input>
</div>
<template #default="{node, data}"> <div class="body">
<span class="custom-tree-node el-tree-node__label"> <a-tree v-model:selectedKeys="selectedMenuKeys" v-model:checkedKeys="checkedMenuKeys"
<span class="label"> :tree-data="filteredMenuTree" :field-names="{ title: 'title', key: 'id', children: 'children' }"
{{ node.label }} showLine checkable :check-strictly="true" :expand-on-click-node="false"
</span> @select="onMenuSelect" @check="onMenuCheck">
<span class="do"> <template #icon="{ dataRef }">
<el-icon @click.stop="add(node, data)"><el-icon-plus /></el-icon> <folder-outlined v-if="dataRef.children" />
</span> <file-outlined v-else />
</span> </template>
<template #title="{ dataRef }">
<span class="tree-node-title">{{ dataRef.title }}</span>
<plus-outlined class="tree-node-add" @click.stop="handleAdd(dataRef)" />
</template>
</a-tree>
</div>
<div class="footer">
<a-space>
<a-button type="primary" @click="handleAdd(null)">
<template #icon><plus-outlined /></template>
新增
</a-button>
<a-button danger @click="handleDeleteBatch">
<template #icon><delete-outlined /></template>
删除
</a-button>
</a-space>
</div>
</div>
<div class="right-box">
<div class="header">
<div class="title">{{ selectedMenu?.title || '请选择菜单' }}</div>
</div>
<div class="body">
<save-form v-if="selectedMenu" :menu="menuTree" :menu-id="selectedMenu.id" :parent-id="parentId"
@success="handleSaveSuccess" />
<a-empty v-else description="请选择左侧菜单后操作" :image-size="100" />
</div>
</div>
</div>
</template> </template>
</el-tree> <script setup>
</el-main> import { ref, reactive, onMounted } from 'vue'
<el-footer style="height:51px;"> import { message, Modal } from 'ant-design-vue'
<el-button type="primary" icon="el-icon-plus" @click="add()"></el-button> import saveForm from './save.vue'
<el-button type="danger" plain icon="el-icon-delete" @click="delMenu"></el-button> import authApi from '@/api/auth'
</el-footer>
</el-container>
</el-aside>
<el-container>
<el-main class="nopadding" style="padding:20px;" ref="main">
<save ref="save" :menu="menuList"></save>
</el-main>
</el-container>
</el-container>
</template>
<script> defineOptions({
let newMenuIndex = 1; name: 'authPermission'
import save from './save' })
export default { // 菜单树数据
name: "auth.permission", const menuTree = ref([])
components: { const filteredMenuTree = ref([])
save const selectedMenuKeys = ref([])
}, const checkedMenuKeys = ref([])
data(){ const menuFilterText = ref('')
return {
menuloading: false,
menuList: [],
menuProps: {
label: (data)=>{
return data.title
}
},
menuFilterText: ""
}
},
watch: {
menuFilterText(val){
this.$refs.menu.filter(val);
}
},
mounted() {
this.getMenu();
},
methods: {
//加载树数据
async getMenu(){
this.menuloading = true
var res = await this.$API.auth.menu.list.get({is_tree: 1});
if(res.code == 1){
this.menuloading = false
this.menuList = res.data;
}
},
//树点击
menuClick(data, node){
var pid = node.level==1?undefined:node.parent.data.id;
this.$refs.save.setData(data, pid)
this.$refs.main.$el.scrollTop = 0
},
//树过滤
menuFilterNode(value, data){
if (!value) return true;
var targetText = data.title;
return targetText.indexOf(value) !== -1;
},
//树拖拽
nodeDrop(draggingNode, dropNode, dropType){
if(dropType == 'before'){
}else if(dropType == 'after'){ // 当前选中的菜单
const selectedMenu = ref(null)
}else if(dropType == 'inner'){ const parentId = ref(null)
// 加载菜单树
const loadMenuTree = async () => {
try {
const res = await authApi.menu.list.get({ is_tree: 1 })
if (res.code === 1) {
menuTree.value = res.data || []
filteredMenuTree.value = res.data || []
} }
}, } catch (error) {
//增加 console.error('加载菜单树失败:', error)
async add(node, data){ }
var newMenuName = "新菜单" + newMenuIndex++; }
var newMenuData = {
parent_id: data ? data.id : 0, // 菜单搜索
name: newMenuName, const handleMenuSearch = (e) => {
path: "", const keyword = e.target?.value || ''
component: "", menuFilterText.value = keyword
title: newMenuName, if (!keyword) {
type: "menu", filteredMenuTree.value = menuTree.value
return
}
// 递归过滤菜单树
const filterTree = (nodes) => {
return nodes.reduce((acc, node) => {
const isMatch = node.title && node.title.toLowerCase().includes(keyword.toLowerCase())
const filteredChildren = node.children ? filterTree(node.children) : []
if (isMatch || filteredChildren.length > 0) {
acc.push({
...node,
children: filteredChildren.length > 0 ? filteredChildren : undefined
})
}
return acc
}, [])
}
filteredMenuTree.value = filterTree(menuTree.value)
}
// 查找菜单节点
const findMenuNode = (tree, id) => {
for (const node of tree) {
if (node.id === id) {
return node
}
if (node.children && node.children.length > 0) {
const found = findMenuNode(node.children, id)
if (found) return found
}
}
return null
}
// 查找父节点ID
const findParentId = (tree, id) => {
for (const node of tree) {
if (node.children && node.children.length > 0) {
const child = node.children.find(child => child.id === id)
if (child) {
return node.id
}
const found = findParentId(node.children, id)
if (found !== null) return found
}
}
return null
}
// 菜单选择事件
const onMenuSelect = (selectedKeys, { selected, node }) => {
if (selected) {
const menuId = selectedKeys[0]
const menuNode = findMenuNode(menuTree.value, menuId)
selectedMenu.value = menuNode
parentId.value = findParentId(menuTree.value, menuId)
} else {
selectedMenu.value = null
parentId.value = null
}
}
// 菜单勾选事件
const onMenuCheck = (checkedKeys, info) => {
console.log('checkedKeys:', checkedKeys, 'info:', info)
}
// 新增菜单
const handleAdd = async (parentNode) => {
try {
let newMenuData = {
parent_id: parentNode ? parentNode.id : 0,
name: '新菜单',
path: '',
component: '',
title: '新菜单',
type: 'menu',
sort: 0 sort: 0
} }
this.menuloading = true
var res = await this.$API.auth.menu.add.post(newMenuData) const res = await authApi.menu.add.post(newMenuData)
this.menuloading = false if (res.code === 1) {
newMenuData.id = res.data.id newMenuData.id = res.data.id
message.success('添加成功')
await loadMenuTree()
this.$refs.menu.append(newMenuData, node) // 选中新增的菜单
this.$refs.menu.setCurrentKey(newMenuData.id) selectedMenuKeys.value = [newMenuData.id]
var pid = node ? node.data.id : "" const menuNode = findMenuNode(menuTree.value, newMenuData.id)
this.$refs.save.setData(newMenuData, pid) selectedMenu.value = menuNode
}, parentId.value = parentNode ? parentNode.id : null
//删除菜单
async delMenu(){
var CheckedNodes = this.$refs.menu.getCheckedNodes()
if(CheckedNodes.length == 0){
this.$message.warning("请选择需要删除的项")
return false;
}
var confirm = await this.$confirm('确认删除已选择的菜单吗?','提示', {
type: 'warning',
confirmButtonText: '删除',
confirmButtonClass: 'el-button--danger'
}).catch(() => {})
if(confirm != 'confirm'){
return false
}
this.menuloading = true
var reqData = {
ids: CheckedNodes.map(item => item.id)
}
var res = await this.$API.auth.menu.delete.post(reqData)
this.menuloading = false
if(res.code == 1){
CheckedNodes.forEach(item => {
var node = this.$refs.menu.getNode(item)
if(node.isCurrent){
this.$refs.save.setData({})
}
this.$refs.menu.remove(item)
})
} else { } else {
this.$message.warning(res.msg) message.error(res.message || '添加失败')
}
} catch (error) {
console.error('添加菜单失败:', error)
message.error('添加失败')
} }
} }
// 批量删除菜单
const handleDeleteBatch = async () => {
if (checkedMenuKeys.value.length === 0) {
message.warning('请选择需要删除的菜单')
return
}
Modal.confirm({
title: '确认删除',
content: `确定删除已选择的 ${checkedMenuKeys.value.length} 个菜单吗?`,
okText: '删除',
okType: 'danger',
cancelText: '取消',
onOk: async () => {
try {
const res = await authApi.menu.delete.post({ ids: checkedMenuKeys.value })
if (res.code === 1) {
message.success('删除成功')
// 如果当前选中的菜单被删除了,清空选择
if (selectedMenu.value && checkedMenuKeys.value.includes(selectedMenu.value.id)) {
selectedMenu.value = null
selectedMenuKeys.value = []
}
checkedMenuKeys.value = []
await loadMenuTree()
} else {
message.error(res.message || '删除失败')
}
} catch (error) {
console.error('删除菜单失败:', error)
message.error('删除失败')
} }
} }
})
}
// 保存成功回调
const handleSaveSuccess = async () => {
await loadMenuTree()
// 重新设置当前选中的菜单
if (selectedMenu.value) {
const menuNode = findMenuNode(menuTree.value, selectedMenu.value.id)
selectedMenu.value = menuNode
}
}
// 初始化
onMounted(() => {
loadMenuTree()
})
</script> </script>
<style scoped> <style scoped lang="scss">
.custom-tree-node {display: flex;flex: 1;align-items: center;justify-content: space-between;font-size: 14px;padding-right: 24px;height:100%;} .permission-page {
.custom-tree-node .label {display: flex;align-items: center;;height: 100%;} display: flex;
.custom-tree-node .label .el-tag {margin-left: 5px;} flex-direction: row;
.custom-tree-node .do {display: none;} height: 100%;
.custom-tree-node .do i {margin-left:5px;color: #999;} padding: 0;
.custom-tree-node .do i:hover {color: #333;}
.custom-tree-node:hover .do {display: inline-block;} .left-box {
width: 300px;
border-right: 1px solid #f0f0f0;
display: flex;
flex-direction: column;
background: #fff;
.header {
padding: 12px 16px;
border-bottom: 1px solid #f0f0f0;
background: #fafafa;
}
.body {
flex: 1;
overflow-y: auto;
padding: 12px;
.tree-node-title {
flex: 1;
}
.tree-node-add {
margin-left: 8px;
color: #999;
display: none;
&:hover {
color: #1890ff;
}
}
:deep(.ant-tree-node-content-wrapper) {
width: 100%;
&:hover {
.tree-node-add {
display: inline-block;
}
}
}
}
.footer {
padding: 12px 16px;
border-top: 1px solid #f0f0f0;
background: #fafafa;
}
}
.right-box {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
.header {
padding: 16px 24px;
border-bottom: 1px solid #f0f0f0;
background: #fff;
.title {
font-size: 18px;
font-weight: 500;
color: #333;
}
}
.body {
flex: 1;
overflow-y: auto;
background: #f5f5f5;
}
}
}
</style> </style>

View File

@@ -1,209 +1,305 @@
<template> <template>
<el-row :gutter="40"> <div class="save-form">
<el-col v-if="!form.id"> <a-row :gutter="24">
<el-empty description="请选择左侧菜单后操作" :image-size="100"></el-empty> <a-col :span="12">
</el-col> <a-card title="基本信息" :bordered="false" size="small">
<template v-else> <a-form :model="form" :rules="rules" ref="dialogForm" :label-col="{ span: 6 }"
<el-col :lg="12"> :wrapper-col="{ span: 16 }">
<h2>{{form.title || "新增菜单"}}</h2> <a-form-item label="显示名称" name="title">
<el-form :model="form" :rules="rules" ref="dialogForm" label-width="80px" label-position="left"> <a-input v-model:value="form.title" placeholder="菜单显示名字" allow-clear />
<el-form-item label="显示名称" prop="title"> </a-form-item>
<el-input v-model="form.title" clearable placeholder="菜单显示名字"></el-input> <a-form-item label="上级菜单" name="parent_id">
</el-form-item> <a-tree-select v-model:value="form.parent_id" :tree-data="menuOptions"
<el-form-item label="上级菜单" prop="parentId"> :field-names="menuFieldNames" :tree-default-expand-all="false" show-icon
<el-cascader v-model="form.parentId" :options="menuOptions" :props="menuProps" :show-all-levels="false" placeholder="顶级菜单" clearable disabled></el-cascader> placeholder="顶级菜单" allow-clear tree-node-filter-prop="title"
</el-form-item> :disabled="!!menuId" />
<el-form-item label="类型" prop="type"> </a-form-item>
<el-radio-group v-model="form.type"> <a-form-item label="类型" name="type">
<el-radio-button value="menu">菜单</el-radio-button> <a-radio-group v-model:value="form.type" button-style="solid">
<el-radio-button value="iframe">Iframe</el-radio-button> <a-radio-button value="menu">菜单</a-radio-button>
<el-radio-button value="link">外链</el-radio-button> <a-radio-button value="iframe">Iframe</a-radio-button>
<el-radio-button value="button">按钮</el-radio-button> <a-radio-button value="link">外链</a-radio-button>
</el-radio-group> <a-radio-button value="button">按钮</a-radio-button>
</el-form-item> </a-radio-group>
<el-form-item label="别名" prop="name"> </a-form-item>
<el-input v-model="form.name" clearable placeholder="菜单别名"></el-input> <a-form-item label="别名" name="name">
<div class="el-form-item-msg">系统唯一且与内置组件名一致否则导致缓存失效如类型为Iframe的菜单别名将代替源地址显示在地址栏</div> <a-input v-model:value="form.name" placeholder="菜单别名" allow-clear />
</el-form-item> <div class="form-item-msg">系统唯一且与内置组件名一致否则导致缓存失效如类型为Iframe的菜单别名将代替源地址显示在地址栏</div>
<el-form-item label="菜单图标" prop="icon"> </a-form-item>
<sc-icon-select v-model="form.icon" clearable></sc-icon-select> <a-form-item label="菜单图标" name="icon">
</el-form-item> <a-input v-model:value="form.icon" placeholder="请输入图标类名" allow-clear />
<el-form-item label="路由地址" prop="path"> </a-form-item>
<el-input v-model="form.path" clearable placeholder=""></el-input> <a-form-item label="路由地址" name="path">
</el-form-item> <a-input v-model:value="form.path" placeholder="" allow-clear />
<el-form-item label="重定向" prop="redirect"> </a-form-item>
<el-input v-model="form.redirect" clearable placeholder=""></el-input> <a-form-item label="重定向" name="redirect">
</el-form-item> <a-input v-model:value="form.redirect" placeholder="" allow-clear />
<el-form-item label="排序" prop="sort"> </a-form-item>
<el-input type="number" v-model="form.sort" class="mx-4" :min="0" :max="100" controls-position="right" clearable /> <a-form-item label="排序" name="sort">
</el-form-item> <a-input-number v-model:value="form.sort" :min="0" :max="100" style="width: 100%" />
<el-form-item label="菜单高亮" prop="active"> </a-form-item>
<el-input v-model="form.active" clearable placeholder=""></el-input> <a-form-item label="菜单高亮" name="active">
<div class="el-form-item-msg">子节点或详情页需要高亮的上级菜单路由地址</div> <a-input v-model:value="form.active" placeholder="" allow-clear />
</el-form-item> <div class="form-item-msg">子节点或详情页需要高亮的上级菜单路由地址</div>
<el-form-item label="视图" prop="component"> </a-form-item>
<el-input v-model="form.component" clearable placeholder=""> <a-form-item label="视图" name="component">
<template #prepend>pages/</template> <a-input v-model:value="form.component" placeholder="" allow-clear>
</el-input> <template #addonBefore>pages/</template>
<div class="el-form-item-msg">如父节点链接或Iframe等没有视图的菜单不需要填写</div> </a-input>
</el-form-item> <div class="form-item-msg">如父节点链接或Iframe等没有视图的菜单不需要填写</div>
<el-form-item label="颜色" prop="color"> </a-form-item>
<el-color-picker v-model="form.color" :predefine="predefineColors"></el-color-picker> <a-form-item label="颜色" name="color">
</el-form-item> <a-input v-model:value="form.color" placeholder="请输入颜色值" allow-clear />
<el-form-item label="是否隐藏" prop="hidden"> </a-form-item>
<el-checkbox v-model="form.hidden">隐藏菜单</el-checkbox> <a-form-item label="是否隐藏" name="hidden">
<el-checkbox v-model="form.hiddenBreadcrumb">隐藏面包屑</el-checkbox> <a-checkbox v-model:checked="form.hidden">隐藏菜单</a-checkbox>
<div class="el-form-item-msg">菜单不显示在导航中但用户依然可以访问例如详情页</div> <a-checkbox v-model:checked="form.hiddenBreadcrumb">隐藏面包屑</a-checkbox>
</el-form-item> <div class="form-item-msg">菜单不显示在导航中但用户依然可以访问例如详情页</div>
<el-form-item label="是否固定" prop="affix"> </a-form-item>
<el-switch v-model="form.affix" /> <a-form-item label="是否固定" name="affix">
<div class="el-form-item-msg">是否固定类似首页控制台在标签中是没有关闭按钮的</div> <a-switch v-model:checked="form.affix" />
</el-form-item> <div class="form-item-msg">是否固定类似首页控制台在标签中是没有关闭按钮的</div>
<el-form-item label="是否全屏" prop="fullpage"> </a-form-item>
<el-switch v-model="form.fullpage" /> <a-form-item label="是否全屏" name="fullpage">
<div class="el-form-item-msg">是否全屏</div> <a-switch v-model:checked="form.fullpage" />
</el-form-item> <div class="form-item-msg">是否全屏</div>
<el-form-item> </a-form-item>
<el-button type="primary" @click="save" :loading="loading"> </el-button> <a-form-item :wrapper-col="{ span: 16, offset: 6 }">
</el-form-item> <a-button type="primary" @click="handleSave" :loading="loading">保存</a-button>
</el-form> </a-form-item>
</a-form>
</el-col> </a-card>
<el-col :lg="12" class="apilist"> </a-col>
<h2>接口权限</h2> <a-col :span="12">
<sc-form-table v-model="form.apiList" :addTemplate="apiListAddTemplate" placeholder="暂无匹配接口权限"> <a-card title="接口权限" :bordered="false" size="small">
<el-table-column prop="code" label="标识" width="150"> <a-button type="dashed" block @click="addApiRow">
<template #default="scope"> <template #icon><plus-outlined /></template>
<el-input v-model="scope.row.code" placeholder="请输入内容"></el-input> 添加接口权限
</a-button>
<a-list :data-source="form.apiList" item-layout="horizontal" style="margin-top: 16px">
<template #renderItem="{ item, index }">
<a-list-item>
<a-row :gutter="8" style="width: 100%">
<a-col :span="8">
<a-input v-model:value="item.code" placeholder="标识" allow-clear />
</a-col>
<a-col :span="14">
<a-input v-model:value="item.url" placeholder="Api url" allow-clear />
</a-col>
<a-col :span="2">
<a-button type="text" danger @click="removeApiRow(index)">
<template #icon><delete-outlined /></template>
</a-button>
</a-col>
</a-row>
</a-list-item>
</template> </template>
</el-table-column> </a-list>
<el-table-column prop="url" label="Api url"> <a-empty v-if="form.apiList.length === 0" description="暂无接口权限" style="margin-top: 16px" />
<template #default="scope"> </a-card>
<el-input v-model="scope.row.url" placeholder="请输入内容"></el-input> </a-col>
</template> </a-row>
</el-table-column> </div>
</sc-form-table>
</el-col>
</template>
</el-row>
</template> </template>
<script> <script setup>
import scIconSelect from '@/components/scIconSelect' import { ref, reactive, watch, onMounted } from 'vue'
import { message } from 'ant-design-vue'
import authApi from '@/api/auth'
export default { const props = defineProps({
components: { menu: { type: Object, default: () => [] },
scIconSelect menuId: { type: [Number, String], default: null },
}, parentId: { type: [Number, String], default: null }
props: { })
menu: { type: Object, default: () => {} },
}, const emit = defineEmits(['success'])
data(){
return { // 表单数据
form: { const form = reactive({
id: "", id: '',
parentId: "", parent_id: 0,
name: "", name: '',
path: "", path: '',
component: "", component: '',
redirect: "", redirect: '',
sort: 0, sort: 0,
title: "", title: '',
icon: "", icon: '',
active: "", active: '',
color: "", color: '',
type: "menu", type: 'menu',
affix: false, affix: false,
hidden: false,
hiddenBreadcrumb: false,
fullpage: false,
apiList: [] apiList: []
}, })
menuOptions: [],
menuProps: { // 表单引用
const dialogForm = ref()
const loading = ref(false)
// 验证规则
const rules = {
title: [{ required: true, message: '请输入显示名称', trigger: 'blur' }],
name: [{ required: true, message: '请输入别名', trigger: 'blur' }],
type: [{ required: true, message: '请选择类型', trigger: 'change' }]
}
// 菜单选项
const menuOptions = ref([])
const menuFieldNames = {
value: 'id', value: 'id',
label: 'title', label: 'title',
checkStrictly: true children: 'children'
},
predefineColors: [
'#ff4500',
'#ff8c00',
'#ffd700',
'#67C23A',
'#00ced1',
'#409EFF',
'#c71585'
],
rules: [],
apiListAddTemplate: {
code: "",
url: ""
},
loading: false
} }
},
watch: {
menu: {
handler(){
this.menuOptions = this.treeToMap(this.menu)
},
deep: true
}
},
mounted() {
},
methods: {
// 简单化菜单 // 简单化菜单
treeToMap(tree){ const treeToMap = (tree) => {
const map = [] const map = []
tree.forEach(item => { tree.forEach(item => {
var obj = { const obj = {
id: item.id, id: item.id,
parentId: item.parentId, parent_id: item.parent_id,
title: item.title, title: item.title,
children: item.children&&item.children.length>0 ? this.treeToMap(item.children) : null children: item.children && item.children.length > 0 ? treeToMap(item.children) : null
} }
map.push(obj) map.push(obj)
}) })
return map return map
},
//保存
async save(){
this.loading = true
let res = {};
this.form.parent_id = this.form.parent_id ? this.form.parent_id : 0;
if(this.form.id){
res = await this.$API.auth.menu.edit.post(this.form)
}else{
res = await this.$API.auth.menu.add.post(this.form)
} }
this.loading = false // 监听菜单树变化
if(res.code == 1){ watch(
this.$message.success("保存成功") () => props.menu,
// this.$TOOL.data.set("MENU", res.data) (newVal) => {
}else{ if (newVal) {
this.$message.error(res.message) menuOptions.value = treeToMap(newVal)
} }
}, },
{ deep: true, immediate: true }
)
// 添加接口权限行
const addApiRow = () => {
form.apiList.push({
code: '',
url: ''
})
}
// 删除接口权限行
const removeApiRow = (index) => {
form.apiList.splice(index, 1)
}
// 加载菜单详情
const loadMenuDetail = async (id) => {
try {
const res = await authApi.menu.list.get({ id })
if (res.code === 1 && res.data) {
setData(res.data, props.parentId)
}
} catch (error) {
console.error('加载菜单详情失败:', error)
}
}
// 保存
const handleSave = async () => {
try {
await dialogForm.value.validate()
loading.value = true
let res = {}
form.parent_id = form.parent_id || 0
if (form.id) {
res = await authApi.menu.edit.post(form)
} else {
res = await authApi.menu.add.post(form)
}
loading.value = false
if (res.code === 1) {
message.success('保存成功')
emit('success')
} else {
message.error(res.message || '保存失败')
}
} catch (error) {
console.error('表单验证失败', error)
loading.value = false
}
}
// 表单注入数据 // 表单注入数据
setData(data, pid){ const setData = (data, pid) => {
this.form = data form.id = data.id
this.form.hidden = this.form.hidden == 1 ? true : false; form.parent_id = data.parent_id || pid || 0
this.form.hiddenBreadcrumb = this.form.hiddenBreadcrumb == 1 ? true : false; form.name = data.name || ''
this.form.affix = this.form.affix == 1 ? true : false; form.path = data.path || ''
this.form.fullpage = this.form.fullpage == 1 ? true : false; form.component = data.component || ''
this.form.apiList = data.apiList || [] form.redirect = data.redirect || ''
this.form.sort = data.sort + "" form.sort = data.sort || 0
this.form.parent_id = data.parent_id || pid form.title = data.title || ''
} form.icon = data.icon || ''
form.active = data.active || ''
form.color = data.color || ''
form.type = data.type || 'menu'
form.affix = data.affix == 1
form.hidden = data.hidden == 1
form.hiddenBreadcrumb = data.hiddenBreadcrumb == 1
form.fullpage = data.fullpage == 1
form.apiList = data.apiList || []
} }
// 初始化
onMounted(() => {
if (props.menuId) {
loadMenuDetail(props.menuId)
} else if (props.parentId) {
form.parent_id = props.parentId
} }
})
// 暴露方法给父组件
defineExpose({
setData
})
</script> </script>
<style scoped> <style scoped lang="scss">
h2 {font-size: 17px;color: #3c4a54;padding:0 0 30px 0;} .save-form {
.apilist {border-left: 1px solid #eee;} .form-item-msg {
font-size: 12px;
color: #999;
margin-top: 4px;
}
[data-theme="dark"] h2 {color: #fff;} :deep(.ant-card) {
[data-theme="dark"] .apilist {border-color: #434343;} .ant-card-head {
background: #fafafa;
border-bottom: 1px solid #f0f0f0;
.ant-card-head-title {
font-weight: 500;
font-size: 16px;
}
}
.ant-card-body {
padding: 24px;
}
}
:deep(.ant-list-item) {
padding: 12px 0;
border-bottom: 1px solid #f0f0f0;
&:last-child {
border-bottom: none;
}
}
}
</style> </style>

View File

@@ -1,173 +1,323 @@
<template> <template>
<el-container> <div class="pages role-page">
<el-header> <div class="tool-bar">
<div class="left-panel"> <div class="left-panel">
<el-button type="primary" icon="el-icon-plus" @click="add"></el-button> <a-form layout="inline" :model="searchForm">
<el-button type="danger" plain icon="el-icon-delete" :disabled="selection.length==0" @click="batch_del"></el-button> <a-form-item>
<el-button type="primary" plain :disabled="selection.length!=1" @click="permission">权限设置</el-button> <a-input v-model:value="searchForm.keyword" placeholder="请输入角色名称" allow-clear
style="width: 200px" />
</a-form-item>
<a-form-item>
<a-space>
<a-button type="primary" @click="handleSearch">
<template #icon><search-outlined /></template>
</a-button>
<a-button @click="handleReset">
<template #icon><redo-outlined /></template>
</a-button>
</a-space>
</a-form-item>
</a-form>
</div> </div>
<div class="right-panel"> <div class="right-panel">
<div class="right-panel-search"> <a-button type="primary" @click="handleAdd">
<el-input v-model="search.keyword" placeholder="角色名称" clearable></el-input> <template #icon><plus-outlined /></template>
<el-button type="primary" icon="el-icon-search" @click="upsearch"></el-button> </a-button>
<a-button danger :disabled="selection.length === 0" @click="handleBatchDelete">
<template #icon><delete-outlined /></template>
</a-button>
<a-button :disabled="selection.length !== 1" @click="handlePermission">
<template #icon><key-outlined /></template>
</a-button>
</div> </div>
</div> </div>
</el-header> <div class="table-content">
<el-main class="nopadding"> <scTable ref="tableRef" :columns="columns" :data-source="tableData" :loading="loading"
<scTable ref="table" :apiObj="apiObj" row-key="id" @selection-change="selectionChange" :params="search"> :pagination="pagination" :row-key="rowKey" :row-selection="rowSelection" @refresh="loadData"
<el-table-column type="selection" width="50"></el-table-column> @paginationChange="handlePaginationChange" @select="handleSelectChange" @selectAll="handleSelectAll">
<el-table-column label="ID" prop="id" width="50"></el-table-column> <template #status="{ record }">
<el-table-column label="角色名称" prop="title"></el-table-column> <a-tag :color="record.status === 1 ? 'success' : 'error'">
<el-table-column label="别名" prop="name" width="150"></el-table-column> {{ record.status === 1 ? '正常' : '禁用' }}
<el-table-column label="状态" prop="status" width="150"> </a-tag>
<template #default="scope">
<el-tag :type="scope.row.status == 1 ? 'success' : 'danger'">{{ scope.row.status == 1 ? '正常' : '禁用' }}</el-tag>
</template> </template>
</el-table-column> <template #action="{ record }">
<el-table-column label="操作" fixed="right" align="right" width="180"> <a-space>
<template #default="scope"> <a-button type="link" size="small" @click="handleView(record)">查看</a-button>
<el-button-group> <a-button type="link" size="small" @click="handleEdit(record)">编辑</a-button>
<el-button type="success" @click="table_show(scope.row, scope.$index)">查看</el-button> <a-popconfirm title="确定删除该角色吗?" @confirm="handleDelete(record)">
<el-button type="primary" @click="table_edit(scope.row, scope.$index)">编辑</el-button> <a-button type="link" size="small" danger>删除</a-button>
<el-popconfirm title="确定删除吗?" @confirm="table_del(scope.row, scope.$index)"> </a-popconfirm>
<template #reference> </a-space>
<el-button type="danger">删除</el-button>
</template> </template>
</el-popconfirm>
</el-button-group>
</template>
</el-table-column>
</scTable> </scTable>
</el-main> </div>
</el-container> </div>
<save-dialog v-if="dialog.save" ref="saveDialog" @success="handleSaveSuccess" @closed="dialog.save=false"></save-dialog> <!-- 新增/编辑角色弹窗 -->
<save-dialog v-if="dialog.save" ref="saveDialogRef" @success="handleSaveSuccess" @closed="dialog.save = false" />
<permission-dialog v-if="dialog.permission" ref="permissionDialog" @closed="dialog.permission=false" @success="permissionSuccess"></permission-dialog>
<!-- 权限设置弹窗 -->
<permission-dialog v-if="dialog.permission" ref="permissionDialogRef" @success="permissionSuccess"
@closed="dialog.permission = false" />
</template> </template>
<script> <script setup>
import saveDialog from './save' import { ref, reactive, onMounted, computed } from 'vue'
import permissionDialog from './permission' import { message, Modal } from 'ant-design-vue'
import scTable from '@/components/scTable/index.vue'
import saveDialog from './save.vue'
import permissionDialog from './permission.vue'
import authApi from '@/api/auth'
export default { defineOptions({
name: 'auth.role', name: 'authRole'
components: { })
saveDialog,
permissionDialog // 表格引用
}, const tableRef = ref(null)
data() {
return { // 对话框状态
dialog: { const dialog = reactive({
save: false, save: false,
permission: false permission: false
}, })
apiObj: this.$API.auth.role.list,
selection: [], // 弹窗引用
search: {keyword: null} const saveDialogRef = ref(null)
const permissionDialogRef = ref(null)
// 选中的行
const selection = ref([])
// 搜索表单
const searchForm = reactive({
keyword: ''
})
// 表格数据
const tableData = ref([])
const loading = ref(false)
// 分页配置
const pagination = reactive({
current: 1,
pageSize: 20,
total: 0,
showSizeChanger: true,
showTotal: (total) => `${total}`,
pageSizeOptions: ['20', '50', '100', '200']
})
// 行key
const rowKey = 'id'
// 行选择配置
const rowSelection = computed(() => ({
selectedRowKeys: selection.value.map(item => item.id),
onChange: (selectedRowKeys, selectedRows) => {
selection.value = selectedRows
}
}))
// 表格列配置
const columns = [
{ title: 'ID', dataIndex: 'id', key: 'id', width: 80, align: 'center' },
{ title: '角色名称', dataIndex: 'title', key: 'title', width: 200 },
{ title: '别名', dataIndex: 'name', key: 'name', width: 200 },
{ title: '排序', dataIndex: 'sort', key: 'sort', width: 100, align: 'center' },
{ title: '状态', dataIndex: 'status', key: 'status', width: 100, align: 'center', slot: 'status' },
{ title: '操作', dataIndex: 'action', key: 'action', width: 200, align: 'center', slot: 'action', fixed: 'right' }
]
// 分页变化处理
const handlePaginationChange = ({ page, pageSize }) => {
pagination.current = page
pagination.pageSize = pageSize
loadData()
}
// 加载角色列表数据
const loadData = async () => {
loading.value = true
try {
const params = {
page: pagination.current,
limit: pagination.pageSize,
...searchForm
}
const res = await authApi.role.list.get(params)
if (res.code === 1) {
tableData.value = res.data?.data || []
pagination.total = res.data?.total || 0
} else {
message.error(res.message || '加载数据失败')
}
} catch (error) {
console.error('加载角色列表失败:', error)
message.error('加载数据失败')
} finally {
loading.value = false
}
}
// 选择变化处理
const handleSelectChange = (record, selected, selectedRows) => {
if (selected) {
selection.value.push(record)
} else {
const index = selection.value.findIndex(item => item.id === record.id)
if (index > -1) {
selection.value.splice(index, 1)
}
}
}
// 全选/取消全选处理
const handleSelectAll = (selected, selectedRows, changeRows) => {
if (selected) {
changeRows.forEach(record => {
if (!selection.value.find(item => item.id === record.id)) {
selection.value.push(record)
} }
},
methods: {
//添加
add(){
this.dialog.save = true
this.$nextTick(() => {
this.$refs.saveDialog.open()
}) })
}, } else {
//编辑 changeRows.forEach(record => {
table_edit(row){ const index = selection.value.findIndex(item => item.id === record.id)
this.dialog.save = true if (index > -1) {
this.$nextTick(() => { selection.value.splice(index, 1)
this.$refs.saveDialog.open('edit').setData(row) }
}) })
}, }
//查看 }
table_show(row){
this.dialog.save = true // 搜索
this.$nextTick(() => { const handleSearch = () => {
this.$refs.saveDialog.open('show').setData(row) pagination.current = 1
}) loadData()
}, }
//权限设置
permission(){ // 重置
if(this.selection.length != 1){ const handleReset = () => {
this.$message.error("请选择要设置的角色") searchForm.keyword = ''
selection.value = []
pagination.current = 1
loadData()
}
// 新增角色
const handleAdd = () => {
dialog.save = true
setTimeout(() => {
saveDialogRef.value?.open('add')
}, 0)
}
// 查看角色
const handleView = (record) => {
dialog.save = true
setTimeout(() => {
saveDialogRef.value?.open('show').setData(record)
}, 0)
}
// 编辑角色
const handleEdit = (record) => {
dialog.save = true
setTimeout(() => {
saveDialogRef.value?.open('edit').setData(record)
}, 0)
}
// 删除角色
const handleDelete = async (record) => {
try {
const res = await authApi.role.delete.post({ id: record.id })
if (res.code === 1) {
message.success('删除成功')
loadData()
} else {
message.error(res.message || '删除失败')
}
} catch (error) {
console.error('删除角色失败:', error)
message.error('删除失败')
}
}
// 批量删除
const handleBatchDelete = () => {
if (selection.value.length === 0) {
message.warning('请选择要删除的角色')
return return
} }
this.dialog.permission = true
this.$nextTick(() => {
this.$refs.permissionDialog.open().setData(this.selection[0])
})
},
//删除
async table_del(row){
var reqData = {id: row.id}
var res = await this.$API.auth.role.delete.post(reqData);
if(res.code == 1){
this.$refs.table.refresh()
this.$message.success("删除成功")
}else{
this.$message.error(res.message)
}
},
//批量删除
async batch_del(){
this.$confirm(`确定删除选中的 ${this.selection.length} 项吗?如果删除项中含有子集将会被一并删除`, '提示', {
type: 'warning'
}).then(async () => {
var ids = this.selection.map(item => item.id)
var reqData = {ids: ids}
var res = await this.$API.auth.role.delete.post(reqData);
if(res.code == 1){
const loading = this.$loading();
this.$refs.table.refresh()
loading.close();
this.$message.success("操作成功")
}else{
this.$message.error(res.message)
}
}).catch(() => {
}) Modal.confirm({
}, title: '确认删除',
//表格选择后回调事件 content: `确定删除选中的 ${selection.value.length} 个角色吗?`,
selectionChange(selection){ okText: '确定',
this.selection = selection; cancelText: '取消',
}, okType: 'danger',
//搜索 onOk: async () => {
upsearch(){ try {
this.$refs.table.upData(this.search) const ids = selection.value.map(item => item.id)
}, const res = await authApi.role.delete.post({ ids })
//根据ID获取树结构 if (res.code === 1) {
filterTree(id){ message.success('删除成功')
var target = null; selection.value = []
function filter(tree){ loadData()
tree.forEach(item => { } else {
if(item.id == id){ message.error(res.message || '删除失败')
target = item }
} catch (error) {
console.error('批量删除角色失败:', error)
message.error('删除失败')
} }
if(item.children){
filter(item.children)
} }
}) })
} }
filter(this.$refs.table.tableData)
return target // 权限设置
}, const handlePermission = () => {
//本地更新数据 if (selection.value.length !== 1) {
handleSaveSuccess(data, mode){ message.error('请选择一个角色进行权限设置')
if(mode=='add'){ return
this.$refs.table.refresh()
}else if(mode=='edit'){
this.$refs.table.refresh()
} }
}, dialog.permission = true
permissionSuccess(){ setTimeout(() => {
this.$refs.table.refresh() permissionDialogRef.value?.open().setData(selection.value[0])
}, 0)
}
// 保存成功回调
const handleSaveSuccess = (data, mode) => {
if (mode === 'add') {
loadData()
} else if (mode === 'edit') {
loadData()
} }
} }
// 权限设置成功回调
const permissionSuccess = () => {
loadData()
} }
// 初始化
onMounted(() => {
loadData()
})
</script> </script>
<style scoped lang="scss">
.role-page {
display: flex;
flex-direction: column;
height: 100%;
padding: 0;
.table-content {
flex: 1;
overflow: hidden;
}
}
</style>

View File

@@ -1,129 +1,181 @@
<template> <template>
<el-dialog title="角色权限设置" v-model="visible" :width="500" destroy-on-close @closed="$emit('closed')"> <a-modal title="角色权限设置" :open="visible" :width="600" :destroy-on-close="true" :footer="null" @cancel="handleCancel">
<el-tabs tab-position="top"> <a-tabs tab-position="top">
<el-tab-pane label="菜单权限"> <a-tab-pane key="menu" tab="菜单权限">
<div class="treeMain"> <div class="treeMain">
<el-tree ref="menu" node-key="id" :data="menu.list" :default-checked-keys="menu.checked" :props="menu.props" check-strictly show-checkbox></el-tree> <a-tree ref="menuTreeRef" v-model:checkedKeys="menu.checked" :tree-data="menu.list"
:field-names="menu.fieldNames" :checkable="true" :default-expand-all="true"
:check-strictly="true" :selectable="false">
<template #title="{ title }">
{{ title }}
</template>
</a-tree>
</div> </div>
</el-tab-pane> </a-tab-pane>
<el-tab-pane label="数据权限"> <a-tab-pane key="data" tab="数据权限">
<el-form label-width="100px" label-position="left"> <a-form :label-col="{ span: 5 }" :wrapper-col="{ span: 18 }">
<el-form-item label="数据权限"> <a-form-item label="数据权限">
<sc-select v-model="form.data_range" dic="data_auth" style="width: 80%;"></sc-select> <a-select v-model:value="form.data_range" placeholder="请选择数据权限" style="width: 100%">
</el-form-item> <a-select-option value="0">全部数据权限</a-select-option>
</el-form> <a-select-option value="1">本部门及以下数据</a-select-option>
</el-tab-pane> <a-select-option value="2">本部门数据权限</a-select-option>
<el-tab-pane label="控制台"> <a-select-option value="3">仅本人数据权限</a-select-option>
<el-form label-width="100px" label-position="left"> <a-select-option value="4">自定义数据权限</a-select-option>
<el-form-item label="控制台视图"> </a-select>
<el-select v-model="form.dashboard" placeholder="请选择"> </a-form-item>
<el-option v-for="item in dashboardOptions" :key="item.value" :label="item.label" :value="item.value"> </a-form>
<span style="float: left">{{ item.label }}</span> </a-tab-pane>
<span style="float: right; color: #8492a6; font-size: 12px">{{ item.views }}</span> <a-tab-pane key="dashboard" tab="控制台">
</el-option> <a-form :label-col="{ span: 5 }" :wrapper-col="{ span: 18 }">
</el-select> <a-form-item label="控制台视图">
<div class="el-form-item-msg">用于控制角色登录后控制台的视图</div> <a-select v-model:value="form.dashboard" placeholder="请选择" style="width: 100%">
</el-form-item> <a-select-option value="0">
</el-form> <div style="display: flex; justify-content: space-between">
</el-tab-pane> <span>数据统计</span>
</el-tabs> <span style="color: #8492a6; font-size: 12px">stats</span>
</div>
</a-select-option>
<a-select-option value="1">
<div style="display: flex; justify-content: space-between">
<span>工作台</span>
<span style="color: #8492a6; font-size: 12px">work</span>
</div>
</a-select-option>
</a-select>
<div class="ant-form-item-explain">用于控制角色登录后控制台的视图</div>
</a-form-item>
</a-form>
</a-tab-pane>
</a-tabs>
<template #footer> <template #footer>
<el-button @click="visible=false" > </el-button> <a-button @click="handleCancel"> </a-button>
<el-button type="primary" :loading="isSaveing" @click="submit()"> </el-button> <a-button type="primary" :loading="isSaveing" @click="submit"> </a-button>
</template> </template>
</el-dialog> </a-modal>
</template> </template>
<script> <script setup>
export default { import { ref, reactive } from 'vue'
emits: ['success', 'closed'], import { message } from 'ant-design-vue'
data() { import authApi from '@/api/auth'
return {
visible: false, const emit = defineEmits(['success', 'closed'])
isSaveing: false,
menu: { const visible = ref(false)
const isSaveing = ref(false)
const menuTreeRef = ref()
const menu = reactive({
list: [], list: [],
checked: [], checked: [],
props: { fieldNames: {
label: (data)=>{ title: 'title',
return data.title key: 'id',
children: 'children'
} }
} })
},
group: { const form = reactive({
list: [],
checked: [],
props: {}
},
type: {
list: [],
checked: [],
props: {}
},
form: {
role_id: 0, role_id: 0,
auth: [], permissions: [],
data_range: "", data_range: '',
dashboard: "work", dashboard: '1'
},
dashboardOptions: [
{
value: '0',
label: '数据统计',
views: 'stats'
},
{
value: '1',
label: '工作台',
views: 'work'
},
]
}
},
mounted() {
this.getMenu();
},
methods: {
open(){
this.visible = true;
return this;
},
async submit(){
let authSelect = this.$refs.menu.getCheckedNodes();
this.form.permissions = [];
authSelect.map(item => {
this.form.permissions.push(item.id);
}) })
this.isSaveing = true;
var res = await this.$API.auth.role.auth.post(this.form);
this.isSaveing = false; // 打开对话框
if(res.code == 1){ const open = () => {
this.$emit('success', this.form) visible.value = true
this.visible = false; return {
this.$message.success("操作成功") open,
setData,
close
}
}
// 关闭对话框
const close = () => {
visible.value = false
}
// 处理取消
const handleCancel = () => {
emit('closed')
visible.value = false
}
// 提交保存
const submit = async () => {
try {
isSaveing.value = true
// 获取选中的菜单权限 ID
form.permissions = menu.checked || []
form.role_id = form.id
const res = await authApi.role.auth.post(form)
isSaveing.value = false
if (res.code === 1) {
emit('success', form)
visible.value = false
message.success('操作成功')
} else { } else {
this.$alert(res.message, "提示", {type: 'error'}) message.error(res.message)
} }
}, } catch (error) {
async getMenu(){ console.error('保存权限失败:', error)
var res = await this.$API.auth.menu.list.get({is_tree: 1}); isSaveing.value = false
this.menu.list = res.data; message.error('操作失败')
}, }
//表单注入数据 }
setData(data){
this.form.id = data.id; // 获取菜单列表
this.form.data_range = data.data_range; const getMenu = async () => {
this.form.dashboard = data.dashboard; try {
// this.form.mobile_module = data.mobile_module; const res = await authApi.menu.list.get({ is_tree: 1 })
data.permissions.map(item => { menu.list = res.data || []
this.menu.checked.push(item.id); } catch (error) {
console.error('获取菜单列表失败:', error)
message.error('获取菜单列表失败')
}
}
// 设置数据
const setData = (data) => {
form.id = data.id
form.data_range = data.data_range || ''
form.dashboard = data.dashboard || '1'
// 设置选中的菜单权限
if (data.permissions && data.permissions.length > 0) {
menu.checked = data.permissions.map(item => item.id)
}
}
// 组件挂载时加载数据
getMenu()
// 暴露方法给父组件
defineExpose({
open,
setData,
close
}) })
}
}
}
</script> </script>
<style scoped> <style scoped>
.treeMain {height:280px;overflow: auto;border: 1px solid #dcdfe6;margin-bottom: 10px;} .treeMain {
height: 280px;
overflow: auto;
border: 1px solid #dcdfe6;
margin-bottom: 10px;
padding: 8px;
}
.ant-form-item-explain {
color: rgba(0, 0, 0, 0.45);
font-size: 12px;
line-height: 1.5;
margin-top: 4px;
}
</style> </style>

View File

@@ -1,121 +1,124 @@
<template> <template>
<el-dialog :title="titleMap[mode]" v-model="visible" :width="500" destroy-on-close @closed="$emit('closed')"> <a-modal :title="titleMap[mode]" :open="visible" :width="500" :destroy-on-close="true" :footer="null"
<el-form :model="form" :rules="rules" :disabled="mode=='show'" ref="dialogForm" label-width="100px" label-position="left"> @cancel="handleCancel">
<el-form-item label="上级角色" prop="parent_id" v-if="false"> <a-form :model="form" :rules="rules" :disabled="mode === 'show'" ref="dialogForm" :label-col="{ span: 5 }"
<el-cascader v-model="form.parent_id" :options="groups" :props="groupsProps" :show-all-levels="false" clearable style="width: 100%;"></el-cascader> :wrapper-col="{ span: 18 }">
</el-form-item> <a-form-item label="角色名称" name="title">
<el-form-item label="角色名称" prop="title"> <a-input v-model:value="form.title" placeholder="请输入角色名称" allow-clear></a-input>
<el-input v-model="form.title" clearable></el-input> </a-form-item>
</el-form-item> <a-form-item label="角色别名" name="name">
<el-form-item label="角色别名" prop="name"> <a-input v-model:value="form.name" placeholder="请输入角色别名" allow-clear></a-input>
<el-input v-model="form.name" clearable></el-input> </a-form-item>
</el-form-item> <a-form-item label="排序" name="sort">
<el-form-item label="排序" prop="sort"> <a-input-number v-model:value="form.sort" :min="1" :step="1" style="width: 100%" placeholder="请输入排序" />
<el-input-number v-model="form.sort" controls-position="right" :min="1" style="width: 100%;"></el-input-number> </a-form-item>
</el-form-item> </a-form>
</el-form>
<template #footer> <template #footer>
<el-button @click="visible=false" > </el-button> <a-button @click="handleCancel"> </a-button>
<el-button v-if="mode!='show'" type="primary" :loading="isSaveing" @click="submit()"> </el-button> <a-button v-if="mode !== 'show'" type="primary" :loading="isSaveing" @click="submit"> </a-button>
</template> </template>
</el-dialog> </a-modal>
</template> </template>
<script> <script setup>
export default { import { ref, reactive } from 'vue'
emits: ['success', 'closed'], import { message } from 'ant-design-vue'
data() { import authApi from '@/api/auth'
return {
mode: "add", const emit = defineEmits(['success', 'closed'])
titleMap: {
add: '新增', const mode = ref('add')
edit: '编辑', const titleMap = {
show: '查看' add: '新增角色',
}, edit: '编辑角色',
visible: false, show: '查看角色'
isSaveing: false, }
const visible = ref(false)
const isSaveing = ref(false)
// 表单数据 // 表单数据
form: { const form = reactive({
id:"", id: '',
title: "", title: '',
name: "", name: '',
sort: 1, sort: 1
parent_id: 0
},
//验证规则
rules: {
sort: [
{required: true, message: '请输入排序', trigger: 'change'}
],
label: [
{required: true, message: '请输入角色名称'}
],
alias: [
{required: true, message: '请输入角色别名'}
]
},
//所需数据选项
groups: [],
groupsProps: {
value: "id",
label: "title",
emitPath: false,
checkStrictly: true
}
}
},
mounted() {
this.getGroup()
},
methods: {
//显示
open(mode='add'){
this.mode = mode;
this.visible = true;
return this
},
//加载树数据
async getGroup(){
var res = await this.$API.auth.role.list.get({is_tree: 1});
this.groups = res.data;
},
//表单提交方法
submit(){
this.$refs.dialogForm.validate(async (valid) => {
if (valid) {
this.isSaveing = true;
var res = {}
this.form.parent_id = this.form.parent_id ? this.form.parent_id : 0;
if(this.mode == 'add'){
res = await this.$API.auth.role.add.post(this.form);
}else{
res = await this.$API.auth.role.edit.post(this.form);
}
this.isSaveing = false;
if(res.code == 1){
this.$emit('success', this.form, this.mode)
this.visible = false;
this.$message.success("操作成功")
}else{
this.$message.error(res.message)
}
}
}) })
},
//表单注入数据
setData(data){
this.form.id = data.id
this.form.title = data.title
this.form.name = data.name
this.form.sort = data.sort
// this.form.parent_id = data.parent_id
//可以和上面一样单个注入,也可以像下面一样直接合并进去 // 表单引用
//Object.assign(this.form, data) const dialogForm = ref()
// 验证规则
const rules = {
title: [{ required: true, message: '请输入角色名称', trigger: 'blur' }],
name: [{ required: true, message: '请输入角色别名', trigger: 'blur' }],
sort: [
{ required: true, message: '请输入排序', trigger: 'change' },
{ type: 'number', message: '排序必须为数字', trigger: 'change' }
]
}
// 显示对话框
const open = (openMode = 'add') => {
mode.value = openMode
visible.value = true
return {
setData,
open,
close
} }
} }
// 关闭对话框
const close = () => {
visible.value = false
} }
// 处理取消
const handleCancel = () => {
emit('closed')
visible.value = false
}
// 表单提交方法
const submit = async () => {
try {
await dialogForm.value.validate()
isSaveing.value = true
let res = {}
if (mode.value === 'add') {
res = await authApi.role.add.post(form)
} else {
res = await authApi.role.edit.post(form)
}
isSaveing.value = false
if (res.code === 1) {
emit('success', form, mode.value)
visible.value = false
message.success('操作成功')
} else {
message.error(res.message)
}
} catch (error) {
console.error('表单验证失败', error)
isSaveing.value = false
}
}
// 表单注入数据
const setData = (data) => {
form.id = data.id
form.title = data.title
form.name = data.name
form.sort = data.sort
}
// 暴露方法给父组件
defineExpose({
open,
setData,
close
})
</script> </script>
<style> <style></style>
</style>

View File

@@ -2,11 +2,15 @@
<div class="pages user-page"> <div class="pages user-page">
<div class="left-box"> <div class="left-box">
<div class="header"> <div class="header">
部门分类 <a-input v-model:value="departmentKeyword" placeholder="搜索部门..." allow-clear @change="handleDeptSearch">
<template #prefix>
<search-outlined style="color: rgba(0, 0, 0, 0.45)" />
</template>
</a-input>
</div> </div>
<div class="body"> <div class="body">
<a-tree v-model:selectedKeys="selectedDeptKeys" :tree-data="departmentTree" <a-tree v-model:selectedKeys="selectedDeptKeys" :tree-data="filteredDepartmentTree"
:field-names="{ title: 'title', key: 'id', children: 'children' }" show-icon @select="onDeptSelect"> :field-names="{ title: 'title', key: 'id', children: 'children' }" showLine @select="onDeptSelect">
<template #icon="{ dataRef }"> <template #icon="{ dataRef }">
<folder-outlined v-if="dataRef.children" /> <folder-outlined v-if="dataRef.children" />
<file-outlined v-else /> <file-outlined v-else />
@@ -15,37 +19,42 @@
</div> </div>
</div> </div>
<div class="right-box"> <div class="right-box">
<div class="search-bar"> <div class="tool-bar">
<div class="left-panel">
<a-form layout="inline" :model="searchForm"> <a-form layout="inline" :model="searchForm">
<a-form-item label="用户名"> <a-form-item>
<a-input v-model:value="searchForm.username" placeholder="请输入用户名" allow-clear /> <a-input v-model:value="searchForm.username" placeholder="请输入用户名" allow-clear
style="width: 160px" />
</a-form-item> </a-form-item>
<a-form-item label="姓名"> <a-form-item>
<a-input v-model:value="searchForm.nickname" placeholder="请输入姓名" allow-clear /> <a-input v-model:value="searchForm.nickname" placeholder="请输入姓名" allow-clear
style="width: 160px" />
</a-form-item> </a-form-item>
<a-form-item> <a-form-item>
<a-space> <a-space>
<a-button type="primary" @click="handleSearch"> <a-button type="primary" @click="handleSearch">
<template #icon><search-outlined /></template> <template #icon><search-outlined /></template>
搜索
</a-button> </a-button>
<a-button @click="handleReset"> <a-button @click="handleReset">
<template #icon><redo-outlined /></template> <template #icon><redo-outlined /></template>
重置
</a-button> </a-button>
</a-space> </a-space>
</a-form-item> </a-form-item>
</a-form> </a-form>
</div> </div>
<div class="table-content"> <div class="right-panel">
<scTable ref="tableRef" :columns="columns" :data-source="tableData" :loading="loading"
:pagination="pagination" :row-key="rowKey" @change="handleTableChange" @refresh="loadData">
<template #toolLeft>
<a-button type="primary" @click="handleAdd"> <a-button type="primary" @click="handleAdd">
<template #icon><plus-outlined /></template> <template #icon><plus-outlined /></template>
新增用户
</a-button> </a-button>
</template> <a-button @click="handleExport">
<template #icon><export-outlined /></template>
</a-button>
</div>
</div>
<div class="table-content">
<scTable ref="tableRef" :columns="columns" :data-source="tableData" :loading="loading"
:pagination="pagination" :row-key="rowKey" @refresh="loadData"
@paginationChange="handlePaginationChange">
<template #avatar="{ record }"> <template #avatar="{ record }">
<a-avatar :src="record.avatar" :size="32"> <a-avatar :src="record.avatar" :size="32">
<template #icon><user-outlined /></template> <template #icon><user-outlined /></template>
@@ -56,12 +65,15 @@
{{ record.status === 1 ? '正常' : '禁用' }} {{ record.status === 1 ? '正常' : '禁用' }}
</a-tag> </a-tag>
</template> </template>
<template #department_title="{ record }">
{{ record.department?.title }}
</template>
<template #roles="{ record }"> <template #roles="{ record }">
<a-tag v-for="role in record.roles" :key="role.id" color="blue"> <a-tag v-for="role in record.roles" :key="role.id" color="blue">
{{ role.title }} {{ role.title }}
</a-tag> </a-tag>
</template> </template>
<template #_action="{ record }"> <template #action="{ record }">
<a-space> <a-space>
<a-button type="link" size="small" @click="handleView(record)">查看</a-button> <a-button type="link" size="small" @click="handleView(record)">查看</a-button>
<a-button type="link" size="small" @click="handleEdit(record)">编辑</a-button> <a-button type="link" size="small" @click="handleEdit(record)">编辑</a-button>
@@ -86,14 +98,6 @@
<script setup> <script setup>
import { ref, reactive, onMounted } from 'vue' import { ref, reactive, onMounted } from 'vue'
import { message } from 'ant-design-vue' import { message } from 'ant-design-vue'
import {
FolderOutlined,
FileOutlined,
SearchOutlined,
RedoOutlined,
PlusOutlined,
UserOutlined
} from '@ant-design/icons-vue'
import scTable from '@/components/scTable/index.vue' import scTable from '@/components/scTable/index.vue'
import saveDialog from './save.vue' import saveDialog from './save.vue'
import roleDialog from './role.vue' import roleDialog from './role.vue'
@@ -118,7 +122,9 @@ const roleDialogRef = ref(null)
// 部门树数据 // 部门树数据
const departmentTree = ref([]) const departmentTree = ref([])
const filteredDepartmentTree = ref([])
const selectedDeptKeys = ref([]) const selectedDeptKeys = ref([])
const departmentKeyword = ref('')
// 搜索表单 // 搜索表单
const searchForm = reactive({ const searchForm = reactive({
@@ -144,64 +150,23 @@ const pagination = reactive({
// 行key // 行key
const rowKey = 'id' const rowKey = 'id'
// 分页变化处理
const handlePaginationChange = ({ page, pageSize }) => {
pagination.current = page
pagination.pageSize = pageSize
loadData()
}
// 表格列配置 // 表格列配置
const columns = [ const columns = [
{ { title: '头像', dataIndex: 'avatar', key: 'avatar', width: 80, align: 'center', slot: 'avatar' },
title: '头像', { title: '用户名', dataIndex: 'username', key: 'username', width: 150 },
dataIndex: 'avatar', { title: '姓名', dataIndex: 'nickname', key: 'nickname', width: 150 },
key: 'avatar', { title: '部门', dataIndex: 'department_title', key: 'department_title', slot: 'department_title', width: 150 },
width: 80, { title: '角色', dataIndex: 'roles', key: 'roles', width: 200, slot: 'roles' },
align: 'center', { title: '状态', dataIndex: 'status', key: 'status', width: 100, align: 'center', slot: 'status' },
slot: 'avatar' { title: '创建时间', dataIndex: 'created_at', key: 'created_at', width: 180 },
}, { title: '操作', dataIndex: 'action', key: 'action', width: 200, align: 'center', slot: 'action', fixed: 'right' }
{
title: '用户名',
dataIndex: 'username',
key: 'username',
width: 150
},
{
title: '姓名',
dataIndex: 'nickname',
key: 'nickname',
width: 150
},
{
title: '部门',
dataIndex: 'department_title',
key: 'department_title',
width: 150
},
{
title: '角色',
dataIndex: 'roles',
key: 'roles',
width: 200,
slot: 'roles'
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
width: 100,
align: 'center',
slot: 'status'
},
{
title: '创建时间',
dataIndex: 'created_at',
key: 'created_at',
width: 180
},
{
title: '操作',
dataIndex: '_action',
key: '_action',
width: 200,
align: 'center',
slot: '_action',
fixed: 'right'
}
] ]
// 加载部门树 // 加载部门树
@@ -210,12 +175,41 @@ const loadDepartmentTree = async () => {
const res = await authApi.department.list.get({ is_tree: 1 }) const res = await authApi.department.list.get({ is_tree: 1 })
if (res.code === 1) { if (res.code === 1) {
departmentTree.value = res.data || [] departmentTree.value = res.data || []
filteredDepartmentTree.value = res.data || []
} }
} catch (error) { } catch (error) {
console.error('加载部门树失败:', error) console.error('加载部门树失败:', error)
} }
} }
// 部门搜索
const handleDeptSearch = (e) => {
const keyword = e.target?.value || ''
departmentKeyword.value = keyword
if (!keyword) {
filteredDepartmentTree.value = departmentTree.value
return
}
// 递归过滤部门树
const filterTree = (nodes) => {
return nodes.reduce((acc, node) => {
const isMatch = node.title && node.title.toLowerCase().includes(keyword.toLowerCase())
const filteredChildren = node.children ? filterTree(node.children) : []
if (isMatch || filteredChildren.length > 0) {
acc.push({
...node,
children: filteredChildren.length > 0 ? filteredChildren : undefined
})
}
return acc
}, [])
}
filteredDepartmentTree.value = filterTree(departmentTree.value)
}
// 加载用户列表数据 // 加载用户列表数据
const loadData = async () => { const loadData = async () => {
loading.value = true loading.value = true
@@ -240,9 +234,15 @@ const loadData = async () => {
} }
} }
// 表格变化处理(排序、筛选)
const handleTableChange = (pagination, filters, sorter) => {
// 如果需要处理排序或筛选,可以在这里添加逻辑
console.log('表格变化:', { pagination, filters, sorter })
}
// 部门选择事件 // 部门选择事件
const onDeptSelect = (selectedKeys) => { const onDeptSelect = (selectedKeys) => {
if (selectedKeys.length > 0) { if (selectedKeys && selectedKeys.length > 0) {
searchForm.department_id = selectedKeys[0] searchForm.department_id = selectedKeys[0]
} else { } else {
searchForm.department_id = null searchForm.department_id = null
@@ -263,17 +263,25 @@ const handleReset = () => {
searchForm.nickname = '' searchForm.nickname = ''
searchForm.department_id = null searchForm.department_id = null
selectedDeptKeys.value = [] selectedDeptKeys.value = []
departmentKeyword.value = ''
filteredDepartmentTree.value = departmentTree.value
pagination.current = 1 pagination.current = 1
loadData() loadData()
} }
// 表格变化事件 // 刷新表格
const handleTableChange = (pag) => { const refreshTable = () => {
pagination.current = pag.current
pagination.pageSize = pag.pageSize
loadData() loadData()
} }
// 导出数据
const handleExport = () => {
message.info('导出功能开发中...')
// TODO: 实现导出功能
// const params = { ...searchForm }
// 调用导出API
}
// 新增用户 // 新增用户
const handleAdd = () => { const handleAdd = () => {
dialog.save = true dialog.save = true
@@ -309,7 +317,7 @@ const handleRole = (record) => {
// 删除用户 // 删除用户
const handleDelete = async (record) => { const handleDelete = async (record) => {
try { try {
const res = await window.$API.auth.users.delete.post({ id: record.id }) const res = await authApi.users.delete.post({ id: record.id })
if (res.code === 1) { if (res.code === 1) {
message.success('删除成功') message.success('删除成功')
loadData() loadData()
@@ -358,12 +366,11 @@ onMounted(() => {
background: #fff; background: #fff;
.header { .header {
height: 50px; padding: 12px 16px;
line-height: 50px;
padding: 0 16px;
font-weight: 500; font-weight: 500;
border-bottom: 1px solid #f0f0f0; border-bottom: 1px solid #f0f0f0;
font-size: 14px; font-size: 14px;
background: #fafafa;
} }
.body { .body {
@@ -379,23 +386,6 @@ onMounted(() => {
flex-direction: column; flex-direction: column;
overflow: hidden; overflow: hidden;
.search-bar {
height: 50px;
padding: 12px 16px;
background: #fff;
border-bottom: 1px solid #f0f0f0;
display: flex;
align-items: center;
:deep(.ant-form) {
width: 100%;
}
:deep(.ant-form-item) {
margin-bottom: 0;
}
}
.table-content { .table-content {
flex: 1; flex: 1;
overflow: hidden; overflow: hidden;

View File

@@ -1,78 +1,130 @@
<template> <template>
<el-dialog title="角色设置" v-model="visible" :width="500" destroy-on-close @closed="$emit('closed')"> <a-modal title="角色设置" :open="visible" :width="500" :destroy-on-close="true" :footer="null" @cancel="handleCancel">
<el-tabs tab-position="top"> <a-tabs tab-position="top">
<el-tab-pane label="角色选择"> <a-tab-pane key="role" tab="角色选择">
<div class="treeMain"> <div class="treeMain">
<el-tree ref="role" node-key="id" :data="role.list" :default-checked-keys="role.checked" :props="role.props" check-strictly default-expand-all show-checkbox></el-tree> <a-tree ref="roleTreeRef" v-model:checkedKeys="role.checked" :tree-data="role.list"
</div> :field-names="role.fieldNames" :checkable="true" :default-expand-all="true"
</el-tab-pane> :check-strictly="true" :selectable="false">
</el-tabs> <template #title="{ title }">
<template #footer> {{ title }}
<el-button @click="visible=false" > </el-button>
<el-button type="primary" :loading="isSaveing" @click="submit()"> </el-button>
</template> </template>
</el-dialog> </a-tree>
</div>
</a-tab-pane>
</a-tabs>
<template #footer>
<a-button @click="handleCancel"> </a-button>
<a-button type="primary" :loading="isSaveing" @click="submit"> </a-button>
</template>
</a-modal>
</template> </template>
<script> <script setup>
export default { import { ref, reactive } from 'vue'
data(){ import { message } from 'ant-design-vue'
return { import authAPI from '@/api/auth'
visible: false,
isSaveing: false, const emit = defineEmits(['success', 'closed'])
role: {
const visible = ref(false)
const isSaveing = ref(false)
const roleTreeRef = ref()
const role = reactive({
list: [], list: [],
checked: [], checked: [],
props: { fieldNames: {
label: (data)=>{ title: 'title',
return data.title key: 'id',
children: 'children'
} }
}
},
form: {uid: '',roles: []}
}
},
mounted(){
this.getRole();
},
methods:{
open(){
this.visible = true;
return this;
},
async submit(){
this.isSaveing = true;
let roles = this.$refs.role.getCheckedNodes();
this.form.roles = [];
roles.map(item => {
this.form.roles.push(item.id);
}) })
this.isSaveing = true;
var res = await this.$API.auth.users.uprole.post(this.form);
this.isSaveing = false; const form = reactive({
if(res.code == 1){ uid: '',
this.$emit('success', this.form) roles: []
this.visible = false;
this.$message.success("操作成功")
}else{
this.$message.error(res.message)
}
},
async getRole(){
let res = await this.$API.auth.role.list.get({is_tree: 1});
this.role.list = res.data;
},
setData(data){
data.roles.map(item => {
this.role.checked.push(item.id)
}) })
this.form.uid = data.uid;
// 打开对话框
const open = () => {
visible.value = true
return {
open,
setData,
close
} }
} }
// 关闭对话框
const close = () => {
visible.value = false
} }
// 处理取消
const handleCancel = () => {
emit('closed')
visible.value = false
}
// 提交保存
const submit = async () => {
try {
isSaveing.value = true
// 获取选中的角色 ID
form.roles = role.checked || []
const res = await authAPI.users.uprole.post(form)
isSaveing.value = false
if (res.code === 1) {
emit('success', form)
visible.value = false
message.success('操作成功')
} else {
message.error(res.message)
}
} catch (error) {
console.error('保存角色失败:', error)
isSaveing.value = false
message.error('操作失败')
}
}
// 获取角色列表
const getRole = async () => {
try {
const res = await authAPI.role.list.get({ is_tree: 1 })
role.list = res.data || []
} catch (error) {
console.error('获取角色列表失败:', error)
message.error('获取角色列表失败')
}
}
// 设置数据
const setData = (data) => {
role.checked = data.roles ? data.roles.map(item => item.id) : []
form.uid = data.uid
}
// 组件挂载时加载数据
getRole()
// 暴露方法给父组件
defineExpose({
open,
setData,
close
})
</script> </script>
<style scoped> <style scoped>
.treeMain {height:280px;overflow: auto;border: 1px solid #dcdfe6;margin-bottom: 10px;} .treeMain {
height: 280px;
overflow: auto;
border: 1px solid #dcdfe6;
margin-bottom: 10px;
}
</style> </style>

View File

@@ -1,156 +1,194 @@
<template> <template>
<el-dialog :title="titleMap[mode]" v-model="visible" :width="500" destroy-on-close :close-on-click-modal="false" @closed="$emit('closed')"> <a-modal :title="titleMap[mode]" :open="visible" :width="500" :destroy-on-close="true" :mask-closable="false"
<el-form :model="form" :rules="rules" :disabled="mode=='show'" ref="dialogForm" label-width="100px" label-position="left"> :footer="null" @cancel="handleCancel">
<el-form-item label="头像" prop="avatar"> <a-form :model="form" :rules="rules" :disabled="mode === 'show'" ref="dialogForm" :label-col="{ span: 5 }"
:wrapper-col="{ span: 18 }">
<a-form-item label="头像" name="avatar">
<sc-upload v-model="form.avatar" :cropper="true" :aspectRatio="1" title="上传头像"></sc-upload> <sc-upload v-model="form.avatar" :cropper="true" :aspectRatio="1" title="上传头像"></sc-upload>
</el-form-item> </a-form-item>
<el-form-item label="登录账号" prop="username"> <a-form-item label="登录账号" name="username">
<el-input v-model="form.username" placeholder="用于登录系统" clearable></el-input> <a-input v-model:value="form.username" placeholder="用于登录系统" allow-clear />
</el-form-item> </a-form-item>
<el-form-item label="姓名" prop="nickname"> <a-form-item label="姓名" name="nickname">
<el-input v-model="form.nickname" placeholder="请输入完整的真实姓名" clearable></el-input> <a-input v-model:value="form.nickname" placeholder="请输入完整的真实姓名" allow-clear />
</el-form-item> </a-form-item>
<template v-if="mode=='add'"> <template v-if="mode === 'add'">
<el-form-item label="登录密码" prop="password"> <a-form-item label="登录密码" name="password">
<el-input type="password" v-model="form.password" clearable show-password></el-input> <a-input-password v-model:value="form.password" allow-clear />
</el-form-item> </a-form-item>
<el-form-item label="确认密码" prop="password2"> <a-form-item label="确认密码" name="password2">
<el-input type="password" v-model="form.password2" clearable show-password></el-input> <a-input-password v-model:value="form.password2" allow-clear />
</el-form-item> </a-form-item>
</template> </template>
<el-form-item label="所属部门" prop="group"> <a-form-item label="所属部门" name="department_id">
<el-tree-select v-model="form.department_id" :data="department" :props="departmentProps" placeholder="请选择部门" /> <a-tree-select v-model:value="form.department_id" :tree-data="department"
</el-form-item> :field-names="departmentFieldNames" :tree-default-expand-all="false" show-icon placeholder="请选择部门" allow-clear
<el-form-item label="所属角色" prop="roles"> tree-node-filter-prop="title" />
<el-tree-select v-model="form.roles" :data="groups" :props="groupsProps" multiple placeholder="请选择角色" /> </a-form-item>
</el-form-item> <a-form-item label="所属角色" name="roles">
</el-form> <a-tree-select v-model:value="form.roles" :tree-data="groups" :field-names="groupsFieldNames"
:tree-default-expand-all="false" :tree-checkable="true" placeholder="请选择角色" allow-clear
tree-node-filter-prop="title" multiple />
</a-form-item>
</a-form>
<template #footer> <template #footer>
<el-button @click="visible=false" > </el-button> <a-button @click="handleCancel"> </a-button>
<el-button v-if="mode!='show'" type="primary" :loading="isSaveing" @click="submit()"> </el-button> <a-button v-if="mode !== 'show'" type="primary" :loading="isSaveing" @click="submit"> </a-button>
</template> </template>
</el-dialog> </a-modal>
</template> </template>
<script> <script setup>
export default { import { ref, reactive } from 'vue'
emits: ['success', 'closed'], import { message } from 'ant-design-vue'
data() { import scUpload from '@/components/scUpload/index.vue'
return { import authApi from '@/api/auth'
mode: "add",
titleMap: { const emit = defineEmits(['success', 'closed'])
const mode = ref('add')
const titleMap = {
add: '新增用户', add: '新增用户',
edit: '编辑用户', edit: '编辑用户',
show: '查看' show: '查看'
},
visible: false,
isSaveing: false,
//表单数据
form: { username: "", avatar: "", department_id: 0, roles: []},
//验证规则
rules: {
// avatar:[
// {required: true, message: '请上传头像'}
// ],
username: [{required: true, message: '请输入登录账号'}],
nickname: [{required: true, message: '请输入真实姓名'}],
password: [
{required: true, message: '请输入登录密码'},
{validator: (rule, value, callback) => {
if (this.form.password2 !== '') {
this.$refs.dialogForm.validateField('password2');
} }
callback(); const visible = ref(false)
}} const isSaveing = ref(false)
// 表单数据
const form = reactive({
username: '',
avatar: '',
department_id: 0,
roles: []
})
// 表单引用
const dialogForm = ref()
// 验证规则
const rules = {
username: [{ required: true, message: '请输入登录账号', trigger: 'blur' }],
nickname: [{ required: true, message: '请输入真实姓名', trigger: 'blur' }],
password: [
{ required: true, message: '请输入登录密码', trigger: 'blur' },
{
validator: (rule, value) => {
if (form.password2 !== '') {
dialogForm.value?.validateFields('password2')
}
return Promise.resolve()
},
trigger: 'change'
}
], ],
password2: [ password2: [
{required: true, message: '请再次输入密码'}, { required: true, message: '请再次输入密码', trigger: 'blur' },
{validator: (rule, value, callback) => { {
if (value !== this.form.password) { validator: (rule, value) => {
callback(new Error('两次输入密码不一致!')); if (value !== form.password) {
}else{ return Promise.reject(new Error('两次输入密码不一致!'))
callback(); }
return Promise.resolve()
},
trigger: 'blur'
} }
}}
] ]
}, }
// 所需数据选项 // 所需数据选项
department: [], const department = ref([])
departmentProps: { const departmentFieldNames = {
value: "id", value: 'id',
label: "title", label: 'title',
multiple: false, children: 'children'
checkStrictly: true }
},
groups: [], const groups = ref([])
groupsProps: { const groupsFieldNames = {
value: "id", value: 'id',
label: "title", label: 'title',
multiple: false, children: 'children'
checkStrictly: true }
// 显示对话框
const open = (openMode = 'add') => {
mode.value = openMode
visible.value = true
return {
setData,
open,
close
} }
} }
},
mounted() { // 关闭对话框
this.getGroup() const close = () => {
this.getDepartment() visible.value = false
}, }
methods: {
//显示 // 处理取消
open(mode='add'){ const handleCancel = () => {
this.mode = mode; emit('closed')
this.visible = true; visible.value = false
return this }
},
// 加载树数据 // 加载树数据
async getGroup(){ const getGroup = async () => {
var res = await this.$API.auth.role.list.get({is_tree: 1}); const res = await authApi.role.list.get({ is_tree: 1 })
this.groups = res.data; groups.value = res.data
}, }
async getDepartment(){
var res = await this.$API.auth.department.list.get({is_tree: 1}); const getDepartment = async () => {
this.department = res.data; const res = await authApi.department.list.get({ is_tree: 1 })
}, department.value = res.data
}
// 表单提交方法 // 表单提交方法
submit(){ const submit = async () => {
this.$refs.dialogForm.validate(async (valid) => { try {
if (valid) { await dialogForm.value.validate()
this.isSaveing = true; isSaveing.value = true
var res = {}; let res = {}
if(this.mode == 'add'){ if (mode.value === 'add') {
res = await this.$API.auth.users.add.post(this.form); res = await authApi.users.add.post(form)
} else { } else {
res = await this.$API.auth.users.edit.post(this.form); res = await authApi.users.edit.post(form)
} }
this.isSaveing = false; isSaveing.value = false
if(res.code == 1){ if (res.code === 1) {
this.$emit('success', this.form, this.mode) emit('success', form, mode.value)
this.visible = false; visible.value = false
this.$message.success("操作成功") message.success('操作成功')
} else { } else {
this.$alert(res.message, "提示", {type: 'error'}) message.error(res.message)
} }
}else{ } catch (error) {
return false; console.error('表单验证失败', error)
} }
}) }
},
// 表单注入数据 // 表单注入数据
setData(data){ const setData = (data) => {
this.form.uid = data.uid form.uid = data.uid
this.form.username = data.username form.username = data.username
this.form.avatar = data.avatar form.avatar = data.avatar
this.form.nickname = data.nickname form.nickname = data.nickname
this.form.department_id = data.department_id form.department_id = data.department_id
form.roles = data.roles.map(item => item.id)
}
data.roles.map(item => { // 组件挂载时加载数据
this.form.roles.push(item.id) getGroup()
getDepartment()
// 暴露方法给父组件
defineExpose({
open,
setData,
close
}) })
}
}
}
</script> </script>
<style> <style></style>
</style>