权限系统完善,实现维修日志信息自动填写功能,同时优化设备分配页面设计
This commit is contained in:
@ -15,13 +15,11 @@
|
||||
|
||||
<el-table :data="users" border style="width: 100%" v-loading="loading">
|
||||
<el-table-column prop="id" label="ID" width="80" align="center" />
|
||||
|
||||
<el-table-column prop="username" label="用户名" min-width="150">
|
||||
<template #default="{ row }">
|
||||
<span style="font-weight: bold;">{{ row.username }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="role" label="角色身份" width="150" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.role === 'admin'" type="danger" effect="dark">超级管理员</el-tag>
|
||||
@ -29,20 +27,17 @@
|
||||
<el-tag v-else type="info" effect="plain">普通客户</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="created_at" label="创建时间" min-width="180">
|
||||
<template #default="{ row }">
|
||||
{{ new Date(row.created_at).toLocaleString() }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="关联设备数" min-width="120" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.role === 'admin'" type="danger" effect="plain">全部权限</el-tag>
|
||||
<el-tag v-else effect="plain" type="success">{{ row.allowed_device_ids?.length || 0 }} 台</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="操作" width="220" align="center" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button
|
||||
@ -54,9 +49,8 @@
|
||||
>
|
||||
分配设备
|
||||
</el-button>
|
||||
|
||||
<el-popconfirm
|
||||
title="确定删除该用户吗? 此操作不可恢复。"
|
||||
title="确定删除该用户吗?"
|
||||
confirm-button-text="删除"
|
||||
cancel-button-text="取消"
|
||||
icon="Warning"
|
||||
@ -101,21 +95,68 @@
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-model="showPermissionModal" :title="`给 [${currentUser?.username}] 分配设备`" width="650px">
|
||||
<div class="permission-transfer">
|
||||
<el-transfer
|
||||
v-model="selectedDeviceIds"
|
||||
:data="allDevices"
|
||||
:titles="['可选设备', '已授权设备']"
|
||||
:props="{ key: 'id', label: 'label' }"
|
||||
filterable
|
||||
filter-placeholder="搜索设备名称"
|
||||
/>
|
||||
<el-dialog
|
||||
v-model="showPermissionModal"
|
||||
:title="`分配设备 - [${currentUser?.username}]`"
|
||||
width="720px"
|
||||
top="5vh"
|
||||
destroy-on-close
|
||||
>
|
||||
<div class="permission-wrapper">
|
||||
<div class="selection-toolbar">
|
||||
<el-input
|
||||
v-model="deviceFilterKeyword"
|
||||
placeholder="搜索设备名称或地点..."
|
||||
prefix-icon="Search"
|
||||
clearable
|
||||
style="width: 240px"
|
||||
/>
|
||||
|
||||
<div class="toolbar-stats">
|
||||
<span>已选: <span class="highlight-count">{{ selectedDeviceIds.length }}</span> / {{ allDevices.length }}</span>
|
||||
<el-divider direction="vertical" />
|
||||
<el-checkbox v-model="showSelectedOnly" label="只看已选" size="small" />
|
||||
</div>
|
||||
|
||||
<div class="toolbar-actions">
|
||||
<el-button link type="primary" @click="selectAllDevices">全选</el-button>
|
||||
<el-button link type="info" @click="clearAllDevices">清空</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="device-grid-container">
|
||||
<el-scrollbar max-height="450px">
|
||||
<div class="device-grid">
|
||||
<div
|
||||
v-for="device in displayDevices"
|
||||
:key="device.id"
|
||||
class="device-card"
|
||||
:class="{ 'is-active': selectedDeviceIds.includes(device.id) }"
|
||||
@click="toggleDeviceSelection(device.id)"
|
||||
>
|
||||
<div class="card-content">
|
||||
<div class="d-name">{{ device.name }}</div>
|
||||
<div class="d-site">
|
||||
<el-icon><Location /></el-icon> {{ device.install_site || '未分配地点' }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="check-mark" v-if="selectedDeviceIds.includes(device.id)">
|
||||
<el-icon><Check /></el-icon>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="displayDevices.length === 0" class="empty-tip">
|
||||
<el-empty description="没有找到匹配的设备" :image-size="80" />
|
||||
</div>
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="showPermissionModal = false">取消</el-button>
|
||||
<el-button type="primary" @click="savePermissions">保存设置</el-button>
|
||||
<el-button type="primary" @click="savePermissions">保存授权 ({{ selectedDeviceIds.length }})</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
@ -127,28 +168,47 @@ import { ref, onMounted, computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import request from '../utils/request'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { Back, Plus, Setting, Delete, Warning } from '@element-plus/icons-vue'
|
||||
import { Back, Plus, Setting, Delete, Warning, Search, Location, Check } from '@element-plus/icons-vue'
|
||||
|
||||
const router = useRouter()
|
||||
const loading = ref(false)
|
||||
const creating = ref(false)
|
||||
|
||||
const users = ref([])
|
||||
const rawDevices = ref([])
|
||||
const rawDevices = ref([]) // 原始设备数据
|
||||
const showCreateModal = ref(false)
|
||||
const showPermissionModal = ref(false)
|
||||
|
||||
// 默认新建角色为 client
|
||||
const newUser = ref({ username: '', password: '', role: 'client' })
|
||||
const currentUser = ref(null)
|
||||
const selectedDeviceIds = ref([])
|
||||
|
||||
// 转换设备数据供穿梭框使用
|
||||
const allDevices = computed(() => {
|
||||
return rawDevices.value.map(d => ({
|
||||
id: d.id,
|
||||
label: `${d.name} ${d.install_site ? '(' + d.install_site + ')' : ''}`
|
||||
}))
|
||||
// 🟢 新增/修改的状态变量
|
||||
const selectedDeviceIds = ref([]) // 存储当前选中的ID数组
|
||||
const deviceFilterKeyword = ref('') // 搜索关键词
|
||||
const showSelectedOnly = ref(false) // 是否只看已选
|
||||
|
||||
// 统一设备列表数据源
|
||||
const allDevices = computed(() => rawDevices.value)
|
||||
|
||||
// 🟢 核心计算逻辑:过滤设备列表
|
||||
const displayDevices = computed(() => {
|
||||
let list = allDevices.value
|
||||
|
||||
// 1. 关键词过滤
|
||||
if (deviceFilterKeyword.value) {
|
||||
const k = deviceFilterKeyword.value.toLowerCase()
|
||||
list = list.filter(d =>
|
||||
(d.name && d.name.toLowerCase().includes(k)) ||
|
||||
(d.install_site && d.install_site.toLowerCase().includes(k))
|
||||
)
|
||||
}
|
||||
|
||||
// 2. "只看已选"过滤
|
||||
if (showSelectedOnly.value) {
|
||||
list = list.filter(d => selectedDeviceIds.value.includes(d.id))
|
||||
}
|
||||
|
||||
return list
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
@ -161,34 +221,25 @@ const fetchUsers = async () => {
|
||||
try {
|
||||
const res = await request.get('/api/admin/users')
|
||||
users.value = res.data.data || res.data
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
} catch (e) { console.error(e) } finally { loading.value = false }
|
||||
}
|
||||
|
||||
const fetchAllDevices = async () => {
|
||||
try {
|
||||
const res = await request.get('/api/devices_overview')
|
||||
rawDevices.value = res.data.data || res.data
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
} catch (e) { console.error(e) }
|
||||
}
|
||||
|
||||
const openCreateModal = () => {
|
||||
// 每次打开重置表单
|
||||
newUser.value = {username: '', password: '', role: 'client'}
|
||||
showCreateModal.value = true
|
||||
}
|
||||
|
||||
const createUser = async () => {
|
||||
if (!newUser.value.username || !newUser.value.password) return ElMessage.warning('请填写完整')
|
||||
|
||||
creating.value = true
|
||||
try {
|
||||
// 发送 role 到后端,数据库直接存入
|
||||
await request.post('/api/admin/create_user', newUser.value)
|
||||
ElMessage.success('用户创建成功')
|
||||
showCreateModal.value = false
|
||||
@ -200,12 +251,40 @@ const createUser = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 🟢 打开分配弹窗
|
||||
const openPermissionModal = (user) => {
|
||||
currentUser.value = user
|
||||
selectedDeviceIds.value = user.allowed_device_ids || []
|
||||
// 确保是新数组,避免引用污染
|
||||
selectedDeviceIds.value = user.allowed_device_ids ? [...user.allowed_device_ids] : []
|
||||
// 重置筛选状态
|
||||
deviceFilterKeyword.value = ''
|
||||
showSelectedOnly.value = false
|
||||
showPermissionModal.value = true
|
||||
}
|
||||
|
||||
// 🟢 切换单个选中状态
|
||||
const toggleDeviceSelection = (id) => {
|
||||
const index = selectedDeviceIds.value.indexOf(id)
|
||||
if (index > -1) {
|
||||
selectedDeviceIds.value.splice(index, 1)
|
||||
} else {
|
||||
selectedDeviceIds.value.push(id)
|
||||
}
|
||||
}
|
||||
|
||||
// 🟢 全选(基于当前过滤后的列表)
|
||||
const selectAllDevices = () => {
|
||||
const currentIds = displayDevices.value.map(d => d.id)
|
||||
// 将未选中的添加进去(Set去重)
|
||||
const newSet = new Set([...selectedDeviceIds.value, ...currentIds])
|
||||
selectedDeviceIds.value = Array.from(newSet)
|
||||
}
|
||||
|
||||
// 🟢 清空(全部清空)
|
||||
const clearAllDevices = () => {
|
||||
selectedDeviceIds.value = []
|
||||
}
|
||||
|
||||
const savePermissions = async () => {
|
||||
try {
|
||||
await request.post('/api/admin/assign_devices', {
|
||||
@ -263,19 +342,140 @@ const deleteUser = async (id) => {
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
:deep(.el-transfer-panel) {
|
||||
width: 250px;
|
||||
/* 🟢 新增:权限选择器样式 */
|
||||
.permission-wrapper {
|
||||
background: #fff;
|
||||
border: 1px solid #e4e7ed;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
:deep(.el-transfer-panel) {
|
||||
width: 100%;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.selection-toolbar {
|
||||
padding: 10px 15px;
|
||||
background: #f5f7fa;
|
||||
border-bottom: 1px solid #e4e7ed;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.permission-transfer {
|
||||
display: flex;
|
||||
.toolbar-stats {
|
||||
font-size: 13px;
|
||||
color: #606266;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.highlight-count {
|
||||
color: #409EFF;
|
||||
font-weight: bold;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.toolbar-actions {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.device-grid-container {
|
||||
padding: 15px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.device-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.device-card {
|
||||
position: relative;
|
||||
border: 1px solid #dcdfe6;
|
||||
border-radius: 6px;
|
||||
padding: 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
background: #fff;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.device-card:hover {
|
||||
border-color: #c6e2ff;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
|
||||
}
|
||||
|
||||
.device-card.is-active {
|
||||
border-color: #409EFF;
|
||||
background-color: #ecf5ff;
|
||||
}
|
||||
|
||||
.card-content {
|
||||
pointer-events: none; /* 让点击穿透到底层div */
|
||||
}
|
||||
|
||||
.d-name {
|
||||
font-weight: bold;
|
||||
color: #303133;
|
||||
font-size: 14px;
|
||||
margin-bottom: 6px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.d-site {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
/* 右下角打钩图标 */
|
||||
.check-mark {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-style: solid;
|
||||
border-width: 0 28px 28px 0;
|
||||
border-color: transparent #409EFF transparent transparent;
|
||||
}
|
||||
|
||||
.check-mark .el-icon {
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
right: -26px;
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.empty-tip {
|
||||
grid-column: 1 / -1;
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
/* 移动端适配 */
|
||||
@media screen and (max-width: 768px) {
|
||||
.selection-toolbar {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: 10px;
|
||||
}
|
||||
.toolbar-actions {
|
||||
margin-left: 0;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.device-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user