feat: 实现四分组系统管理页面 (#68)

This commit is contained in:
ila
2026-08-26 22:24:01 +08:00
parent 3898aae32e
commit 2482ea31c5
12 changed files with 434 additions and 2533 deletions
-27
View File
@@ -17,30 +17,3 @@ export function getSysApi(id) {
})
}
// 新增SysApi
export function addSysApi(data) {
return request({
url: '/api/v1/sys-api',
method: 'post',
data: data
})
}
// 修改SysApi
export function updateSysApi(data) {
return request({
url: '/api/v1/sys-api/' + data.id,
method: 'put',
data: data
})
}
// 删除SysApi
export function delSysApi(data) {
return request({
url: '/api/v1/sys-api',
method: 'delete',
data: data
})
}
-17
View File
@@ -9,20 +9,3 @@ export function listSysLoginlog(query) {
})
}
// 查询SysLoginlog详细
export function getSysLoginlog(ID) {
return request({
url: '/api/v1/sys-login-log/' + ID,
method: 'get'
})
}
// 删除SysLoginlog
export function delSysLoginlog(data) {
return request({
url: '/api/v1/sys-login-log',
method: 'delete',
data: data
})
}
-27
View File
@@ -32,30 +32,3 @@ export function roleMenuTreeselect(roleId) {
method: 'get'
})
}
// 新增菜单
export function addMenu(data) {
return request({
url: '/api/v1/menu',
method: 'post',
data: data
})
}
// 修改菜单
export function updateMenu(data, id) {
return request({
url: '/api/v1/menu/' + id,
method: 'put',
data: data
})
}
// 删除菜单
export function delMenu(data) {
return request({
url: '/api/v1/menu',
method: 'delete',
data: data
})
}
+1
View File
@@ -7,6 +7,7 @@ const getters = {
token: state => state.user.token,
avatar: state => state.user.avatar,
name: state => state.user.name,
userId: state => state.user.userId,
introduction: state => state.user.introduction,
roles: state => state.user.roles,
permisaction: state => state.user.permisaction,
+8 -1
View File
@@ -6,6 +6,7 @@ import storage from '@/utils/storage'
const state = {
token: getToken(),
name: '',
userId: undefined,
avatar: '',
introduction: '',
roles: [],
@@ -23,6 +24,9 @@ const mutations = {
SET_NAME: (state, name) => {
state.name = name
},
SET_USER_ID: (state, userId) => {
state.userId = userId
},
SET_AVATAR: (state, avatar) => {
if (avatar.indexOf('http') !== -1) {
state.avatar = avatar
@@ -63,7 +67,7 @@ const actions = {
return resolve()
}
const { roles, name, avatar, introduction, permissions } = response.data
const { roles, name, avatar, introduction, permissions, userId } = response.data
// roles must be a non-empty array
if (!roles || roles.length <= 0) {
@@ -72,6 +76,7 @@ const actions = {
commit('SET_PERMISSIONS', permissions)
commit('SET_ROLES', roles)
commit('SET_NAME', name)
commit('SET_USER_ID', userId)
commit('SET_AVATAR', avatar)
commit('SET_INTRODUCTION', introduction)
resolve(response)
@@ -85,6 +90,7 @@ const actions = {
return new Promise((resolve, reject) => {
logout(state.token).then(() => {
commit('SET_TOKEN', '')
commit('SET_USER_ID', undefined)
commit('SET_ROLES', [])
commit('SET_PERMISSIONS', [])
removeToken()
@@ -113,6 +119,7 @@ const actions = {
resetToken({ commit }) {
return new Promise(resolve => {
commit('SET_TOKEN', '')
commit('SET_USER_ID', undefined)
removeToken()
resolve()
})
+42 -406
View File
@@ -1,426 +1,62 @@
<template>
<BasicLayout>
<template #wrapper>
<el-card class="box-card">
<el-card class="box-card system-page">
<div class="page-heading"><div><h2>接口清单</h2><p>用于核对管理端授权范围,不在此处修改接口定义。</p></div><el-tag type="info" effect="plain">只读</el-tag></div>
<el-form ref="queryForm" :model="queryParams" :inline="true" class="search-form">
<el-form-item label="标题" prop="title">
<el-input
v-model="queryParams.title"
placeholder="请输入标题"
clearable
size="small"
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item label="地址" prop="path">
<el-input
v-model="queryParams.path"
placeholder="请输入地址"
clearable
size="small"
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item label="Method" prop="action">
<el-select
v-model="queryParams.action"
placeholder="请选择Method"
clearable
size="small"
>
<el-option value="GET">GET</el-option>
<el-option value="POST">POST</el-option>
<el-option value="PUT">PUT</el-option>
<el-option value="DELETE">DELETE</el-option>
</el-select>
</el-form-item>
<el-form-item label="类型" prop="type">
<el-select
v-model="queryParams.type"
placeholder="请选择类型"
clearable
size="small"
>
<el-option value="SYS">SYS</el-option>
<el-option value="BUS">BUS</el-option>
<el-option value="暂无">暂无</el-option>
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" size="small" :icon="Search" @click="handleQuery">搜索</el-button>
<el-button size="small" :icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
<el-form-item label="标题" prop="title"><el-input v-model="queryParams.title" clearable placeholder="输入接口标题" @keyup.enter="handleQuery" /></el-form-item>
<el-form-item label="路径" prop="path"><el-input v-model="queryParams.path" clearable placeholder="输入接口路径" @keyup.enter="handleQuery" /></el-form-item>
<el-form-item label="方法" prop="action"><el-select v-model="queryParams.action" clearable placeholder="全部方法" style="width: 130px"><el-option v-for="method in methods" :key="method" :label="method" :value="method" /></el-select></el-form-item>
<el-form-item><el-button type="primary" :icon="Search" @click="handleQuery">搜索</el-button><el-button :icon="Refresh" @click="resetQuery">重置</el-button></el-form-item>
</el-form>
<el-table
v-loading="loading"
:data="sysapiList"
border
stripe
@selection-change="handleSelectionChange"
@sort-change="handleSortChang"
>
<el-table-column
label="标题"
header-align="center"
align="left"
prop="title"
fixed="left"
sortable="custom"
width="260px"
:show-overflow-tooltip="true"
>
<template #default="scope">
<span v-if="scope.row.type=='SYS' && scope.row.title!=''"><el-tag type="success">{{ '['+scope.row.type +'] '+ scope.row.title }}</el-tag></span>
<span v-if="scope.row.type!='SYS' && scope.row.title!=''"><el-tag type="">{{ '['+scope.row.type +'] '+scope.row.title }}</el-tag></span>
<span v-if="scope.row.title==''"><el-tag type="danger">暂无</el-tag></span>
</template>
</el-table-column>
<el-table-column
label="Request Info"
header-align="center"
align="left"
prop="path"
sortable="custom"
:show-overflow-tooltip="true"
>
<!-- <template v-slot="scope">
<span>{{ "["+scope.row.action +"] "+ scope.row.path }}</span>
</template> -->
<template #default="scope">
<el-popover trigger="hover" placement="top">
<p><span v-if="scope.row.type=='SYS' && scope.row.title!=''"><el-tag type="success">{{ '['+scope.row.type +'] '+ scope.row.title }}</el-tag></span>
<span v-if="scope.row.type!='SYS' && scope.row.title!=''"><el-tag type="">{{ '['+scope.row.type +'] '+scope.row.title }}</el-tag></span>
<span v-if="scope.row.title==''"><el-tag type="danger">暂无</el-tag></span>
</p>
<p>Handle: {{ scope.row.handle }}</p>
<p>Method:
<el-tag v-if="scope.row.action=='GET'">{{ scope.row.action }}</el-tag>
<el-tag v-if="scope.row.action=='POST'" type="success">{{ scope.row.action }}</el-tag>
<el-tag v-if="scope.row.action=='PUT'" type="warning">{{ scope.row.action }}</el-tag>
<el-tag v-if="scope.row.action=='DELETE'" type="danger">{{ scope.row.action }}</el-tag>
</p>
<p>接口类型: {{ scope.row.type }}</p>
<template #reference><div class="name-wrapper">
<el-tag v-if="scope.row.action=='GET'">{{ scope.row.action }}</el-tag>
<el-tag v-if="scope.row.action=='POST'" type="success">{{ scope.row.action }}</el-tag>
<el-tag v-if="scope.row.action=='PUT'" type="warning">{{ scope.row.action }}</el-tag>
<el-tag v-if="scope.row.action=='DELETE'" type="danger">{{ scope.row.action }}</el-tag>
{{ scope.row.path }}
</div></template>
</el-popover>
</template>
</el-table-column>
<el-table-column
label="创建时间"
align="center"
prop="createdAt"
width="155px"
sortable="custom"
>
<template #default="scope">
<span>{{ parseTime(scope.row.createdAt) }}</span>
</template>
</el-table-column>
<el-table-column
label="操作"
align="center"
width="80px"
class-name="small-padding fixed-width"
>
<template #default="scope">
<el-button v-permisaction="['admin:sysApi:edit']" type="primary" link size="small" :icon="Edit" @click="handleUpdate(scope.row)">修改</el-button>
</template>
</el-table-column>
<el-alert v-if="errorMessage" :title="errorMessage" type="error" show-icon :closable="false"><template #default><el-button link type="primary" @click="getList">重新加载</el-button></template></el-alert>
<el-table v-else v-loading="loading" :data="apiList" border stripe empty-text="暂无接口数据">
<el-table-column prop="title" label="接口名称" min-width="180" show-overflow-tooltip />
<el-table-column prop="handle" label="处理标识" min-width="190" show-overflow-tooltip />
<el-table-column label="方法" width="90" align="center"><template #default="scope"><el-tag :type="methodTag(scope.row.action)" effect="plain">{{ scope.row.action }}</el-tag></template></el-table-column>
<el-table-column prop="path" label="接口路径" min-width="260" show-overflow-tooltip />
<el-table-column prop="type" label="类型" width="90" align="center" />
<el-table-column prop="createdAt" label="创建时间" width="170"><template #default="scope">{{ parseTime(scope.row.createdAt) }}</template></el-table-column>
</el-table>
<pagination
v-show="total>0"
v-model:current-page="queryParams.pageIndex"
v-model:page-size="queryParams.pageSize"
:total="total"
@pagination="getList"
/>
<!-- 添加或修改对话框 -->
<el-drawer
ref="drawer"
v-model="open"
:title="title"
:before-close="cancel"
direction="rtl"
custom-class="demo-drawer"
>
<div class="demo-drawer__content">
<el-form ref="form" :model="form" :rules="rules" label-width="80px">
<el-form-item label="Handle" prop="handle">
<el-input
v-model="form.handle"
placeholder="handle"
/>
</el-form-item>
<el-form-item label="标题" prop="title">
<el-input
v-model="form.title"
placeholder="标题"
/>
</el-form-item>
<el-form-item label="类型" prop="type">
<el-select
v-model="form.type"
placeholder="请选择类型"
clearable
size="small"
@keyup.enter="handleQuery"
>
<el-option value="SYS">SYS</el-option>
<el-option value="BUS">BUS</el-option>
</el-select>
</el-form-item>
<el-form-item label="Method" prop="action">
<el-select
v-model="form.action"
placeholder="请选择方式"
clearable
size="small"
@keyup.enter="handleQuery"
>
<el-option value="GET">GET</el-option>
<el-option value="POST">POST</el-option>
<el-option value="PUT">PUT</el-option>
<el-option value="DELETE">DELETE</el-option>
</el-select>
</el-form-item>
<el-form-item label="地址" prop="path">
<el-input
v-model="form.path"
:disabled="isEdit"
placeholder="地址"
/>
</el-form-item>
</el-form>
<div class="demo-drawer__footer">
<el-button type="primary" @click="submitForm">确 定</el-button>
<el-button @click="cancel">取 消</el-button>
</div>
</div>
</el-drawer>
<pagination v-show="total > queryParams.pageSize" v-model:current-page="queryParams.pageIndex" v-model:page-size="queryParams.pageSize" :total="total" @pagination="getList" />
</el-card>
</template>
</BasicLayout>
</template>
<script>
import { addSysApi, delSysApi, getSysApi, listSysApi, updateSysApi } from '@/api/admin/sys-api'
import { Search, Refresh, Edit } from '@element-plus/icons-vue'
import { listSysApi } from '@/api/admin/sys-api'
import { Refresh, Search } from '@element-plus/icons-vue'
export default {
name: 'SysApiManage',
components: {
},
setup() {
return { Search, Refresh, Edit }
},
data() {
return {
dialog: false,
// 遮罩层
loading: true,
// 选中数组
ids: [],
// 非单个禁用
single: true,
// 非多个禁用
multiple: true,
// 总条数
total: 0,
// 弹出层标题
title: '',
// 是否显示弹出层
open: false,
isEdit: false,
// 类型数据字典
typeOptions: [],
sysapiList: [],
dateRange: [],
// 查询参数
queryParams: {
pageIndex: 1,
pageSize: 10,
name: undefined,
title: undefined,
path: undefined,
type: undefined,
action: undefined,
parentId: undefined
},
// 表单参数
form: {
},
// 表单校验
rules: {
title: [{ required: true, message: '标题不能为空', trigger: 'blur' }],
path: [{ required: true, message: '地址不能为空', trigger: 'blur' }],
action: [{ required: true, message: '类型不能为空', trigger: 'blur' }],
parentId: [{ required: true, message: '按钮id不能为空', trigger: 'blur' }]
}
}
},
created() {
this.getList()
},
name: 'SysApiReadOnly',
setup() { return { Refresh, Search } },
data() { return { loading: false, errorMessage: '', apiList: [], total: 0, methods: ['GET', 'POST', 'PUT', 'DELETE'], queryParams: { pageIndex: 1, pageSize: 10, title: undefined, path: undefined, action: undefined }} },
created() { this.getList() },
methods: {
handleClose(done) {
if (this.loading) {
return
}
done()
},
/** 查询参数列表 */
getList() {
async getList() {
this.loading = true
listSysApi(this.queryParams).then(response => {
this.sysapiList = response.data.list
this.total = response.data.count
this.loading = false
}
)
this.errorMessage = ''
try {
const response = await listSysApi(this.queryParams)
this.apiList = response.data?.list || []
this.total = response.data?.count || 0
} catch (error) {
this.apiList = []
this.total = 0
this.errorMessage = error?.response?.status === 403 ? '没有查看接口清单的权限' : '接口清单加载失败'
} finally { this.loading = false }
},
// 取消按钮
cancel() {
this.open = false
this.reset()
},
// 表单重置
reset() {
this.form = {
id: undefined,
name: undefined,
title: undefined,
path: undefined,
paths: undefined,
action: undefined,
parentId: undefined,
sort: undefined
}
this.resetForm('form')
},
parentIdFormat(row) {
return this.selectItemsLabel(this.parentIdOptions, row.parentId)
},
// 文件
/** 搜索按钮操作 */
handleQuery() {
this.queryParams.pageIndex = 1
this.getList()
},
/** 重置按钮操作 */
resetQuery() {
this.dateRange = []
this.resetForm('queryForm')
this.handleQuery()
},
/** 新增按钮操作 */
handleAdd() {
this.reset()
this.open = true
this.title = '添加接口管理'
this.isEdit = false
},
/** 排序回调函数 */
handleSortChang(column, prop, order) {
prop = column.prop
order = column.order
if (this.order !== '' && this.order !== prop + 'Order') {
this.queryParams[this.order] = undefined
}
if (order === 'descending') {
this.queryParams[prop + 'Order'] = 'desc'
this.order = prop + 'Order'
} else if (order === 'ascending') {
this.queryParams[prop + 'Order'] = 'asc'
this.order = prop + 'Order'
} else {
this.queryParams[prop + 'Order'] = undefined
}
this.getList()
},
// 多选框选中数据
handleSelectionChange(selection) {
this.ids = selection.map(item => item.id)
this.single = selection.length !== 1
this.multiple = !selection.length
},
/** 修改按钮操作 */
handleUpdate(row) {
this.reset()
const id =
row.id || this.ids
getSysApi(id).then(response => {
this.form = response.data
this.open = true
this.title = '修改接口管理'
this.isEdit = true
})
},
/** 提交按钮 */
submitForm: function() {
this.$refs['form'].validate(valid => {
if (valid) {
if (this.form.id !== undefined) {
updateSysApi(this.form).then(response => {
if (response.code === 200) {
this.msgSuccess(response.msg)
this.open = false
this.getList()
} else {
this.msgError(response.msg)
}
})
} else {
addSysApi(this.form).then(response => {
if (response.code === 200) {
this.msgSuccess(response.msg)
this.open = false
this.getList()
} else {
this.msgError(response.msg)
}
})
}
}
})
},
/** 删除按钮操作 */
handleDelete(row) {
var Ids = (row.id && [row.id]) || this.ids
this.$confirm('是否确认删除编号为"' + Ids + '"的数据项?', '警告', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(function() {
return delSysApi({ 'ids': Ids })
}).then((response) => {
if (response.code === 200) {
this.msgSuccess(response.msg)
this.open = false
this.getList()
} else {
this.msgError(response.msg)
}
}).catch(function() {})
}
methodTag(method) { return { POST: 'success', PUT: 'warning', DELETE: 'danger' }[method] || 'primary' },
handleQuery() { this.queryParams.pageIndex = 1; this.getList() },
resetQuery() { this.$refs.queryForm?.resetFields(); this.handleQuery() }
}
}
</script>
<style scoped>
.system-page { min-height: 360px; }
.page-heading { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; margin-bottom: 20px; }
.page-heading h2 { margin: 0 0 6px; font-size: 20px; letter-spacing: 0; }
.page-heading p { margin: 0; color: var(--el-text-color-secondary); line-height: 1.5; }
</style>
+42 -267
View File
@@ -1,287 +1,62 @@
<template>
<BasicLayout>
<template #wrapper>
<el-card class="box-card">
<el-card class="box-card system-page">
<div class="page-heading"><div><h2>登录日志</h2><p>保留管理员登录结果,日志不可在管理端删除。</p></div><el-tag type="info" effect="plain">只读</el-tag></div>
<el-form ref="queryForm" :model="queryParams" :inline="true" class="search-form">
<el-form-item label="用户名" prop="username">
<el-input
v-model="queryParams.username"
placeholder="请输入用户名"
clearable
size="small"
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select
v-model="queryParams.status"
placeholder="系统登录日志状态"
clearable
size="small"
>
<el-option
v-for="dict in statusOptions"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item label="ip地址" prop="ipaddr">
<el-input
v-model="queryParams.ipaddr"
placeholder="请输入ip地址"
clearable
size="small"
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" size="small" :icon="Search" @click="handleQuery">搜索</el-button>
<el-button size="small" :icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
<el-form-item label="账号" prop="username"><el-input v-model="queryParams.username" clearable placeholder="输入管理员账号" @keyup.enter="handleQuery" /></el-form-item>
<el-form-item label="状态" prop="status"><el-select v-model="queryParams.status" clearable placeholder="全部状态" style="width: 130px"><el-option label="成功" value="2" /><el-option label="失败" value="1" /></el-select></el-form-item>
<el-form-item><el-button type="primary" :icon="Search" @click="handleQuery">搜索</el-button><el-button :icon="Refresh" @click="resetQuery">重置</el-button></el-form-item>
</el-form>
<div class="toolbar mb8">
<el-button v-permisaction="['admin:sysLoginLog:remove']" type="danger" size="small" :icon="Delete" :disabled="multiple" @click="handleDelete">删除</el-button>
</div>
<el-table v-loading="loading" :data="sysloginlogList" border stripe @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column
label="用户名"
align="center"
prop="username"
:show-overflow-tooltip="true"
/>
<el-table-column
label="类型"
align="center"
prop="msg"
:show-overflow-tooltip="true"
/>
<el-table-column
label="状态"
align="center"
prop="status"
width="100"
>
<template #default="scope">
<el-tag :type="scope.row.status === '2' || scope.row.status === 2 ? 'success' : 'danger'" size="small" disable-transitions>
{{ statusFormat(scope.row) }}
</el-tag>
</template>
</el-table-column>
<el-table-column
label="ip地址"
align="center"
prop="ipaddr"
>
<template #default="scope">
<el-popover trigger="hover" placement="top">
<p>IP: {{ scope.row.ipaddr }}</p>
<p>归属地: {{ scope.row.loginLocation }}</p>
<p>浏览器: {{ scope.row.browser }}</p>
<p>系统: {{ scope.row.os }}</p>
<p>固件: {{ scope.row.platform }}</p>
<template #reference><div class="name-wrapper">
{{ scope.row.ipaddr }}
</div></template>
</el-popover>
</template>
</el-table-column>
<el-table-column
label="登录时间"
align="center"
prop="loginTime"
width="180"
>
<template #default="scope">
<span>{{ parseTime(scope.row.loginTime) }}</span>
</template>
</el-table-column>
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<template #default="scope">
<el-button v-permisaction="['admin:sysLoginLog:remove']" type="danger" link size="small" :icon="Delete" @click="handleDelete(scope.row)">删除</el-button>
</template>
</el-table-column>
<el-alert v-if="errorMessage" :title="errorMessage" type="error" show-icon :closable="false"><template #default><el-button link type="primary" @click="getList">重新加载</el-button></template></el-alert>
<el-table v-else v-loading="loading" :data="logList" border stripe empty-text="暂无登录日志">
<el-table-column prop="username" label="管理员账号" min-width="140" show-overflow-tooltip />
<el-table-column label="结果" width="90" align="center"><template #default="scope"><el-tag :type="isSuccess(scope.row.status) ? 'success' : 'danger'">{{ isSuccess(scope.row.status) ? '成功' : '失败' }}</el-tag></template></el-table-column>
<el-table-column prop="msg" label="说明" min-width="180" show-overflow-tooltip />
<el-table-column prop="ipaddr" label="来源 IP" min-width="140" show-overflow-tooltip />
<el-table-column prop="browser" label="浏览器" min-width="130" show-overflow-tooltip />
<el-table-column prop="os" label="操作系统" min-width="130" show-overflow-tooltip />
<el-table-column prop="loginTime" label="登录时间" width="180"><template #default="scope">{{ parseTime(scope.row.loginTime) }}</template></el-table-column>
</el-table>
<pagination
v-show="total>0"
v-model:current-page="queryParams.pageIndex"
v-model:page-size="queryParams.pageSize"
:total="total"
@pagination="getList"
/>
<pagination v-show="total > queryParams.pageSize" v-model:current-page="queryParams.pageIndex" v-model:page-size="queryParams.pageSize" :total="total" @pagination="getList" />
</el-card>
</template>
</BasicLayout>
</template>
<script>
import { delSysLoginlog, getSysLoginlog, listSysLoginlog } from '@/api/admin/sys-login-log'
import { Search, Refresh, Delete } from '@element-plus/icons-vue'
import { listSysLoginlog } from '@/api/admin/sys-login-log'
import { Refresh, Search } from '@element-plus/icons-vue'
export default {
name: 'SysLoginLogManage',
components: {
},
setup() {
return { Search, Refresh, Delete }
},
data() {
return {
// 遮罩层
loading: true,
// 选中数组
ids: [],
// 非单个禁用
single: true,
// 非多个禁用
multiple: true,
// 总条数
total: 0,
// 弹出层标题
title: '',
// 是否显示弹出层
open: false,
isEdit: false,
fileOpen: false,
fileIndex: undefined,
// 类型数据字典
typeOptions: [],
sysloginlogList: [],
statusOptions: [],
// 关系表类型
// 查询参数
queryParams: {
pageIndex: 1,
pageSize: 10,
username: undefined,
status: undefined,
ipaddr: undefined,
loginLocation: undefined,
createdAtOrder: 'desc'
},
// 表单参数
form: {
},
// 表单校验
rules: {
}
}
},
created() {
this.getList()
this.getDicts('sys_common_status').then(response => {
this.statusOptions = response.data
})
},
name: 'SysLoginLogReadOnly',
setup() { return { Refresh, Search } },
data() { return { loading: false, errorMessage: '', logList: [], total: 0, queryParams: { pageIndex: 1, pageSize: 10, username: undefined, status: undefined, createdAtOrder: 'desc' }} },
created() { this.getList() },
methods: {
/** 查询参数列表 */
getList() {
async getList() {
this.loading = true
listSysLoginlog(this.addDateRange(this.queryParams, this.dateRange)).then(response => {
this.sysloginlogList = response.data.list
this.total = response.data.count
this.loading = false
}
)
this.errorMessage = ''
try {
const response = await listSysLoginlog(this.queryParams)
this.logList = response.data?.list || []
this.total = response.data?.count || 0
} catch (error) {
this.logList = []
this.total = 0
this.errorMessage = error?.response?.status === 403 ? '没有查看登录日志的权限' : '登录日志加载失败'
} finally { this.loading = false }
},
// 取消按钮
cancel() {
this.open = false
this.reset()
},
// 表单重置
reset() {
this.form = {
ID: undefined,
username: undefined,
status: undefined,
ipaddr: undefined,
loginLocation: undefined,
browser: undefined,
os: undefined,
platform: undefined,
loginTime: undefined,
remark: undefined,
msg: undefined
}
this.resetForm('form')
},
getImgList: function() {
this.form[this.fileIndex] = this.$refs['fileChoose'].resultList[0].fullUrl
},
fileClose: function() {
this.fileOpen = false
},
statusFormat(row) {
return this.selectDictLabel(this.statusOptions, row.status)
},
// 关系
// 文件
/** 搜索按钮操作 */
handleQuery() {
this.queryParams.pageIndex = 1
this.getList()
},
/** 重置按钮操作 */
resetQuery() {
this.dateRange = []
this.resetForm('queryForm')
this.handleQuery()
},
/** 新增按钮操作 */
handleAdd() {
this.reset()
this.open = true
this.title = '添加系统登录日志'
this.isEdit = false
},
// 多选框选中数据
handleSelectionChange(selection) {
this.ids = selection.map(item => item.id)
this.single = selection.length !== 1
this.multiple = !selection.length
},
/** 修改按钮操作 */
handleUpdate(row) {
this.reset()
const ID =
row.id || this.ids
getSysLoginlog(ID).then(response => {
this.form = response.data
this.open = true
this.title = '修改系统登录日志'
this.isEdit = true
})
},
/** 删除按钮操作 */
handleDelete(row) {
var Ids = (row.id && [row.id]) || this.ids
this.$confirm('是否确认删除编号为"' + Ids + '"的数据项?', '警告', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(function() {
return delSysLoginlog({ 'ids': Ids })
}).then((response) => {
if (response.code === 200) {
this.msgSuccess(response.msg)
this.open = false
this.getList()
} else {
this.msgError(response.msg)
}
}).catch(function() {})
}
isSuccess(status) { return status === '2' || status === 2 },
handleQuery() { this.queryParams.pageIndex = 1; this.getList() },
resetQuery() { this.$refs.queryForm?.resetFields(); this.handleQuery() }
}
}
</script>
<style scoped>
.system-page { min-height: 360px; }
.page-heading { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; margin-bottom: 20px; }
.page-heading h2 { margin: 0 0 6px; font-size: 20px; letter-spacing: 0; }
.page-heading p { margin: 0; color: var(--el-text-color-secondary); line-height: 1.5; }
</style>
+40 -613
View File
@@ -1,634 +1,61 @@
<template>
<BasicLayout>
<template #wrapper>
<el-card class="box-card">
<el-form :inline="true" class="search-form">
<el-form-item label="菜单名称">
<el-input
v-model="queryParams.title"
placeholder="请输入菜单名称"
clearable
size="small"
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item label="状态">
<el-select v-model="queryParams.visible" placeholder="菜单状态" clearable size="small">
<el-option
v-for="dict in visibleOptions"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" size="small" :icon="Search" @click="handleQuery">搜索</el-button>
<el-button v-permisaction="['admin:sysMenu:add']" type="primary" size="small" :icon="Plus" @click="handleAdd">新增</el-button>
</el-form-item>
<el-card class="box-card system-page">
<div class="page-heading">
<div><h2>菜单结构</h2><p>当前导航由系统配置生成,此页面仅供核对。</p></div>
<el-tag type="info" effect="plain">只读</el-tag>
</div>
<el-form ref="queryForm" :model="queryParams" :inline="true" class="search-form">
<el-form-item label="菜单名称" prop="title"><el-input v-model="queryParams.title" clearable placeholder="输入菜单名称" @keyup.enter="handleQuery" /></el-form-item>
<el-form-item><el-button type="primary" :icon="Search" @click="handleQuery">搜索</el-button><el-button :icon="Refresh" @click="resetQuery">重置</el-button></el-form-item>
</el-form>
<el-table
v-loading="loading"
:data="menuList"
border
stripe
row-key="menuId"
:tree-props="{children: 'children', hasChildren: 'hasChildren'}"
>
<el-table-column prop="title" label="菜单名称" :show-overflow-tooltip="true" width="180px" />
<el-table-column prop="icon" label="图标" align="center" width="100px">
<template #default="scope">
<svg-icon :icon-class="scope.row.icon" />
</template>
</el-table-column>
<el-table-column prop="sort" label="排序" width="60px" />
<el-table-column prop="permission" label="权限标识" :show-overflow-tooltip="true">
<template #default="scope">
<el-popover v-if="scope.row.sysApi.length>0" trigger="hover" placement="top">
<el-table
:data="scope.row.sysApi"
border
style="width: 100%"
>
<el-table-column
prop="title"
label="title"
width="260px"
>
<template #default="scope">
<span v-if="scope.row.type=='SYS' && scope.row.title!=''"><el-tag type="success">{{ '['+scope.row.type +'] '+ scope.row.title }}</el-tag></span>
<span v-if="scope.row.type!='SYS' && scope.row.title!=''"><el-tag type="">{{ '['+scope.row.type +'] '+scope.row.title }}</el-tag></span>
<span v-if="scope.row.title==''"><el-tag type="danger">暂无</el-tag></span>
</template>
</el-table-column>
<el-table-column
prop="path"
label="path"
width="270px"
>
<template #default="scope">
<el-tag v-if="scope.row.action=='GET'">{{ scope.row.action }}</el-tag>
<el-tag v-if="scope.row.action=='POST'" type="success">{{ scope.row.action }}</el-tag>
<el-tag v-if="scope.row.action=='PUT'" type="warning">{{ scope.row.action }}</el-tag>
<el-tag v-if="scope.row.action=='DELETE'" type="danger">{{ scope.row.action }}</el-tag>
{{ scope.row.path }}
</template>
</el-table-column>
</el-table>
<template #reference><div class="name-wrapper">
<span v-if="scope.row.permission==''">-</span>
<span v-else>{{ scope.row.permission }}</span>
</div></template>
</el-popover>
<span v-else>
<span v-if="scope.row.permission==''">-</span>
<span v-else>{{ scope.row.permission }}</span>
</span>
</template>
</el-table-column>
<el-table-column prop="path" label="组件路径" :show-overflow-tooltip="true">
<template #default="scope">
<span v-if="scope.row.menuType=='A'">{{ scope.row.path }}</span>
<span v-else>{{ scope.row.component }}</span>
</template>
</el-table-column>
<el-table-column prop="visible" label="可见" :formatter="visibleFormat" width="80">
<template #default="scope">
<el-tag
:type="scope.row.visible === '1' ? 'danger' : 'success'"
disable-transitions
>{{ visibleFormat(scope.row) }}</el-tag>
</template>
</el-table-column>
<el-table-column label="创建时间" align="center" prop="createdAt" width="180">
<template #default="scope">
<span>{{ parseTime(scope.row.createdAt) }}</span>
</template>
</el-table-column>
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="220">
<template #default="scope">
<el-button v-permisaction="['admin:sysMenu:edit']" type="primary" link size="small" :icon="Edit" @click="handleUpdate(scope.row)">修改</el-button>
<el-divider direction="vertical" />
<el-button v-permisaction="['admin:sysMenu:add']" type="primary" link size="small" :icon="Plus" @click="handleAdd(scope.row)">新增</el-button>
<el-divider direction="vertical" />
<el-button v-permisaction="['admin:sysMenu:remove']" type="danger" link size="small" :icon="Delete" @click="handleDelete(scope.row)">删除</el-button>
</template>
</el-table-column>
<el-alert v-if="errorMessage" :title="errorMessage" type="error" show-icon :closable="false">
<template #default><el-button link type="primary" @click="getList">重新加载</el-button></template>
</el-alert>
<el-table v-else v-loading="loading" :data="menuList" row-key="menuId" border stripe default-expand-all :tree-props="{ children: 'children' }" empty-text="暂无菜单数据">
<el-table-column prop="title" label="菜单名称" min-width="180" />
<el-table-column prop="path" label="访问路径" min-width="210" show-overflow-tooltip />
<el-table-column prop="component" label="页面组件" min-width="220" show-overflow-tooltip />
<el-table-column prop="permission" label="权限标识" min-width="190" show-overflow-tooltip />
<el-table-column prop="sort" label="排序" width="80" align="center" />
<el-table-column label="可见" width="80" align="center"><template #default="scope">{{ scope.row.visible === '0' ? '是' : '否' }}</template></el-table-column>
</el-table>
<!-- 添加或修改菜单对话框 -->
<el-drawer
ref="drawer"
v-model="open"
:title="title"
:before-close="cancel"
direction="rtl"
custom-class="demo-drawer"
size="830px"
>
<div class="demo-drawer__content">
<el-form ref="form" :model="form" :rules="rules" label-position="top" label-width="106px">
<el-row>
<el-col :span="24">
<el-form-item prop="parentId">
<template #label>
<span>
上级菜单
<el-tooltip content="指当前菜单停靠的菜单归属" placement="top">
<i class="ri-question-line" />
</el-tooltip>
</span>
</template>
<treeselect
v-model="form.parentId"
:options="menuOptions"
:normalizer="normalizer"
:show-count="true"
placeholder="选择上级菜单"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item prop="title">
<template #label>
<span>
菜单标题
<el-tooltip content="菜单位置显示的说明信息" placement="top">
<i class="ri-question-line" />
</el-tooltip>
</span>
</template>
<el-input v-model="form.title" placeholder="请输入菜单标题" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item prop="sort">
<template #label>
<span>
显示排序
<el-tooltip content="根据序号升序排列" placement="top">
<i class="ri-question-line" />
</el-tooltip>
</span>
</template>
<el-input-number v-model="form.sort" controls-position="right" :min="0" />
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item prop="menuType">
<template #label>
<span>
菜单类型
<el-tooltip content="包含目录:以及菜单或者菜单组,菜单:具体对应某一个页面,按钮:功能才做按钮;" placement="top">
<i class="ri-question-line" />
</el-tooltip>
</span>
</template>
<el-radio-group v-model="form.menuType">
<el-radio label="M">目录</el-radio>
<el-radio label="C">菜单</el-radio>
<el-radio label="F">按钮</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="菜单图标">
<el-popover
placement="bottom-start"
width="460"
trigger="click"
@show="$refs['iconSelect'].reset()"
>
<IconSelect ref="iconSelect" @selected="selected" />
<template #reference>
<el-input v-model="form.icon" placeholder="点击选择图标" readonly>
<template #prefix>
<svg-icon
v-if="form.icon"
:icon-class="form.icon"
class="el-input__icon"
style="height: 32px;width: 16px;"
/>
<i v-else class="ri-search-line el-input__icon" />
</template>
</el-input>
</template>
</el-popover>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item v-if="form.menuType == 'M' || form.menuType == 'C'" prop="menuName">
<template #label>
<span>
路由名称
<el-tooltip content="需要和页面name保持一致,对应页面即可选择缓存" placement="top">
<i class="ri-question-line" />
</el-tooltip>
</span>
</template>
<el-input v-model="form.menuName" placeholder="请输入路由名称" />
</el-form-item>
</el-col>
<el-col v-if="form.menuType == 'M' || form.menuType == 'C'" :span="12">
<el-form-item prop="component">
<template #label>
<span>
组件路径
<el-tooltip content="菜单对应的具体vue页面文件路径views的下级路径/admin/sys-api/index;目录类型:填写Layout,如何有二级目录请参照日志目录填写;" placement="top">
<i class="ri-question-line" />
</el-tooltip>
</span>
</template>
<el-input v-model="form.component" placeholder="请输入组件路径" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item v-if="form.menuType == 'M' || form.menuType == 'C'">
<template #label>
<span>
是否外链
<el-tooltip content="可以通过iframe打开指定地址" placement="top">
<i class="ri-question-line" />
</el-tooltip>
</span>
</template>
<el-radio-group v-model="form.isFrame">
<el-radio label="0">是</el-radio>
<el-radio label="1">否</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item v-if="form.menuType != 'F'" prop="path">
<template #label>
<span>
路由地址
<el-tooltip content="访问此页面自定义的url地址,建议/开头书写,例如 /app-name/menu-name" placement="top">
<i class="ri-question-line" />
</el-tooltip>
</span>
</template>
<el-input v-model="form.path" placeholder="请输入路由地址" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item v-if="form.menuType == 'F' || form.menuType == 'C'">
<template #label>
<span>
权限标识
<el-tooltip content="前端权限控制按钮是否显示" placement="top">
<i class="ri-question-line" />
</el-tooltip>
</span>
</template>
<el-input v-model="form.permission" placeholder="请权限标识" maxlength="50" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item v-if="form.menuType != 'F'">
<template #label>
<span>
菜单状态
<el-tooltip content="需要显示在菜单列表的菜单设置为显示,否则设置为隐藏" placement="top">
<i class="ri-question-line" />
</el-tooltip>
</span>
</template>
<el-radio-group v-model="form.visible">
<el-radio
v-for="dict in visibleOptions"
:key="dict.value"
:label="dict.value"
>{{ dict.label }}</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item v-if="form.menuType == 'F' || form.menuType == 'C'">
<template #label>
<span>
api权限
<el-tooltip content="配置在这个才做上需要使用到的接口,否则在设置用户角色时,接口将无权访问。" placement="top">
<i class="ri-question-line" />
</el-tooltip>
</span>
</template>
<el-transfer
v-model="form.apis"
style="text-align: left; display: inline-block"
filterable
:props="{
key: 'id',
label: 'title'
}"
:titles="['未授权', '已授权']"
:button-texts="['收回', '授权 ']"
:format="{
noChecked: '${total}',
hasChecked: '${checked}/${total}'
}"
class="panel"
:data="sysapiList"
@change="handleChange"
>
<template #default="{ option }">
<span>{{ option.title }}</span>
</template>
</el-transfer>
</el-form-item>
</el-col>
</el-row>
</el-form>
<div class="demo-drawer__footer">
<el-button type="primary" @click="submitForm">确 定</el-button>
<el-button @click="cancel">取 消</el-button>
</div>
</div>
</el-drawer>
</el-card>
</template>
</BasicLayout>
</template>
<script>
import { listMenu, getMenu, delMenu, addMenu, updateMenu } from '@/api/admin/sys-menu'
import { listSysApi } from '@/api/admin/sys-api'
import Treeselect from 'vue3-treeselect'
import 'vue3-treeselect/dist/vue3-treeselect.css'
import IconSelect from '@/components/IconSelect'
import { Search, Plus, Edit, Delete } from '@element-plus/icons-vue'
import { listMenu } from '@/api/admin/sys-menu'
import { Refresh, Search } from '@element-plus/icons-vue'
export default {
name: 'SysMenuManage',
components: { Treeselect, IconSelect },
setup() {
return { Search, Plus, Edit, Delete }
},
data() {
return {
// 遮罩层
loading: true,
// 菜单表格树数据
menuList: [],
sysapiList: [],
// 菜单树选项
menuOptions: [],
// 弹出层标题
title: '',
// 是否显示弹出层
open: false,
// 菜单状态数据字典
visibleOptions: [],
// 查询参数
queryParams: {
title: undefined,
visible: undefined
},
// 表单参数
form: {
apis: [],
sysApi: []
},
// 表单校验
rules: {
title: [{ required: true, message: '菜单标题不能为空', trigger: 'blur' }],
sort: [{ required: true, message: '菜单顺序不能为空', trigger: 'blur' }]
}
}
},
created() {
this.getList()
this.getApiList()
this.getDicts('sys_show_hide').then(response => {
this.visibleOptions = response.data
})
},
name: 'SysMenuReadOnly',
setup() { return { Refresh, Search } },
data() { return { loading: false, errorMessage: '', menuList: [], queryParams: { title: undefined }} },
created() { this.getList() },
methods: {
handleChange(value, direction, movedKeys) {
console.log(value, direction, movedKeys)
const list = this.form.sysApi
this.form.apis = value
if (direction === 'right') {
for (let x = 0; x < movedKeys.length; x++) {
for (let index = 0; index < this.sysapiList.length; index++) {
const element = this.sysapiList[index]
if (element.id === movedKeys[x]) {
list.push(element)
break
}
}
}
this.form.sysApi = list
} else if (direction === 'left') {
const l = []
for (let index = 0; index < movedKeys.length; index++) {
const element = movedKeys[index]
for (let x = 0; x < list.length; x++) {
const e = list[x]
if (element !== e.id) {
l.push()
break
}
}
}
this.form.sysApi = l
}
// this.setApis(this.form.SysApi)
console.log(this.form.sysApi)
},
getApiList() {
async getList() {
this.loading = true
listSysApi({ 'pageSize': 10000, 'type': 'BUS' }).then(response => {
this.sysapiList = response.data.list
this.loading = false
}
)
this.errorMessage = ''
try {
const response = await listMenu(this.queryParams)
this.menuList = response.data || []
} catch (error) {
this.menuList = []
this.errorMessage = error?.response?.status === 403 ? '没有查看菜单结构的权限' : '菜单结构加载失败'
} finally { this.loading = false }
},
handleClose(done) {
// if (this.loading) {
// return
// }
// this.$confirm('需要提交表单吗?')
// .then(_ => {
// this.loading = true
// this.timer = setTimeout(() => {
// done()
// // 动画关闭需要一定的时间
// setTimeout(() => {
// this.loading = false
// }, 400)
// }, 1000)
// })
// .catch(_ => {})
},
// 选择图标
selected(name) {
this.form.icon = name
},
/** 查询菜单列表 */
getList() {
this.loading = true
listMenu(this.queryParams).then(response => {
this.menuList = response.data
this.loading = false
})
},
/** 转换菜单数据结构 */
normalizer(node) {
if (node.children && !node.children.length) {
delete node.children
}
return {
id: node.menuId,
label: node.title,
children: node.children
}
},
/** 查询菜单下拉树结构 */
getTreeselect() {
listMenu().then(response => {
this.menuOptions = []
const menu = { menuId: 0, title: '主类目', children: [] }
menu.children = response.data
this.menuOptions.push(menu)
})
},
// 菜单显示状态字典翻译
visibleFormat(row) {
if (row.menuType === 'F') {
return '-- --'
}
return this.selectDictLabel(this.visibleOptions, row.visible)
},
// 取消按钮
cancel() {
this.open = false
this.reset()
},
// 表单重置
reset() {
this.form = {
menuId: undefined,
parentId: 0,
menuName: undefined,
icon: undefined,
menuType: 'M',
apis: [],
sort: 0,
action: this.form.menuType === 'A' ? this.form.action : '',
isFrame: '1',
visible: '0'
}
this.resetForm('form')
},
/** 搜索按钮操作 */
handleQuery() {
this.getList()
},
/** 新增按钮操作 */
handleAdd(row) {
this.reset()
this.getTreeselect()
if (row != null) {
this.form.parentId = row.menuId
}
this.open = true
this.title = '添加菜单'
},
/** 修改按钮操作 */
handleUpdate(row) {
this.reset()
this.getTreeselect()
getMenu(row.menuId).then(response => {
this.form = response.data
this.open = true
this.title = '修改菜单'
})
},
setApis(apiArray) {
var l = []
for (var index = 0; index < apiArray.length; index++) {
const element = apiArray[index]
l.push(element.id)
}
this.form.apis = l
},
/** 提交按钮 */
submitForm: function() {
this.$refs['form'].validate(valid => {
if (valid) {
if (this.form.menuId !== undefined) {
updateMenu(this.form, this.form.menuId).then(response => {
if (response.code === 200) {
this.msgSuccess(response.msg)
this.open = false
this.getList()
} else {
this.msgError(response.msg)
}
})
} else {
addMenu(this.form).then(response => {
if (response.code === 200) {
this.msgSuccess(response.msg)
this.open = false
this.getList()
} else {
this.msgError(response.msg)
}
})
}
}
})
},
/** 删除按钮操作 */
handleDelete(row) {
this.$confirm('是否确认删除名称为"' + row.title + '"的数据项?', '警告', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(function() {
var Ids = (row.menuId && [row.menuId]) || this.ids
return delMenu({ 'ids': Ids })
}).then((response) => {
if (response.code === 200) {
this.msgSuccess(response.msg)
this.open = false
this.getList()
} else {
this.msgError(response.msg)
}
}).catch(function() {})
}
handleQuery() { this.getList() },
resetQuery() { this.$refs.queryForm?.resetFields(); this.getList() }
}
}
</script>
<style lang="css">
.panel .el-transfer__buttons{
width: 150px;
}
.panel .el-transfer__buttons .el-button + .el-button{
margin-left:0;
}
.panel .el-transfer-panel{
width: 300px;
}
.el-col {
padding: 0 5px;
}
.el-drawer__header{
margin-bottom: 0;
}
<style scoped>
.system-page { min-height: 360px; }
.page-heading { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; margin-bottom: 20px; }
.page-heading h2 { margin: 0 0 6px; font-size: 20px; letter-spacing: 0; }
.page-heading p { margin: 0; color: var(--el-text-color-secondary); line-height: 1.5; }
</style>
+63 -522
View File
@@ -1,184 +1,35 @@
<template>
<BasicLayout>
<template #wrapper>
<el-card class="box-card">
<el-card class="box-card system-page">
<div class="page-heading"><div><h2>角色权限</h2><p>角色决定可见菜单;内置 chorus_operator 允许调整菜单,但不可改名、停用或删除。</p></div><el-button v-permisaction="['admin:sysRole:add']" type="primary" :icon="Plus" @click="handleAdd">新增角色</el-button></div>
<el-form ref="queryForm" :model="queryParams" :inline="true" class="search-form">
<el-form-item label="名称" prop="roleName">
<el-input
v-model="queryParams.roleName"
placeholder="请输入角色名称"
clearable
size="small"
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item label="权限字符" prop="roleKey">
<el-input
v-model="queryParams.roleKey"
placeholder="请输入权限字符"
clearable
size="small"
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select
v-model="queryParams.status"
placeholder="角色状态"
clearable
size="small"
>
<el-option
v-for="dict in statusOptions"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" size="small" :icon="Search" @click="handleQuery">搜索</el-button>
<el-button size="small" :icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
<el-form-item label="名称" prop="roleName"><el-input v-model="queryParams.roleName" clearable placeholder="输入角色名称" @keyup.enter="handleQuery" /></el-form-item>
<el-form-item label="权限字符" prop="roleKey"><el-input v-model="queryParams.roleKey" clearable placeholder="输入权限字符" @keyup.enter="handleQuery" /></el-form-item>
<el-form-item><el-button type="primary" :icon="Search" @click="handleQuery">搜索</el-button><el-button :icon="Refresh" @click="resetQuery">重置</el-button></el-form-item>
</el-form>
<div class="toolbar mb8">
<el-button v-permisaction="['admin:sysRole:add']" type="primary" size="small" :icon="Plus" @click="handleAdd">新增</el-button>
<el-button v-permisaction="['admin:sysRole:update']" type="primary" size="small" :icon="Edit" :disabled="single" @click="handleUpdate">修改</el-button>
<el-button v-permisaction="['admin:sysRole:remove']" type="danger" size="small" :icon="Delete" :disabled="multiple" @click="handleDelete">删除</el-button>
<el-button v-permisaction="['admin:sysRole:export']" size="small" :icon="Download" @click="handleExport">导出</el-button>
</div>
<el-table
v-loading="loading"
:data="roleList"
border
stripe
@selection-change="handleSelectionChange"
@sort-change="handleSortChang"
>
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="编码" sortable="custom" prop="roleId" width="80" />
<el-table-column label="名称" sortable="custom" prop="roleName" :show-overflow-tooltip="true" />
<el-table-column label="权限字符" prop="roleKey" :show-overflow-tooltip="true" width="150" />
<el-table-column label="排序" sortable="custom" prop="roleSort" width="80" />
<el-table-column label="状态" sortable="custom" width="80">
<template #default="scope">
<el-switch
v-model="scope.row.status"
active-value="2"
inactive-value="1"
@change="handleStatusChange(scope.row)"
/>
</template>
</el-table-column>
<el-table-column label="创建时间" sortable="custom" prop="createdAt" width="160">
<template #default="scope">
<span>{{ parseTime(scope.row.createdAt) }}</span>
</template>
</el-table-column>
<el-table-column
label="操作"
align="left"
class-name="small-padding fixed-width"
width="220"
>
<template #default="scope">
<el-button v-permisaction="['admin:sysRole:update']" type="primary" link size="small" :icon="Edit" @click="handleUpdate(scope.row)">修改</el-button>
<el-divider direction="vertical" />
<el-button v-permisaction="['admin:sysRole:update']" type="primary" link size="small" @click="handleDataScope(scope.row)">数据权限</el-button>
<template v-if="scope.row.roleKey!=='admin'">
<el-divider direction="vertical" />
<el-button v-permisaction="['admin:sysRole:remove']" type="danger" link size="small" :icon="Delete" @click="handleDelete(scope.row)">删除</el-button>
</template>
</template>
</el-table-column>
<el-alert v-if="errorMessage" :title="errorMessage" type="error" show-icon :closable="false"><template #default><el-button link type="primary" @click="getList">重新加载</el-button></template></el-alert>
<el-table v-else v-loading="loading" :data="roleList" border stripe empty-text="暂无角色">
<el-table-column prop="roleName" label="角色名称" min-width="160"><template #default="scope"><span>{{ scope.row.roleName }}</span><el-tag v-if="isProtected(scope.row)" class="protected-tag" size="small" type="warning" effect="plain">内置保护</el-tag></template></el-table-column>
<el-table-column prop="roleKey" label="权限字符" min-width="180" show-overflow-tooltip />
<el-table-column prop="roleSort" label="排序" width="80" align="center" />
<el-table-column label="状态" width="100" align="center"><template #default="scope"><el-switch v-model="scope.row.status" active-value="2" inactive-value="1" :disabled="isProtected(scope.row)" aria-label="角色状态" @change="handleStatusChange(scope.row)" /></template></el-table-column>
<el-table-column prop="createdAt" label="创建时间" width="170"><template #default="scope">{{ parseTime(scope.row.createdAt) }}</template></el-table-column>
<el-table-column label="操作" width="170" fixed="right"><template #default="scope"><el-button v-permisaction="['admin:sysRole:update']" link type="primary" :icon="Edit" @click="handleEdit(scope.row)">编辑授权</el-button><el-button v-if="!isProtected(scope.row)" v-permisaction="['admin:sysRole:remove']" link type="danger" :icon="Delete" @click="handleDelete(scope.row)">删除</el-button></template></el-table-column>
</el-table>
<pagination v-show="total > queryParams.pageSize" v-model:current-page="queryParams.pageIndex" v-model:page-size="queryParams.pageSize" :total="total" @pagination="getList" />
<pagination
v-show="total>0"
v-model:current-page="queryParams.pageIndex"
v-model:page-size="queryParams.pageSize"
:total="total"
@pagination="getList"
/>
<!-- 添加或修改角色配置对话框 -->
<el-dialog v-if="open" v-model="open" :title="title" width="500px" :close-on-click-modal="false">
<el-form ref="form" :model="form" :rules="rules" label-width="80px">
<el-form-item label="角色名称" prop="roleName">
<el-input v-model="form.roleName" placeholder="请输入角色名称" :disabled="isEdit" />
</el-form-item>
<el-form-item label="权限字符" prop="roleKey">
<el-input v-model="form.roleKey" placeholder="请输入权限字符" :disabled="isEdit" />
</el-form-item>
<el-form-item label="角色顺序" prop="roleSort">
<el-input-number v-model="form.roleSort" controls-position="right" :min="0" />
</el-form-item>
<el-form-item label="状态">
<el-radio-group v-model="form.status">
<el-radio
v-for="dict in statusOptions"
:key="dict.value"
:label="dict.value"
>{{ dict.label }}</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="菜单权限">
<el-tree
ref="menuTree"
:data="menuOptions"
show-checkbox
node-key="id"
:empty-text="menuOptionsAlert"
style="height:171px;overflow-y:auto;overflow-x:hidden;"
/>
</el-form-item>
<el-form-item label="备注">
<el-input v-model="form.remark" type="textarea" placeholder="请输入内容" />
</el-form-item>
<el-dialog v-model="dialogOpen" :title="form.roleId ? '编辑角色权限' : '新增角色'" width="560px" :close-on-click-modal="false">
<el-alert v-if="isProtected(form)" title="内置角色的名称、权限字符和状态不可修改,菜单授权仍可调整。" type="warning" show-icon :closable="false" class="dialog-alert" />
<el-form ref="form" :model="form" :rules="rules" label-width="90px">
<el-form-item label="角色名称" prop="roleName"><el-input v-model="form.roleName" :disabled="isProtected(form)" /></el-form-item>
<el-form-item label="权限字符" prop="roleKey"><el-input v-model="form.roleKey" :disabled="isProtected(form)" /></el-form-item>
<el-form-item label="排序" prop="roleSort"><el-input-number v-model="form.roleSort" :min="0" controls-position="right" /></el-form-item>
<el-form-item label="状态" prop="status"><el-radio-group v-model="form.status" :disabled="isProtected(form)"><el-radio label="2">正常</el-radio><el-radio label="1">停用</el-radio></el-radio-group></el-form-item>
<el-form-item label="菜单授权"><el-tree ref="menuTree" :data="menuOptions" node-key="id" show-checkbox default-expand-all :props="{ label: 'label', children: 'children' }" empty-text="暂无可授权菜单" class="menu-tree" /></el-form-item>
<el-form-item label="备注"><el-input v-model="form.remark" type="textarea" :rows="3" /></el-form-item>
</el-form>
<template #footer><div class="dialog-footer">
<el-button type="primary" @click="submitForm">确 定</el-button>
<el-button @click="cancel">取 消</el-button>
</div></template>
</el-dialog>
<!-- 分配角色数据权限对话框 -->
<el-dialog v-if="openDataScope" v-model="open" :title="title" width="500px" :close-on-click-modal="false">
<el-form :model="form" label-width="80px">
<el-form-item label="角色名称">
<el-input v-model="form.roleName" :disabled="true" />
</el-form-item>
<el-form-item label="权限字符">
<el-input v-model="form.roleKey" :disabled="true" />
</el-form-item>
<el-form-item label="权限范围">
<el-select v-model="form.dataScope">
<el-option
v-for="item in dataScopeOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item v-show="form.dataScope == 2" label="数据权限">
<el-tree
ref="dept"
:data="deptOptions"
show-checkbox
default-expand-all
node-key="id"
empty-text="加载中,请稍后"
:props="defaultProps"
/>
</el-form-item>
</el-form>
<template #footer><div class="dialog-footer">
<el-button type="primary" @click="submitDataScope">确 定</el-button>
<el-button @click="cancelDataScope">取 消</el-button>
</div></template>
<template #footer><el-button @click="dialogOpen = false">取消</el-button><el-button type="primary" :loading="saving" @click="submitForm">保存</el-button></template>
</el-dialog>
</el-card>
</template>
@@ -186,367 +37,57 @@
</template>
<script>
import { listRole, getRole, delRole, addRole, updateRole, dataScope, changeRoleStatus } from '@/api/admin/sys-role'
import { roleMenuTreeselect } from '@/api/admin/sys-menu'
import { treeselect as deptTreeselect, roleDeptTreeselect } from '@/api/admin/sys-dept'
import { formatJson } from '@/utils'
import { Search, Refresh, Plus, Edit, Delete, Download } from '@element-plus/icons-vue'
import { addRole, changeRoleStatus, delRole, getRole, listRole, updateRole } from '@/api/admin/sys-role'
import { listMenu } from '@/api/admin/sys-menu'
import { Delete, Edit, Plus, Refresh, Search } from '@element-plus/icons-vue'
const PROTECTED_ROLE_KEY = 'chorus_operator'
export default {
name: 'RoleManagement',
components: {
},
setup() {
return { Search, Refresh, Plus, Edit, Delete, Download }
},
setup() { return { Delete, Edit, Plus, Refresh, Search } },
data() {
return {
// 遮罩层
loading: true,
// 选中数组
ids: [],
// 非单个禁用
single: true,
// 非多个禁用
multiple: true,
// 总条数
total: 0,
// 角色表格数据
roleList: [],
menuIdsChecked: [],
// 弹出层标题
title: '',
// 是否显示弹出层
open: false,
// 是否显示弹出层(数据权限)
openDataScope: false,
isEdit: false,
// 日期范围
dateRange: [],
// 状态数据字典
statusOptions: [],
// 数据范围选项
dataScopeOptions: [
{
value: '1',
label: '全部数据权限'
},
{
value: '2',
label: '自定数据权限'
},
{
value: '3',
label: '本部门数据权限'
},
{
value: '4',
label: '本部门及以下数据权限'
},
{
value: '5',
label: '仅本人数据权限'
}
],
// 菜单列表
menuOptions: [],
menuList: [],
// 部门列表
deptOptions: [],
menuOptionsAlert: '加载中,请稍后',
// 查询参数
queryParams: {
pageIndex: 1,
pageSize: 10,
roleName: undefined,
roleKey: undefined,
status: undefined
},
// 表单参数
form: {
sysMenu: []
},
defaultProps: {
children: 'children',
label: 'label'
},
// 表单校验
loading: false, saving: false, errorMessage: '', dialogOpen: false,
roleList: [], menuOptions: [], total: 0, form: {},
queryParams: { pageIndex: 1, pageSize: 10, roleName: undefined, roleKey: undefined },
rules: {
roleName: [
{ required: true, message: '角色名称不能为空', trigger: 'blur' }
],
roleKey: [
{ required: true, message: '权限字符不能为空', trigger: 'blur' }
],
roleSort: [
{ required: true, message: '角色顺序不能为空', trigger: 'blur' }
]
roleName: [{ required: true, message: '请输入角色名称', trigger: 'blur' }],
roleKey: [{ required: true, message: '请输入权限字符', trigger: 'blur' }],
roleSort: [{ required: true, message: '请输入排序', trigger: 'change' }]
}
}
},
created() {
this.getList()
this.getMenuTreeselect()
this.getDicts('sys_normal_disable').then(response => {
this.statusOptions = response.data
})
},
created() { this.getList(); this.getMenus() },
methods: {
/** 查询角色列表 */
getList() {
this.loading = true
listRole(this.addDateRange(this.queryParams, this.dateRange)).then(
response => {
this.roleList = response.data.list
this.total = response.data.count
this.loading = false
}
)
async getList() {
this.loading = true; this.errorMessage = ''
try { const response = await listRole(this.queryParams); this.roleList = response.data?.list || []; this.total = response.data?.count || 0 } catch (error) { this.roleList = []; this.total = 0; this.errorMessage = error?.response?.status === 403 ? '没有查看角色权限的权限' : '角色权限加载失败' } finally { this.loading = false }
},
/** 查询菜单树结构 */
getMenuTreeselect() {
roleMenuTreeselect(0).then(response => {
this.menuOptions = response.data.menus
this.menuList = this.menuOptions
})
async getMenus() { try { const response = await listMenu({}); this.menuOptions = this.toTreeOptions(response.data || []) } catch (error) { this.menuOptions = [] } },
toTreeOptions(nodes) { return nodes.map(node => ({ id: node.menuId, label: node.title, children: this.toTreeOptions(node.children || []) })) },
isProtected(role) { return role?.roleKey === PROTECTED_ROLE_KEY },
handleQuery() { this.queryParams.pageIndex = 1; this.getList() },
resetQuery() { this.$refs.queryForm?.resetFields(); this.handleQuery() },
handleAdd() { this.form = { roleName: '', roleKey: '', roleSort: 0, status: '2', remark: '', menuIds: [] }; this.dialogOpen = true; this.$nextTick(() => this.$refs.menuTree?.setCheckedKeys([])) },
async handleEdit(row) { const response = await getRole(row.roleId); this.form = { ...response.data }; this.dialogOpen = true; this.$nextTick(() => this.$refs.menuTree?.setCheckedKeys(response.data?.menuIds || [])) },
async submitForm() {
await this.$refs.form.validate(); this.saving = true
try { this.form.menuIds = this.$refs.menuTree?.getCheckedKeys() || []; if (this.form.roleId) await updateRole(this.form, this.form.roleId); else await addRole(this.form); this.msgSuccess('角色权限已保存'); this.dialogOpen = false; this.getList() } finally { this.saving = false }
},
/** 查询部门树结构 */
getDeptTreeselect() {
deptTreeselect().then(response => {
this.deptOptions = response.data.list
})
},
// 所有菜单节点数据
getMenuAllCheckedKeys() {
// 目前被选中的菜单节点
const checkedKeys = this.$refs.menuTree.getHalfCheckedKeys()
console.log('目前被选中的菜单节点', checkedKeys)
// 半选中的菜单节点
const halfCheckedKeys = this.$refs.menuTree.getCheckedKeys()
console.log('半选中的菜单节点', halfCheckedKeys)
// checkedKeys.unshift.apply(checkedKeys, halfCheckedKeys)
return halfCheckedKeys
},
// 所有部门节点数据
getDeptAllCheckedKeys() {
// 目前被选中的部门节点
const checkedKeys = this.$refs.dept.getCheckedKeys()
// 半选中的部门节点
// const halfCheckedKeys = this.$refs.dept.getCheckedKeys()
// checkedKeys.unshift.apply(checkedKeys, halfCheckedKeys)
return checkedKeys
},
/** 根据角色ID查询菜单树结构 */
getRoleMenuTreeselect(row, checkedKeys) {
if (row.roleKey === 'admin') {
this.menuOptionsAlert = '系统超级管理员无需此操作'
this.menuOptions = []
} else {
this.$nextTick(() => {
this.$refs.menuTree.setCheckedKeys(checkedKeys)
})
}
},
/** 根据角色ID查询部门树结构 */
getRoleDeptTreeselect(roleId) {
roleDeptTreeselect(roleId).then(response => {
this.deptOptions = response.data.depts
this.$nextTick(() => {
this.$refs.dept.setCheckedKeys(response.data.checkedKeys)
})
})
},
// 角色状态修改
handleStatusChange(row) {
const text = row.status === '2' ? '启用' : '停用'
this.$confirm('确认要"' + text + '""' + row.roleName + '"角色吗?', '警告', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(function() {
return changeRoleStatus(row.roleId, row.status)
}).then((res) => {
console.log('res', res)
this.msgSuccess(res.msg)
}).catch(function() {
row.status = row.status === '2' ? '1' : '2'
})
},
// 取消按钮
cancel() {
this.open = false
this.reset()
},
// 取消按钮(数据权限)
cancelDataScope() {
this.openDataScope = false
this.reset()
},
// 表单重置
reset() {
this.menuOptions = this.menuList
if (this.$refs.menuTree !== undefined) {
this.$refs.menuTree.setCheckedKeys([])
}
this.form = {
roleId: undefined,
roleName: undefined,
roleKey: undefined,
roleSort: 0,
status: '2',
menuIds: [],
deptIds: [],
sysMenu: [],
remark: undefined
}
this.resetForm('form')
},
/** 搜索按钮操作 */
handleQuery() {
this.queryParams.pageIndex = 1
this.getList()
},
/** 重置按钮操作 */
resetQuery() {
this.dateRange = []
this.resetForm('queryForm')
this.handleQuery()
},
// 多选框选中数据
handleSelectionChange(selection) {
this.ids = selection.map(item => item.roleId)
this.single = selection.length !== 1
this.multiple = !selection.length
},
/** 新增按钮操作 */
handleAdd() {
this.reset()
// this.getMenuTreeselect(0)
this.open = true
this.title = '添加角色'
this.isEdit = false
},
handleSortChang(column, prop, order) {
prop = column.prop
order = column.order
if (order === 'descending') {
this.queryParams[prop + 'Order'] = 'desc'
} else if (order === 'ascending') {
this.queryParams[prop + 'Order'] = 'asc'
} else {
this.queryParams[prop + 'Order'] = undefined
}
this.getList()
},
/** 修改按钮操作 */
handleUpdate(row) {
this.menuIdsChecked = []
this.reset()
const roleId = row.roleId || this.ids
getRole(roleId).then(response => {
this.form = response.data
this.menuIdsChecked = response.data.menuIds
this.title = '修改角色'
this.isEdit = true
this.open = true
this.getRoleMenuTreeselect(row, response.data.menuIds)
})
},
/** 分配数据权限操作 */
handleDataScope(row) {
this.reset()
getRole(row.roleId).then(response => {
this.form = response.data
this.openDataScope = true
this.title = '分配数据权限'
this.getRoleDeptTreeselect(row.roleId)
})
},
/** 提交按钮 */
submitForm: function() {
this.$refs['form'].validate(valid => {
if (valid) {
if (this.form.roleId !== undefined) {
this.form.menuIds = this.getMenuAllCheckedKeys()
updateRole(this.form, this.form.roleId).then(response => {
if (response.code === 200) {
this.msgSuccess(response.msg)
this.open = false
this.getList()
} else {
this.msgError(response.msg)
}
})
} else {
this.form.menuIds = this.getMenuAllCheckedKeys()
addRole(this.form).then(response => {
if (response.code === 200) {
this.msgSuccess(response.msg)
this.open = false
this.getList()
} else {
this.msgError(response.msg)
}
})
}
}
})
},
/** 提交按钮(数据权限) */
submitDataScope: function() {
if (this.form.roleId !== undefined) {
this.form.deptIds = this.getDeptAllCheckedKeys()
console.log(this.getDeptAllCheckedKeys())
dataScope(this.form).then(response => {
if (response.code === 200) {
this.msgSuccess(response.msg)
this.openDataScope = false
this.getList()
} else {
this.msgError(response.msg)
}
})
}
},
/** 删除按钮操作 */
handleDelete(row) {
const roleIds = (row.roleId && [row.roleId]) || this.ids
this.$confirm('是否确认删除角色编号为"' + roleIds + '"的数据项?', '警告', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(function() {
return delRole({ 'ids': roleIds })
}).then((response) => {
this.getList()
this.msgSuccess(response.msg)
}).catch(function() {})
},
/** 导出按钮操作 */
handleExport() {
this.$confirm('是否确认导出所有角色数据项?', '警告', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.downloadLoading = true
import('@/vendor/Export2Excel').then(excel => {
const tHeader = ['角色编号', '角色名称', '权限字符', '显示顺序', '状态', '创建时间']
const filterVal = ['roleId', 'roleName', 'roleKey', 'roleSort', 'status', 'createdAt']
const list = this.roleList
const data = formatJson(filterVal, list)
excel.export_json_to_excel({
header: tHeader,
data,
filename: '角色管理',
autoWidth: true, // Optional
bookType: 'xlsx' // Optional
})
this.downloadLoading = false
})
})
}
async handleStatusChange(row) { const previous = row.status === '2' ? '1' : '2'; try { await changeRoleStatus(row.roleId, row.status); this.msgSuccess('角色状态已更新') } catch (error) { row.status = previous } },
async handleDelete(row) { await this.$confirm(`确认删除角色“${row.roleName}”?`, '删除角色', { type: 'warning' }); await delRole({ ids: [row.roleId] }); this.msgSuccess('角色已删除'); this.getList() }
}
}
</script>
<style scoped>
.system-page { min-height: 420px; }
.page-heading { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; margin-bottom: 20px; }
.page-heading h2 { margin: 0 0 6px; font-size: 20px; letter-spacing: 0; }
.page-heading p { margin: 0; color: var(--el-text-color-secondary); line-height: 1.5; }
.protected-tag { margin-left: 8px; }
.dialog-alert { margin-bottom: 18px; }
.menu-tree { max-height: 260px; overflow: auto; border: 1px solid var(--el-border-color); padding: 8px; }
@media (max-width: 720px) { .page-heading { align-items: stretch; flex-direction: column; } .page-heading .el-button { align-self: flex-start; } }
</style>
+107 -653
View File
@@ -1,683 +1,137 @@
<template>
<BasicLayout>
<template #wrapper>
<el-card class="box-card">
<el-row :gutter="20">
<!--部门数据-->
<el-col :span="4" :xs="24">
<div class="head-container">
<el-input
v-model="deptName"
placeholder="请输入部门名称"
clearable
size="small"
style="margin-bottom: 20px"
/>
</div>
<div class="head-container">
<el-tree
ref="tree"
:data="deptOptions"
:props="defaultProps"
:expand-on-click-node="false"
:filter-node-method="filterNode"
default-expand-all
@node-click="handleNodeClick"
/>
</div>
</el-col>
<!--用户数据-->
<el-col :span="20" :xs="24">
<el-form ref="queryForm" :model="queryParams" :inline="true" label-width="68px">
<el-form-item label="用户名称" prop="username">
<el-input
v-model="queryParams.username"
placeholder="请输入用户名称"
clearable
size="small"
style="width: 160px"
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item label="手机号码" prop="phone">
<el-input
v-model="queryParams.phone"
placeholder="请输入手机号码"
clearable
size="small"
style="width: 160px"
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select
v-model="queryParams.status"
placeholder="用户状态"
clearable
size="small"
style="width: 160px"
>
<el-option
v-for="dict in statusOptions"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" size="mini" @click="handleQuery">搜索</el-button>
<el-button size="mini" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button
v-permisaction="['admin:sysUser:add']"
type="primary"
size="mini"
@click="handleAdd"
>新增</el-button>
</el-col>
<el-col :span="1.5">
<el-button
v-permisaction="['admin:sysUser:edit']"
type="success"
size="mini"
:disabled="single"
@click="handleUpdate"
>修改</el-button>
</el-col>
<el-col :span="1.5">
<el-button
v-permisaction="['admin:sysUser:remove']"
type="danger"
size="mini"
:disabled="multiple"
@click="handleDelete"
>删除</el-button>
</el-col>
</el-row>
<el-table
v-loading="loading"
:data="userList"
border
@selection-change="handleSelectionChange"
@sort-change="handleSortChang"
>
<el-table-column type="selection" width="45" align="center" />
<el-table-column label="编号" width="75" prop="userId" sortable="custom" />
<el-table-column label="登录名" width="105" prop="username" sortable="custom" :show-overflow-tooltip="true" />
<el-table-column label="昵称" prop="nickName" :show-overflow-tooltip="true" />
<el-table-column label="部门" prop="dept.deptName" :show-overflow-tooltip="true" />
<el-table-column label="手机号" prop="phone" width="108" />
<el-table-column label="状态" width="80" sortable="custom">
<template #default="scope">
<el-switch
v-model="scope.row.status"
active-value="2"
inactive-value="1"
@change="handleStatusChange(scope.row)"
/>
</template>
</el-table-column>
<el-table-column
label="创建时间"
prop="createdAt"
sortable="custom"
width="155"
>
<template #default="scope">
<span>{{ parseTime(scope.row.createdAt) }}</span>
</template>
</el-table-column>
<el-table-column
label="操作"
width="160"
fix="right"
class-name="small-padding fixed-width"
>
<template #default="scope">
<el-button
v-permisaction="['admin:sysUser:edit']"
size="mini"
type="text"
@click="handleUpdate(scope.row)"
>修改</el-button>
<el-button
v-if="scope.row.userId !== 1"
v-permisaction="['admin:sysUser:remove']"
size="mini"
type="text"
@click="handleDelete(scope.row)"
>删除</el-button>
<el-button
v-permisaction="['admin:sysUser:resetPassword']"
size="mini"
type="text"
@click="handleResetPwd(scope.row)"
>重置</el-button>
</template>
</el-table-column>
</el-table>
<pagination
v-show="total>0"
v-model:current-page="queryParams.pageIndex"
v-model:page-size="queryParams.pageSize"
:total="total"
@pagination="getList"
/>
</el-col>
</el-row>
</el-card>
<!-- 添加或修改参数配置对话框 -->
<el-dialog v-model="open" :title="title" width="600px" :close-on-click-modal="false">
<el-form ref="form" :model="form" :rules="rules" label-width="80px">
<el-row>
<el-col :span="12">
<el-form-item label="用户昵称" prop="nickName">
<el-input v-model="form.nickName" placeholder="请输入用户昵称" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="归属部门" prop="deptId">
<treeselect
v-model="form.deptId"
:options="deptOptions"
placeholder="请选择归属部门"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="手机号码" prop="phone">
<el-input v-model="form.phone" placeholder="请输入手机号码" maxlength="11" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="邮箱" prop="email">
<el-input v-model="form.email" placeholder="请输入邮箱" maxlength="50" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="用户名称" prop="username">
<el-input v-model="form.username" placeholder="请输入用户名称" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item v-if="form.userId == undefined" label="用户密码" prop="password">
<el-input v-model="form.password" placeholder="请输入用户密码" type="password" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="用户性别">
<el-select v-model="form.sex" placeholder="请选择">
<el-option
v-for="dict in sexOptions"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="状态">
<el-radio-group v-model="form.status">
<el-radio
v-for="dict in statusOptions"
:key="dict.value"
:label="dict.value"
>{{ dict.label }}</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="岗位">
<el-select v-model="form.postId" placeholder="请选择" @change="$forceUpdate()">
<el-option
v-for="item in postOptions"
:key="item.postId"
:label="item.postName"
:value="item.postId"
:disabled="item.status == 1"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="角色">
<el-select v-model="form.roleId" placeholder="请选择" @change="$forceUpdate()">
<el-option
v-for="item in roleOptions"
:key="item.roleId"
:label="item.roleName"
:value="item.roleId"
:disabled="item.status == 1"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="备注">
<el-input v-model="form.remark" type="textarea" placeholder="请输入内容" />
</el-form-item>
</el-col>
</el-row>
<el-card class="box-card system-page">
<div class="page-heading">
<div><h2>管理员账号</h2><p>维护管理端登录账号;当前账号和最后一个有效管理员受后端保护。</p></div>
<el-button v-permisaction="['admin:sysUser:add']" type="primary" :icon="Plus" @click="handleAdd">新增管理员</el-button>
</div>
<el-form ref="queryForm" :model="queryParams" :inline="true" class="search-form">
<el-form-item label="账号" prop="username"><el-input v-model="queryParams.username" clearable placeholder="输入登录账号" @keyup.enter="handleQuery" /></el-form-item>
<el-form-item label="昵称" prop="nickName"><el-input v-model="queryParams.nickName" clearable placeholder="输入昵称" @keyup.enter="handleQuery" /></el-form-item>
<el-form-item label="状态" prop="status"><el-select v-model="queryParams.status" clearable placeholder="全部状态" style="width: 130px"><el-option label="正常" value="2" /><el-option label="停用" value="1" /></el-select></el-form-item>
<el-form-item><el-button type="primary" :icon="Search" @click="handleQuery">搜索</el-button><el-button :icon="Refresh" @click="resetQuery">重置</el-button></el-form-item>
</el-form>
<template #footer><div class="dialog-footer">
<el-button type="primary" @click="submitForm">确 定</el-button>
<el-button @click="cancel">取 消</el-button>
</div></template>
</el-dialog>
<!-- 用户导入对话框 -->
<el-dialog v-model="open" :title="upload.title" width="400px" :close-on-click-modal="false">
<el-upload
ref="upload"
:limit="1"
accept=".xlsx, .xls"
:headers="upload.headers"
:action="upload.url + '?updateSupport=' + upload.updateSupport"
:disabled="upload.isUploading"
:on-progress="handleFileUploadProgress"
:on-success="handleFileSuccess"
:auto-upload="false"
drag
>
<i class="ri-upload-line" />
<div class="el-upload__text">
将文件拖到此处,或
<em>点击上传</em>
</div>
<template #tip>
<div class="el-upload__tip">
<el-checkbox v-model="upload.updateSupport" />是否更新已经存在的用户数据
<el-link type="info" style="font-size:12px" @click="importTemplate">下载模板</el-link>
<el-alert v-if="errorMessage" :title="errorMessage" type="error" show-icon :closable="false"><template #default><el-button link type="primary" @click="getList">重新加载</el-button></template></el-alert>
<el-table v-else v-loading="loading" class="desktop-user-table" :data="userList" border stripe empty-text="暂无管理员账号">
<el-table-column prop="username" label="登录账号" min-width="150" show-overflow-tooltip>
<template #default="scope"><span>{{ scope.row.username }}</span><el-tag v-if="scope.row.userId === currentUserId" class="current-tag" size="small" effect="plain">当前账号</el-tag></template>
</el-table-column>
<el-table-column prop="nickName" label="昵称" min-width="140" show-overflow-tooltip />
<el-table-column prop="roleId" label="角色" min-width="150"><template #default="scope">{{ roleName(scope.row.roleId) }}</template></el-table-column>
<el-table-column label="状态" width="100" align="center"><template #default="scope"><el-switch v-model="scope.row.status" active-value="2" inactive-value="1" :disabled="scope.row.userId === currentUserId" aria-label="管理员状态" @change="handleStatusChange(scope.row)" /></template></el-table-column>
<el-table-column prop="createdAt" label="创建时间" width="170"><template #default="scope">{{ parseTime(scope.row.createdAt) }}</template></el-table-column>
<el-table-column label="操作" width="220" fixed="right"><template #default="scope">
<el-button v-permisaction="['admin:sysUser:edit']" link type="primary" :icon="Edit" @click="handleEdit(scope.row)">编辑</el-button>
<el-button v-permisaction="['admin:sysUser:resetPassword']" link type="primary" :icon="Key" @click="handleResetPassword(scope.row)">重置密码</el-button>
<el-button v-if="scope.row.userId !== currentUserId" v-permisaction="['admin:sysUser:remove']" link type="danger" :icon="Delete" @click="handleDelete(scope.row)">删除</el-button>
</template></el-table-column>
</el-table>
<div v-if="!errorMessage" v-loading="loading" class="mobile-user-list">
<div v-for="user in userList" :key="user.userId" class="mobile-user-item">
<div class="mobile-user-heading"><strong>{{ user.username }}</strong><el-tag v-if="user.userId === currentUserId" size="small" effect="plain">当前账号</el-tag></div>
<dl><div><dt>昵称</dt><dd>{{ user.nickName || '-' }}</dd></div><div><dt>角色</dt><dd>{{ roleName(user.roleId) }}</dd></div><div><dt>状态</dt><dd>{{ user.status === '2' ? '正常' : '停用' }}</dd></div></dl>
<div class="mobile-user-actions">
<el-button v-permisaction="['admin:sysUser:edit']" link type="primary" :icon="Edit" @click="handleEdit(user)">编辑</el-button>
<el-button v-permisaction="['admin:sysUser:resetPassword']" link type="primary" :icon="Key" @click="handleResetPassword(user)">重置密码</el-button>
<el-button v-if="user.userId !== currentUserId" v-permisaction="['admin:sysUser:remove']" link type="danger" :icon="Delete" @click="handleDelete(user)">删除</el-button>
</div>
<div class="el-upload__tip" style="color:red">提示:仅允许导入"xls"或"xlsx"格式文件!</div>
</template>
</el-upload>
<template #footer><div class="dialog-footer">
<el-button type="primary" @click="submitFileForm">确 定</el-button>
<el-button @click="upload.open = false">取 消</el-button>
</div></template>
</el-dialog>
</div>
<el-empty v-if="!loading && userList.length === 0" description="暂无管理员账号" :image-size="72" />
</div>
<pagination v-show="total > queryParams.pageSize" v-model:current-page="queryParams.pageIndex" v-model:page-size="queryParams.pageSize" :total="total" @pagination="getList" />
<el-dialog v-model="dialogOpen" :title="form.userId ? '编辑管理员' : '新增管理员'" width="520px" :close-on-click-modal="false">
<el-form ref="form" :model="form" :rules="rules" label-width="90px">
<el-form-item label="登录账号" prop="username"><el-input v-model="form.username" autocomplete="off" /></el-form-item>
<el-form-item label="昵称" prop="nickName"><el-input v-model="form.nickName" /></el-form-item>
<el-form-item v-if="!form.userId" label="初始密码" prop="password"><el-input v-model="form.password" type="password" show-password autocomplete="new-password" /></el-form-item>
<el-form-item label="角色" prop="roleId"><el-select v-model="form.roleId" style="width: 100%"><el-option v-for="role in activeRoles" :key="role.roleId" :label="role.roleName" :value="role.roleId" /></el-select></el-form-item>
<el-form-item v-if="!form.userId" label="状态" prop="status"><el-radio-group v-model="form.status"><el-radio label="2">正常</el-radio><el-radio label="1">停用</el-radio></el-radio-group></el-form-item>
<el-form-item label="备注" prop="remark"><el-input v-model="form.remark" type="textarea" :rows="3" /></el-form-item>
</el-form>
<template #footer><el-button @click="dialogOpen = false">取消</el-button><el-button type="primary" :loading="saving" @click="submitForm">保存</el-button></template>
</el-dialog>
</el-card>
</template>
</BasicLayout>
</template>
<script>
import { listUser, getUser, delUser, addUser, updateUser, exportUser, resetUserPwd, changeUserStatus, importTemplate } from '@/api/admin/sys-user'
import { getToken } from '@/utils/auth'
import { listPost } from '@/api/admin/sys-post'
import { addUser, changeUserStatus, delUser, getUser, listUser, resetUserPwd, updateUser } from '@/api/admin/sys-user'
import { listRole } from '@/api/admin/sys-role'
import { treeselect } from '@/api/admin/sys-dept'
import Treeselect from 'vue3-treeselect'
import 'vue3-treeselect/dist/vue3-treeselect.css'
import { Delete, Edit, Key, Plus, Refresh, Search } from '@element-plus/icons-vue'
export default {
name: 'SysUserManage',
components: { Treeselect },
setup() { return { Delete, Edit, Key, Plus, Refresh, Search } },
data() {
return {
// 遮罩层
loading: true,
// 选中数组
ids: [],
// 非单个禁用
single: true,
// 非多个禁用
multiple: true,
// 总条数
total: 0,
// 用户表格数据
userList: null,
// 弹出层标题
title: '',
// 部门树选项
deptOptions: undefined,
// 是否显示弹出层
open: false,
// 部门名称
deptName: undefined,
// 默认密码
initPassword: undefined,
// 日期范围
dateRange: [],
// 状态数据字典
statusOptions: [],
// 性别状态字典
sexOptions: [],
// 岗位选项
postOptions: [],
// 角色选项
roleOptions: [],
// 表单参数
loading: false, saving: false, errorMessage: '', dialogOpen: false,
userList: [], roleOptions: [], total: 0,
queryParams: { pageIndex: 1, pageSize: 10, username: undefined, nickName: undefined, status: undefined },
form: {},
defaultProps: {
children: 'children',
label: 'label'
},
// 用户导入参数
upload: {
// 是否显示弹出层(用户导入)
open: false,
// 弹出层标题(用户导入)
title: '',
// 是否禁用上传
isUploading: false,
// 是否更新已经存在的用户数据
updateSupport: 0,
// 设置上传的请求头部
headers: { Authorization: 'Bearer ' + getToken() },
// 上传的地址
url: process.env.VUE_APP_BASE_API + '/system/user/importData'
},
// 查询参数
queryParams: {
pageIndex: 1,
pageSize: 10,
username: undefined,
phone: undefined,
status: undefined,
deptId: undefined
},
// 表单校验
rules: {
username: [{ required: true, message: '用户名称不能为空', trigger: 'blur' }],
nickName: [{ required: true, message: '用户昵称不能为空', trigger: 'blur' }],
deptId: [{ required: true, message: '归属部门不能为空', trigger: 'blur' }],
password: [{ required: true, message: '用户密码不能为空', trigger: 'blur' }],
email: [
{ required: true, message: '邮箱地址不能为空', trigger: 'blur' },
{ type: 'email', message: "'请输入正确的邮箱地址", trigger: ['blur', 'change'] }
],
phone: [
{ required: true, message: '手机号码不能为空', trigger: 'blur' },
{ pattern: /^1[3|4|5|6|7|8|9][0-9]\d{8}$/, message: '请输入正确的手机号码', trigger: 'blur' }
]
username: [{ required: true, message: '请输入登录账号', trigger: 'blur' }],
nickName: [{ required: true, message: '请输入昵称', trigger: 'blur' }],
password: [{ required: true, min: 6, message: '密码至少 6 个字符', trigger: 'blur' }],
roleId: [{ required: true, message: '请选择角色', trigger: 'change' }]
}
}
},
watch: {
// 根据名称筛选部门树
deptName(val) {
this.$refs.tree.filter(val)
}
},
created() {
this.getList()
this.getTreeselect()
this.getDicts('sys_normal_disable').then(response => {
this.statusOptions = response.data
})
this.getDicts('sys_user_sex').then(response => {
this.sexOptions = response.data
})
this.getConfigKey('sys_user_initPassword').then(response => {
this.initPassword = response.data.configValue
})
computed: {
currentUserId() { return this.$store.state.user.userId },
activeRoles() { return this.roleOptions.filter(role => role.status === '2') }
},
created() { this.getList(); this.getRoles() },
methods: {
/** 查询用户列表 */
getList() {
this.loading = true
listUser(this.addDateRange(this.queryParams, this.dateRange)).then(response => {
this.userList = response.data.list
this.total = response.data.count
this.loading = false
}
)
async getList() {
this.loading = true; this.errorMessage = ''
try { const response = await listUser(this.queryParams); this.userList = response.data?.list || []; this.total = response.data?.count || 0 } catch (error) { this.userList = []; this.total = 0; this.errorMessage = error?.response?.status === 403 ? '没有查看管理员账号的权限' : '管理员账号加载失败' } finally { this.loading = false }
},
/** 查询部门下拉树结构 */
getTreeselect() {
treeselect().then(response => {
this.deptOptions = response.data
})
async getRoles() { try { const response = await listRole({ pageIndex: 1, pageSize: 100 }); this.roleOptions = response.data?.list || [] } catch (error) { this.roleOptions = [] } },
roleName(roleId) { return this.roleOptions.find(role => role.roleId === roleId)?.roleName || `角色 #${roleId}` },
handleQuery() { this.queryParams.pageIndex = 1; this.getList() },
resetQuery() { this.$refs.queryForm?.resetFields(); this.handleQuery() },
handleAdd() { this.form = { username: '', nickName: '', password: '', roleId: undefined, status: '2', remark: '' }; this.dialogOpen = true },
async handleEdit(row) { const response = await getUser(row.userId); const data = response.data || {}; this.form = { userId: data.userId, username: data.username, nickName: data.nickName, roleId: data.roleId, remark: data.remark || '' }; this.dialogOpen = true },
async submitForm() {
await this.$refs.form.validate(); this.saving = true
try { if (this.form.userId) await updateUser(this.form); else await addUser(this.form); this.msgSuccess('管理员账号已保存'); this.dialogOpen = false; this.getList() } finally { this.saving = false }
},
// 筛选节点
filterNode(value, data) {
if (!value) return true
return data.label.indexOf(value) !== -1
async handleStatusChange(row) {
const previous = row.status === '2' ? '1' : '2'
try { await changeUserStatus(row); this.msgSuccess('管理员状态已更新') } catch (error) { row.status = previous }
},
// 节点单击事件
handleNodeClick(data) {
this.queryParams.deptId = '/' + data.id + '/'
this.getList()
async handleResetPassword(row) {
const value = await this.$prompt(`为 ${row.username} 设置新密码`, '重置密码', { inputType: 'password', inputPattern: /^.{6,}$/, inputErrorMessage: '密码至少 6 个字符', confirmButtonText: '确定', cancelButtonText: '取消' }).then(result => result.value).catch(() => '')
if (!value) return
await resetUserPwd(row.userId, value); this.msgSuccess('密码已重置')
},
/** 转换菜单数据结构 */
normalizer(node) {
if (node.children && !node.children.length) {
delete node.children
}
return {
id: node.id,
label: node.label,
children: node.children
}
},
/** 排序回调函数 */
handleSortChang(column, prop, order) {
prop = column.prop
order = column.order
if (this.order !== '' && this.order !== prop + 'Order') {
this.queryParams[this.order] = undefined
}
if (order === 'descending') {
this.queryParams[prop + 'Order'] = 'desc'
this.order = prop + 'Order'
} else if (order === 'ascending') {
this.queryParams[prop + 'Order'] = 'asc'
this.order = prop + 'Order'
} else {
this.queryParams[prop + 'Order'] = undefined
}
this.getList()
},
// 用户状态修改
handleStatusChange(row) {
const text = row.status === '2' ? '启用' : '停用'
this.$confirm('确认要"' + text + '""' + row.username + '"用户吗?', '警告', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(function() {
return changeUserStatus(row)
}).then(() => {
this.msgSuccess(text + '成功')
}).catch(function() {
row.status = row.status === '2' ? '1' : '2'
})
},
// 取消按钮
cancel() {
this.open = false
this.reset()
},
// 表单重置
reset() {
this.form = {
userId: undefined,
deptId: undefined,
username: undefined,
nickName: undefined,
password: undefined,
phone: undefined,
email: undefined,
sex: undefined,
status: '2',
remark: undefined,
postIds: undefined,
roleIds: undefined
}
this.resetForm('form')
},
/** 搜索按钮操作 */
handleQuery() {
this.queryParams.page = 1
this.getList()
},
/** 重置按钮操作 */
resetQuery() {
this.dateRange = []
this.resetForm('queryForm')
this.queryParams.deptId = ''
this.handleQuery()
},
// 多选框选中数据
handleSelectionChange(selection) {
this.ids = selection.map(item => item.userId)
this.single = selection.length !== 1
this.multiple = !selection.length
},
/** 新增按钮操作 */
handleAdd() {
this.reset()
this.getTreeselect()
listPost({ pageSize: 1000 }).then(response => {
this.postOptions = response.data.list
})
listRole({ pageSize: 1000 }).then(response => {
this.roleOptions = response.data.list
})
this.open = true
this.title = '添加用户'
this.form.password = this.initPassword
},
/** 修改按钮操作 */
handleUpdate(row) {
this.reset()
const userId = row.userId || this.ids
getUser(userId).then(response => {
this.form = response.data
this.open = true
this.title = '修改用户'
this.form.password = ''
})
listPost({ pageSize: 1000 }).then(response => {
this.postOptions = response.data.list
})
listRole({ pageSize: 1000 }).then(response => {
this.roleOptions = response.data.list
})
},
/** 重置密码按钮操作 */
handleResetPwd(row) {
this.$prompt('请输入"' + row.username + '"的新密码', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消'
}).then(({ value }) => {
resetUserPwd(row.userId, value).then(response => {
if (response.code === 200) {
this.msgSuccess(response.msg)
} else {
this.msgError(response.msg)
}
})
}).catch(() => {})
},
/** 提交按钮 */
submitForm: function() {
this.$refs['form'].validate(valid => {
if (valid) {
if (this.form.userId !== undefined) {
updateUser(this.form).then(response => {
if (response.code === 200) {
this.msgSuccess(response.msg)
this.open = false
this.getList()
} else {
this.msgError(response.msg)
}
})
} else {
addUser(this.form).then(response => {
if (response.code === 200) {
this.msgSuccess(response.msg)
this.open = false
this.getList()
} else {
this.msgError(response.msg)
}
})
}
}
})
},
/** 删除按钮操作 */
handleDelete(row) {
const Ids = (row.userId && [row.userId]) || this.ids
this.$confirm('是否确认删除用户编号为"' + Ids + '"的数据项?', '警告', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(function() {
return delUser({ 'ids': Ids })
}).then((response) => {
if (response.code === 200) {
this.msgSuccess(response.msg)
this.open = false
this.getList()
} else {
this.msgError(response.msg)
}
}).catch(function() {})
},
/** 导出按钮操作 */
handleExport() {
const queryParams = this.queryParams
this.$confirm('是否确认导出所有用户数据项?', '警告', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(function() {
return exportUser(queryParams)
}).then(response => {
this.download(response.msg)
}).catch(function() {})
},
/** 导入按钮操作 */
handleImport() {
this.upload.title = '用户导入'
this.upload.open = true
},
/** 下载模板操作 */
importTemplate() {
importTemplate().then(response => {
this.download(response.msg)
})
},
// 文件上传中处理
handleFileUploadProgress(event, file, fileList) {
this.upload.isUploading = true
},
// 文件上传成功处理
handleFileSuccess(response, file, fileList) {
this.upload.open = false
this.upload.isUploading = false
this.$refs.upload.clearFiles()
this.$alert(response.msg, '导入结果', { dangerouslyUseHTMLString: true })
this.getList()
},
// 提交上传文件
submitFileForm() {
this.$refs.upload.submit()
}
async handleDelete(row) { await this.$confirm(`确认删除管理员“${row.username}”?`, '删除管理员', { type: 'warning' }); await delUser({ ids: [row.userId] }); this.msgSuccess('管理员已删除'); this.getList() }
}
}
</script>
<style scoped>
.system-page { min-height: 420px; }
.page-heading { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; margin-bottom: 20px; }
.page-heading h2 { margin: 0 0 6px; font-size: 20px; letter-spacing: 0; }
.page-heading p { margin: 0; color: var(--el-text-color-secondary); line-height: 1.5; }
.current-tag { margin-left: 8px; }
.mobile-user-list { display: none; }
@media (max-width: 720px) {
.page-heading { align-items: stretch; flex-direction: column; }
.page-heading .el-button { align-self: flex-start; }
.desktop-user-table { display: none; }
.mobile-user-list { display: block; }
.mobile-user-item { padding: 16px 0; border-bottom: 1px solid var(--el-border-color); }
.mobile-user-item:first-child { border-top: 1px solid var(--el-border-color); }
.mobile-user-heading { display: flex; align-items: center; gap: 8px; overflow-wrap: anywhere; }
.mobile-user-item dl { margin: 12px 0; }
.mobile-user-item dl div { display: grid; grid-template-columns: 56px minmax(0, 1fr); gap: 8px; margin-top: 6px; }
.mobile-user-item dt { color: var(--el-text-color-secondary); }
.mobile-user-item dd { margin: 0; overflow-wrap: anywhere; }
.mobile-user-actions { display: flex; flex-wrap: wrap; gap: 8px; }
}
</style>
@@ -0,0 +1,92 @@
import { expect, test } from '@playwright/test'
const baseURL = process.env.ADMIN_UI_URL || 'http://127.0.0.1:9528'
const group = (title: string, path: string, children: object[]) => ({
menuName: path.split('/').pop(),
title,
icon: 'ri:settings-3-line',
path,
component: 'Layout',
visible: '0',
children
})
const page = (title: string, path: string, component: string) => ({
menuName: path.split('/').pop(), title, path, component, visible: '0', children: []
})
const navigation = [
group('生成配置', '/chorus/configuration', [
page('上游服务商', '/chorus/providers', 'chorus/providers/index'),
page('模型配置', '/chorus/models', 'chorus/models/index'),
page('路由策略', '/chorus/routes', 'chorus/routes/index'),
page('提示词模板', '/chorus/templates', 'chorus/templates/index')
]),
group('运行监控', '/chorus/monitoring', [
page('上游健康', '/chorus/health', 'chorus/health/index'),
page('生成记录', '/chorus/generations', 'chorus/generations/index')
]),
group('用户与访问', '/chorus/access', [
page('终端用户', '/chorus/users', 'chorus/users/index'),
page('API 密钥', '/chorus/api-keys', 'chorus/api-keys/index')
]),
group('系统管理', '/chorus/system', [
page('管理员账号', '/chorus/system/admins', 'admin/sys-user/index'),
page('角色权限', '/chorus/system/roles', 'admin/sys-role/index'),
page('菜单结构', '/chorus/system/menus', 'admin/sys-menu/index'),
page('接口清单', '/chorus/system/apis', 'admin/sys-api/index'),
page('登录日志', '/chorus/system/login-logs', 'admin/sys-login-log/index')
])
]
test.beforeEach(async ({ context, page: browserPage }) => {
await context.addCookies([{ name: 'Admin-Token', value: 'test-token', domain: '127.0.0.1', path: '/' }])
await browserPage.route('**/api/v1/**', async route => {
const url = new URL(route.request().url())
let data: unknown = {}
if (url.pathname.endsWith('/getinfo')) {
data = { roles: ['admin'], permissions: ['*:*:*'], name: '本地管理员', avatar: '', introduction: '', userId: 1 }
} else if (url.pathname.endsWith('/menurole')) {
data = navigation
} else if (url.pathname.endsWith('/sys-user')) {
data = { list: [{ userId: 1, username: 'local-admin', nickName: '本地管理员', roleId: 1, status: '2' }], count: 1 }
} else if (url.pathname.endsWith('/role')) {
data = { list: [{ roleId: 1, roleName: 'Chorus Operator', roleKey: 'chorus_operator', roleSort: 100, status: '2' }], count: 1 }
} else if (url.pathname.endsWith('/menu')) {
data = navigation
} else if (url.pathname.endsWith('/sys-api') || url.pathname.endsWith('/sys-login-log')) {
data = { list: [], count: 0 }
}
await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ code: 200, data }) })
})
})
test('renders four groups and all thirteen confirmed entries', async ({ page: browserPage }) => {
await browserPage.goto(`${baseURL}/#/chorus/system/admins`)
await expect(browserPage.getByRole('heading', { name: '管理员账号' })).toBeVisible()
for (const title of ['生成配置', '运行监控', '用户与访问', '系统管理']) {
await expect(browserPage.getByText(title, { exact: true }).first()).toBeVisible()
}
for (const title of navigation.flatMap(item => item.children.map(child => child.title))) {
await expect(browserPage.getByText(title, { exact: true }).first()).toBeAttached()
}
await expect(browserPage.getByText('当前账号', { exact: true }).first()).toBeVisible()
await browserPage.screenshot({ path: 'test-results/system-admin-desktop.png', fullPage: true })
})
test('keeps system inventory pages read-only on desktop and narrow screens', async ({ page: browserPage }) => {
for (const [path, heading] of [['menus', '菜单结构'], ['apis', '接口清单'], ['login-logs', '登录日志']]) {
await browserPage.goto(`${baseURL}/#/chorus/system/${path}`)
await expect(browserPage.getByRole('heading', { name: heading })).toBeVisible()
await expect(browserPage.getByText('只读', { exact: true })).toBeVisible()
await expect(browserPage.getByRole('button', { name: /新增|编辑|删除/ })).toHaveCount(0)
}
await browserPage.setViewportSize({ width: 390, height: 844 })
await browserPage.goto(`${baseURL}/#/chorus/system/admins`)
await expect(browserPage.getByRole('heading', { name: '管理员账号' })).toBeVisible()
const overflow = await browserPage.evaluate(() => document.documentElement.scrollWidth > document.documentElement.clientWidth)
expect(overflow).toBeFalsy()
await browserPage.screenshot({ path: 'test-results/system-admin-narrow.png', fullPage: true })
})
@@ -0,0 +1,39 @@
import fs from 'fs'
import path from 'path'
const read = relativePath => fs.readFileSync(path.resolve(__dirname, `../../../${relativePath}`), 'utf8')
describe('Chorus system management pages', () => {
test.each([
['sys-menu', '菜单结构'],
['sys-api', '接口清单'],
['sys-login-log', '登录日志']
])('%s is explicitly read-only and exposes no write action', (view, heading) => {
const source = read(`src/views/admin/${view}/index.vue`)
expect(source).toContain(`<h2>${heading}</h2>`)
expect(source).toContain('>只读</el-tag>')
expect(source).not.toMatch(/handle(Add|Update|Delete)/)
expect(source).not.toMatch(/新增|编辑|删除[^。]/)
})
test('read-only API modules only issue GET requests', () => {
for (const api of ['sys-menu', 'sys-api', 'sys-login-log']) {
const source = read(`src/api/admin/${api}.js`)
expect(source).not.toMatch(/method: '(post|put|delete)'/)
}
})
test('administrator page omits legacy organization and contact dependencies', () => {
const source = read('src/views/admin/sys-user/index.vue')
expect(source).toContain('<h2>管理员账号</h2>')
expect(source).toContain('当前账号')
expect(source).not.toMatch(/部门|岗位|手机号|邮箱|sys-dept|sys-post/)
})
test('role page presents the built-in role protection boundary', () => {
const source = read('src/views/admin/sys-role/index.vue')
expect(source).toContain("const PROTECTED_ROLE_KEY = 'chorus_operator'")
expect(source).toContain('内置保护')
expect(source).not.toMatch(/数据权限|sys-dept/)
})
})